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) + + +def note(text): + 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)}
  • "), append=True) diff --git a/examples/frozenlist/hashable_when_frozen/config.toml b/examples/frozenlist/hashable_when_frozen/config.toml new file mode 100644 index 0000000..138061c --- /dev/null +++ b/examples/frozenlist/hashable_when_frozen/config.toml @@ -0,0 +1 @@ +packages = ["frozenlist"] diff --git a/examples/frozenlist/hashable_when_frozen/setup.py b/examples/frozenlist/hashable_when_frozen/setup.py new file mode 100644 index 0000000..ca3fca8 --- /dev/null +++ b/examples/frozenlist/hashable_when_frozen/setup.py @@ -0,0 +1,22 @@ +"""Lightweight setup for example 2: re-establish names from cell 1.""" +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 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" +]