Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/pyrsistent/README.md
Original file line number Diff line number Diff line change
@@ -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.
90 changes: 90 additions & 0 deletions examples/pyrsistent/deep_transformations/code.py
Original file line number Diff line number Diff line change
@@ -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 <em>key</em> 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)")
1 change: 1 addition & 0 deletions examples/pyrsistent/deep_transformations/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pyrsistent"]
19 changes: 19 additions & 0 deletions examples/pyrsistent/deep_transformations/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
67 changes: 67 additions & 0 deletions examples/pyrsistent/immutable_basics/code.py
Original file line number Diff line number Diff line change
@@ -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)}")
1 change: 1 addition & 0 deletions examples/pyrsistent/immutable_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pyrsistent"]
41 changes: 41 additions & 0 deletions examples/pyrsistent/immutable_basics/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

5 changes: 5 additions & 0 deletions examples/pyrsistent/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"immutable_basics",
"records_and_invariants",
"deep_transformations"
]
78 changes: 78 additions & 0 deletions examples/pyrsistent/records_and_invariants/code.py
Original file line number Diff line number Diff line change
@@ -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'])}")
1 change: 1 addition & 0 deletions examples/pyrsistent/records_and_invariants/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pyrsistent"]
20 changes: 20 additions & 0 deletions examples/pyrsistent/records_and_invariants/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)