From 51ac14e2438c5705da44b3a2c6c6f2ef269e47f7 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:14 +0100 Subject: [PATCH 1/2] Add PyScript examples for pyrsistent Generated by apply_llm_response.py from prompts/pyrsistent/response.toml. Examples included: - immutable_basics: Immutable basics: PVector and PMap - records_and_invariants: PRecord: typed fields and invariants - deep_transformations: Deep updates with transform() Generated-By: apply_llm_response.py --- examples/pyrsistent/README.md | 18 +++++ .../pyrsistent/deep_transformations/code.py | 81 +++++++++++++++++++ .../deep_transformations/config.toml | 1 + .../pyrsistent/deep_transformations/setup.py | 22 +++++ examples/pyrsistent/immutable_basics/code.py | 63 +++++++++++++++ .../pyrsistent/immutable_basics/config.toml | 1 + examples/pyrsistent/immutable_basics/setup.py | 45 +++++++++++ examples/pyrsistent/order.json | 5 ++ .../pyrsistent/records_and_invariants/code.py | 72 +++++++++++++++++ .../records_and_invariants/config.toml | 1 + .../records_and_invariants/setup.py | 26 ++++++ 11 files changed, 335 insertions(+) create mode 100644 examples/pyrsistent/README.md create mode 100644 examples/pyrsistent/deep_transformations/code.py create mode 100644 examples/pyrsistent/deep_transformations/config.toml create mode 100644 examples/pyrsistent/deep_transformations/setup.py create mode 100644 examples/pyrsistent/immutable_basics/code.py create mode 100644 examples/pyrsistent/immutable_basics/config.toml create mode 100644 examples/pyrsistent/immutable_basics/setup.py create mode 100644 examples/pyrsistent/order.json create mode 100644 examples/pyrsistent/records_and_invariants/code.py create mode 100644 examples/pyrsistent/records_and_invariants/config.toml create mode 100644 examples/pyrsistent/records_and_invariants/setup.py 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..d58052f --- /dev/null +++ b/examples/pyrsistent/deep_transformations/code.py @@ -0,0 +1,81 @@ +# --------------------------------------------------------------------- +# transform() lets you update deeply nested immutable structures with +# a path + a transformation. Paths can include literal keys/indices, +# the `ny` matcher (any element), regex matchers, and predicates. +# --------------------------------------------------------------------- + +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, by index, and by predicate") +note( + "ny matches every element in a collection; a callable matches " + "elements where it returns True. inc is a built-in transform " + "that adds 1." +) + +# 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. Regex matchers and the discard sentinel") +note( + "rex matches keys by regular expression. The discard sentinel " + "removes the matched element entirely." +) + +# Anonymize authors whose names start with 'S'. +anonymized = newspaper.transform( + ["articles", lambda a: a["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." +) + +note(f"Untouched article shared? " + f"{bumped['articles'][0] is not newspaper['articles'][0]} " + f"(views changed, so a new article object was created)") +note(f"Weather shared with bumped? " + f"{bumped['weather'] is newspaper['weather']} " + f"(weather wasn't touched, so it's the same object)") 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..7121bcf --- /dev/null +++ b/examples/pyrsistent/deep_transformations/setup.py @@ -0,0 +1,22 @@ +"""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) + + +from pyrsistent import freeze, thaw, ny, rex, discard, inc diff --git a/examples/pyrsistent/immutable_basics/code.py b/examples/pyrsistent/immutable_basics/code.py new file mode 100644 index 0000000..cc78e81 --- /dev/null +++ b/examples/pyrsistent/immutable_basics/code.py @@ -0,0 +1,63 @@ +""" +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 + +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..d620e87 --- /dev/null +++ b/examples/pyrsistent/immutable_basics/setup.py @@ -0,0 +1,45 @@ +""" +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) + + +# Pyrsistent provides immutable, "persistent" data structures inspired +# by Clojure. We import the most common building blocks here. +from pyrsistent import v, pvector, m, pmap, s, pset, freeze, thaw 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..15e18a4 --- /dev/null +++ b/examples/pyrsistent/records_and_invariants/code.py @@ -0,0 +1,72 @@ +# --------------------------------------------------------------------- +# PRecord: like a PMap, but with declared fields, types, and invariants. +# Great for modeling domain entities you want to keep honest. +# --------------------------------------------------------------------- + +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..698bc74 --- /dev/null +++ b/examples/pyrsistent/records_and_invariants/setup.py @@ -0,0 +1,26 @@ +"""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) + + +from pyrsistent import ( + PRecord, field, pvector_field, pmap_field, + InvariantException, PTypeError, v, m, freeze, thaw, +) From 25fe2e20614a6c59525f0767c2283d3b9dad8af5 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 11 Jun 2026 15:36:13 +0100 Subject: [PATCH 2/2] Fix final example and imports. --- .../pyrsistent/deep_transformations/code.py | 39 ++++++++++++------- .../pyrsistent/deep_transformations/setup.py | 3 -- examples/pyrsistent/immutable_basics/code.py | 4 ++ examples/pyrsistent/immutable_basics/setup.py | 4 -- .../pyrsistent/records_and_invariants/code.py | 6 +++ .../records_and_invariants/setup.py | 6 --- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/examples/pyrsistent/deep_transformations/code.py b/examples/pyrsistent/deep_transformations/code.py index d58052f..aed9ade 100644 --- a/examples/pyrsistent/deep_transformations/code.py +++ b/examples/pyrsistent/deep_transformations/code.py @@ -1,8 +1,9 @@ # --------------------------------------------------------------------- # transform() lets you update deeply nested immutable structures with # a path + a transformation. Paths can include literal keys/indices, -# the `ny` matcher (any element), regex matchers, and predicates. +# the `ny` matcher (any element), and callable predicates. # --------------------------------------------------------------------- +from pyrsistent import freeze, thaw, ny, discard, inc heading("1. A nested newspaper") note( @@ -24,11 +25,11 @@ note(f"Original first article: {newspaper['articles'][0]}") -heading("2. Update by path, by index, and by predicate") +heading("2. Update by path with ny and a transform function") note( - "ny matches every element in a collection; a callable matches " - "elements where it returns True. inc is a built-in transform " - "that adds 1." + "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. @@ -46,15 +47,22 @@ def truncate(text): for article in shortened["articles"]: note(f"- {article['author']}: {article['content']!r}") -heading("3. Regex matchers and the discard sentinel") +heading("3. Callable predicates and the discard sentinel") note( - "rex matches keys by regular expression. The discard sentinel " - "removes the matched element entirely." + "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'. +# 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 a: a["author"].startswith("S"), "author"], + ["articles", lambda i: articles[i]["author"].startswith("S"), "author"], "anonymous", ) for article in anonymized["articles"]: @@ -70,12 +78,13 @@ def truncate(text): heading("4. Structural sharing") note( "When a sub-structure isn't touched, the new value reuses the " - "old object identity -- no copy is made." + "old object identity -- no copy is made. Touched sub-structures " + "get new objects, but their untouched siblings stay shared." ) -note(f"Untouched article 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 object was created)") -note(f"Weather shared with bumped? " + 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 it's the same object)") + f"(weather wasn't touched, so the same PMap is reused)") \ No newline at end of file diff --git a/examples/pyrsistent/deep_transformations/setup.py b/examples/pyrsistent/deep_transformations/setup.py index 7121bcf..558d69c 100644 --- a/examples/pyrsistent/deep_transformations/setup.py +++ b/examples/pyrsistent/deep_transformations/setup.py @@ -17,6 +17,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -from pyrsistent import freeze, thaw, ny, rex, discard, inc diff --git a/examples/pyrsistent/immutable_basics/code.py b/examples/pyrsistent/immutable_basics/code.py index cc78e81..34ba033 100644 --- a/examples/pyrsistent/immutable_basics/code.py +++ b/examples/pyrsistent/immutable_basics/code.py @@ -8,6 +8,10 @@ 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( diff --git a/examples/pyrsistent/immutable_basics/setup.py b/examples/pyrsistent/immutable_basics/setup.py index d620e87..0cc7a6f 100644 --- a/examples/pyrsistent/immutable_basics/setup.py +++ b/examples/pyrsistent/immutable_basics/setup.py @@ -39,7 +39,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -# Pyrsistent provides immutable, "persistent" data structures inspired -# by Clojure. We import the most common building blocks here. -from pyrsistent import v, pvector, m, pmap, s, pset, freeze, thaw diff --git a/examples/pyrsistent/records_and_invariants/code.py b/examples/pyrsistent/records_and_invariants/code.py index 15e18a4..a54d802 100644 --- a/examples/pyrsistent/records_and_invariants/code.py +++ b/examples/pyrsistent/records_and_invariants/code.py @@ -3,6 +3,12 @@ # 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. " diff --git a/examples/pyrsistent/records_and_invariants/setup.py b/examples/pyrsistent/records_and_invariants/setup.py index 698bc74..e95f6e3 100644 --- a/examples/pyrsistent/records_and_invariants/setup.py +++ b/examples/pyrsistent/records_and_invariants/setup.py @@ -18,9 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -from pyrsistent import ( - PRecord, field, pvector_field, pmap_field, - InvariantException, PTypeError, v, m, freeze, thaw, -)