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) 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) + 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)