diff --git a/examples/pyrsistent/README.md b/examples/pyrsistent/README.md new file mode 100644 index 0000000..7230a15 --- /dev/null +++ b/examples/pyrsistent/README.md @@ -0,0 +1,18 @@ +# pyrsistent Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/pyrsistent/deep_transformations/code.py b/examples/pyrsistent/deep_transformations/code.py new file mode 100644 index 0000000..aed9ade --- /dev/null +++ b/examples/pyrsistent/deep_transformations/code.py @@ -0,0 +1,90 @@ +# --------------------------------------------------------------------- +# transform() lets you update deeply nested immutable structures with +# a path + a transformation. Paths can include literal keys/indices, +# the `ny` matcher (any element), and callable predicates. +# --------------------------------------------------------------------- +from pyrsistent import freeze, thaw, ny, discard, inc + +heading("1. A nested newspaper") +note( + "We freeze a plain Python dict into nested PMaps and PVectors so " + "we can demonstrate path-based updates." +) + +newspaper = freeze({ + "edition": "2026-03-15", + "articles": [ + {"author": "Sara", "views": 120, "content": "A short article"}, + {"author": "Steve", "views": 45, + "content": "A slightly longer article about the weather"}, + {"author": "Joan", "views": 300, + "content": "Front page scoop with all the details"}, + ], + "weather": {"temperature": "11C", "wind": "5m/s"}, +}) + +note(f"Original first article: {newspaper['articles'][0]}") + +heading("2. Update by path with ny and a transform function") +note( + "ny matches every element in a collection. inc is a built-in " + "transform that adds 1. Custom transforms are just functions " + "from old value to new value." +) + +# Bump the view count of every article by 1. +bumped = newspaper.transform(["articles", ny, "views"], inc) +note(f"Views after bump: " + f"{[a['views'] for a in bumped['articles']]}") + +# Truncate any content longer than 25 characters. +def truncate(text): + return text if len(text) <= 25 else text[:22] + "..." + +shortened = newspaper.transform( + ["articles", ny, "content"], truncate, +) +for article in shortened["articles"]: + note(f"- {article['author']}: {article['content']!r}") + +heading("3. Callable predicates and the discard sentinel") +note( + "A callable in a path is a key matcher: pyrsistent passes it " + "each key of the current structure and keeps those for " + "which it returns True. For a PVector, the keys are integer " + "indices -- not the elements -- so a predicate that needs to " + "inspect element contents must look the element up itself. " + "The discard sentinel removes the matched element entirely." +) + +# Anonymize authors whose names start with 'S'. Because the predicate +# receives an *index* (not the article), we close over `articles` to +# look up the element. This is a real pyrsistent gotcha worth knowing. +articles = newspaper["articles"] +anonymized = newspaper.transform( + ["articles", lambda i: articles[i]["author"].startswith("S"), "author"], + "anonymous", +) +for article in anonymized["articles"]: + note(f"- {article['author']} ({article['views']} views)") + +# Drop the weather block and every article's content in one call. +trimmed = newspaper.transform( + ["weather"], discard, + ["articles", ny, "content"], discard, +) +note(f"Trimmed structure: {thaw(trimmed)}") + +heading("4. Structural sharing") +note( + "When a sub-structure isn't touched, the new value reuses the " + "old object identity -- no copy is made. Touched sub-structures " + "get new objects, but their untouched siblings stay shared." +) + +note(f"First article is a new object after bump? " + f"{bumped['articles'][0] is not newspaper['articles'][0]} " + f"(views changed, so a new article PMap was created)") +note(f"Weather is shared with bumped? " + f"{bumped['weather'] is newspaper['weather']} " + f"(weather wasn't touched, so the same PMap is reused)") \ No newline at end of file diff --git a/examples/pyrsistent/deep_transformations/config.toml b/examples/pyrsistent/deep_transformations/config.toml new file mode 100644 index 0000000..161d04f --- /dev/null +++ b/examples/pyrsistent/deep_transformations/config.toml @@ -0,0 +1 @@ +packages = ["pyrsistent"] diff --git a/examples/pyrsistent/deep_transformations/setup.py b/examples/pyrsistent/deep_transformations/setup.py new file mode 100644 index 0000000..558d69c --- /dev/null +++ b/examples/pyrsistent/deep_transformations/setup.py @@ -0,0 +1,19 @@ +"""Setup for the transformations example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/pyrsistent/immutable_basics/code.py b/examples/pyrsistent/immutable_basics/code.py new file mode 100644 index 0000000..34ba033 --- /dev/null +++ b/examples/pyrsistent/immutable_basics/code.py @@ -0,0 +1,67 @@ +""" +A first look at pyrsistent: persistent (immutable) collections. + +Every "mutating" operation returns a new structure and leaves the +original untouched. Behind the scenes the new and old versions share +most of their data, so this is fast and memory-efficient. + +Docs: https://pyrsistent.readthedocs.org/ +""" +from IPython.core.display import display, HTML +# Pyrsistent provides immutable, "persistent" data structures inspired +# by Clojure. We import the most common building blocks here. +from pyrsistent import v, m, pmap, s, freeze, thaw + + +heading("1. PVector: an immutable list") +note( + "We start with a small shopping list. Each 'change' returns a " + "brand-new vector; the originals never change." +) + +shopping = v("apples", "bread", "cheese") +with_eggs = shopping.append("eggs") +swapped = with_eggs.set(1, "sourdough") + +note(f"Original: {shopping}") +note(f"After append: {with_eggs}") +note(f"After set(1): {swapped}") +note(f"shopping is unchanged: {shopping == v('apples', 'bread', 'cheese')}") + +# PVector supports the full Sequence protocol: indexing, slicing, +# iteration, len, etc. +note(f"swapped[1:3] = {swapped[1:3]}, len = {len(swapped)}") + +heading("2. PMap: an immutable dict") +note( + "A tiny inventory keyed by SKU. We 'evolve' the map by setting " + "and updating; the original map keeps its values." +) + +inventory = m(apples=12, bread=4, cheese=7) +restocked = inventory.set("bread", 20) +combined = restocked.update(m(eggs=18, cheese=10)) + +note(f"Original: {inventory}") +note(f"Restocked: {restocked}") +note(f"Combined: {combined}") + +# PMaps are hashable, so they can be used as dict keys or set members, +# unlike Python's built-in dict. +warehouse = pmap({combined: "Warehouse A", inventory: "Warehouse B"}) +note(f"PMaps as keys works: {len(warehouse)} warehouses indexed.") + +heading("3. PSet and freeze/thaw") +note( + "freeze() recursively converts plain Python containers into " + "pyrsistent ones; thaw() converts back." +) + +tags = s("fresh", "local", "fresh") # duplicates collapse, like set +note(f"PSet (duplicates dropped): {tags}") + +raw = {"name": "Market", "items": [{"sku": "A1", "qty": 3}]} +frozen = freeze(raw) +note(f"Frozen: {frozen}") +note(f"Type of inner list: {type(frozen['items']).__name__}") +note(f"Thawed back to plain Python: {thaw(frozen)}") diff --git a/examples/pyrsistent/immutable_basics/config.toml b/examples/pyrsistent/immutable_basics/config.toml new file mode 100644 index 0000000..161d04f --- /dev/null +++ b/examples/pyrsistent/immutable_basics/config.toml @@ -0,0 +1 @@ +packages = ["pyrsistent"] diff --git a/examples/pyrsistent/immutable_basics/setup.py b/examples/pyrsistent/immutable_basics/setup.py new file mode 100644 index 0000000..0cc7a6f --- /dev/null +++ b/examples/pyrsistent/immutable_basics/setup.py @@ -0,0 +1,41 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/pyrsistent/order.json b/examples/pyrsistent/order.json new file mode 100644 index 0000000..4b63fbe --- /dev/null +++ b/examples/pyrsistent/order.json @@ -0,0 +1,5 @@ +[ + "immutable_basics", + "records_and_invariants", + "deep_transformations" +] diff --git a/examples/pyrsistent/records_and_invariants/code.py b/examples/pyrsistent/records_and_invariants/code.py new file mode 100644 index 0000000..a54d802 --- /dev/null +++ b/examples/pyrsistent/records_and_invariants/code.py @@ -0,0 +1,78 @@ +# --------------------------------------------------------------------- +# PRecord: like a PMap, but with declared fields, types, and invariants. +# Great for modeling domain entities you want to keep honest. +# --------------------------------------------------------------------- + +from pyrsistent import ( + PRecord, field, pvector_field, pmap_field, + InvariantException, PTypeError, v, thaw, +) + + +heading("1. Declaring a record with typed fields") +note( + "A Book has a title, a positive page count, and a list of tags. " + "Field declarations enforce types and simple invariants." +) + + +class Book(PRecord): + title = field(type=str, mandatory=True) + pages = field( + type=int, + invariant=lambda n: (n > 0, "pages must be positive"), + ) + tags = pvector_field(str) + + +hobbit = Book(title="The Hobbit", pages=310, tags=v("fantasy", "classic")) +note(f"Created: {hobbit}") +note(f"Field access: hobbit.title = {hobbit.title!r}, " + f"pages = {hobbit.pages}") + +# "Mutation" returns a new record; the original is untouched. +revised = hobbit.set(pages=320) +note(f"Revised: {revised}") +note(f"Original still has {hobbit.pages} pages.") + +heading("2. Type and invariant enforcement") +note("Bad data is rejected at construction or update time.") + +try: + Book(title="Bad", pages=-5) +except InvariantException as exc: + note(f"InvariantException: {exc.invariant_errors}") + +try: + hobbit.set(pages="three hundred") +except PTypeError as exc: + note(f"PTypeError: {exc}") + +heading("3. Nested records and the create() factory") +note( + "PRecord.create() builds a record (and any nested records) from " + "plain Python data -- handy for parsing JSON-like input." +) + + +class Library(PRecord): + name = field(type=str) + books_by_id = pmap_field(str, Book) + + +raw_data = { + "name": "Riverside Branch", + "books_by_id": { + "B001": {"title": "The Hobbit", "pages": 310, + "tags": ["fantasy", "classic"]}, + "B002": {"title": "Dune", "pages": 688, + "tags": ["sci-fi"]}, + }, +} + +library = Library.create(raw_data) +note(f"Library name: {library.name}") +note(f"Book B002: {library.books_by_id['B002']}") + +# Records serialize cleanly back to plain Python dicts. +note(f"Round-trip via thaw(): {thaw(library.books_by_id['B001'])}") diff --git a/examples/pyrsistent/records_and_invariants/config.toml b/examples/pyrsistent/records_and_invariants/config.toml new file mode 100644 index 0000000..161d04f --- /dev/null +++ b/examples/pyrsistent/records_and_invariants/config.toml @@ -0,0 +1 @@ +packages = ["pyrsistent"] diff --git a/examples/pyrsistent/records_and_invariants/setup.py b/examples/pyrsistent/records_and_invariants/setup.py new file mode 100644 index 0000000..e95f6e3 --- /dev/null +++ b/examples/pyrsistent/records_and_invariants/setup.py @@ -0,0 +1,20 @@ +"""Setup for the PRecord example. No IPython shim here; the first +example already registered it for the notebook session.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True)