diff --git a/examples/frozenlist/README.md b/examples/frozenlist/README.md
new file mode 100644
index 0000000..6893d95
--- /dev/null
+++ b/examples/frozenlist/README.md
@@ -0,0 +1,18 @@
+# frozenlist 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/frozenlist/freeze_a_playlist/code.py b/examples/frozenlist/freeze_a_playlist/code.py
new file mode 100644
index 0000000..888d3d5
--- /dev/null
+++ b/examples/frozenlist/freeze_a_playlist/code.py
@@ -0,0 +1,54 @@
+"""
+A first look at frozenlist.
+
+`FrozenList` behaves like a regular list, but once you call
+`freeze()` on it, any further mutation raises `RuntimeError`.
+This is handy when you want to assemble a collection during
+setup and then guarantee it won't be modified afterwards.
+
+Docs: https://frozenlist.aio-libs.org
+"""
+from IPython.core.display import display, HTML
+
+# Package import for this example.
+from frozenlist import FrozenList
+
+
+# Build a playlist mutably, the same way you'd build a regular list.
+playlist = FrozenList()
+playlist.append("Take Five")
+playlist.append("So What")
+playlist.append("Blue in Green")
+playlist.extend(["All Blues", "Freddie Freeloader"])
+
+heading("Building the playlist")
+note(
+ "Before freezing, the FrozenList accepts all the usual "
+ "MutableSequence operations: append, extend, insert, "
+ "__setitem__, and so on."
+)
+note(f"Tracks so far: {list(playlist)}")
+note(f"Frozen? {playlist.frozen}")
+
+# Lock it down. There is no thaw -- this is a one-way trip.
+playlist.freeze()
+
+heading("After freeze()")
+note(f"Frozen? {playlist.frozen}")
+
+# Read access still works perfectly.
+note(f"First track: {playlist[0]}")
+note(f"Number of tracks: {len(playlist)}")
+note(f"Contains 'So What'? {'So What' in playlist}")
+
+# Any attempt to mutate now raises RuntimeError.
+heading("Mutation is now an error")
+try:
+ playlist.append("Milestones")
+except RuntimeError as exc:
+ note(f"playlist.append(...) raised: {exc}")
+
+try:
+ playlist[0] = "Kind of Blue"
+except RuntimeError as exc:
+ note(f"playlist[0] = ... raised: {exc}")
diff --git a/examples/frozenlist/freeze_a_playlist/config.toml b/examples/frozenlist/freeze_a_playlist/config.toml
new file mode 100644
index 0000000..138061c
--- /dev/null
+++ b/examples/frozenlist/freeze_a_playlist/config.toml
@@ -0,0 +1 @@
+packages = ["frozenlist"]
diff --git a/examples/frozenlist/freeze_a_playlist/setup.py b/examples/frozenlist/freeze_a_playlist/setup.py
new file mode 100644
index 0000000..74ae7f6
--- /dev/null
+++ b/examples/frozenlist/freeze_a_playlist/setup.py
@@ -0,0 +1,37 @@
+"""Shim IPython's display API onto PyScript and import frozenlist."""
+import sys
+import types
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ """Wrap pyscript.display so output lands in the example target."""
+ 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/frozenlist/hashable_when_frozen/code.py b/examples/frozenlist/hashable_when_frozen/code.py new file mode 100644 index 0000000..e7bd909 --- /dev/null +++ b/examples/frozenlist/hashable_when_frozen/code.py @@ -0,0 +1,62 @@ +# --------------------------------------------------------------------- +# Frozen FrozenLists are hashable, so they can be dict keys or set +# members. Unfrozen ones are not -- hashing them raises RuntimeError. +# --------------------------------------------------------------------- + +heading("Why hashability matters") +note( + "Imagine tagging recipes by their ingredient lists. If the " + "ingredient list is a regular Python list, you can't use it " + "as a dict key. A frozen FrozenList works as a key, because " + "freezing makes it hashable." +) + +# A few recipes, each described by an ordered ingredient list. +raw_recipes = { + "Margherita pizza": ["flour", "tomato", "mozzarella", "basil"], + "Caprese salad": ["tomato", "mozzarella", "basil"], + "Tomato bruschetta":["bread", "tomato", "basil", "olive oil"], + "Cheese toastie": ["bread", "mozzarella"], +} + + +def freeze_ingredients(items): + """Build a FrozenList from items and freeze it before returning.""" + fl = FrozenList(items) + fl.freeze() + return fl + + +# Try hashing an unfrozen list to show it fails. +unfrozen = FrozenList(["flour", "water"]) +try: + hash(unfrozen) +except RuntimeError as exc: + note(f"Hashing an unfrozen FrozenList raised:{exc}")
+
+# Now invert the mapping: ingredient list -> recipe name.
+by_ingredients = {
+ freeze_ingredients(ingredients): name
+ for name, ingredients in raw_recipes.items()
+}
+
+heading("Looking up a recipe by its ingredients")
+query = freeze_ingredients(["tomato", "mozzarella", "basil"])
+note(f"Query (frozen): {query}")
+note(f"Hash of query: {hash(query)}")
+note(f"Recipe found: {by_ingredients.get(query)}")
+
+# Frozen FrozenLists also work happily inside sets, which lets us
+# deduplicate ingredient lists across many recipes.
+heading("Deduplicating with a set")
+all_ingredient_lists = {
+ freeze_ingredients(ings) for ings in raw_recipes.values()
+}
+all_ingredient_lists.add(freeze_ingredients(["tomato", "mozzarella", "basil"]))
+note(
+ f"Started with {len(raw_recipes)} recipes, "
+ f"set contains {len(all_ingredient_lists)} unique "
+ f"ingredient lists (the duplicate Caprese was collapsed)."
+)
+for ings in sorted(all_ingredient_lists, key=len):
+ display(HTML(f"{list(ings)}{text}
"), append=True) + + +from frozenlist import FrozenList diff --git a/examples/frozenlist/order.json b/examples/frozenlist/order.json new file mode 100644 index 0000000..eb30d85 --- /dev/null +++ b/examples/frozenlist/order.json @@ -0,0 +1,4 @@ +[ + "freeze_a_playlist", + "hashable_when_frozen" +]