From 440486bc8704e9a12c4e1e95d0841401ac60b3a6 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:12 +0100 Subject: [PATCH 1/2] Add PyScript examples for propcache Generated by apply_llm_response.py from prompts/propcache/response.toml. Examples included: - cached_property_basics: Cached property basics - under_cached_property: under_cached_property: cache in self._cache Generated-By: apply_llm_response.py --- examples/propcache/README.md | 18 +++++ .../propcache/cached_property_basics/code.py | 57 ++++++++++++++++ .../cached_property_basics/config.toml | 1 + .../propcache/cached_property_basics/setup.py | 46 +++++++++++++ examples/propcache/order.json | 4 ++ .../propcache/under_cached_property/code.py | 68 +++++++++++++++++++ .../under_cached_property/config.toml | 1 + .../propcache/under_cached_property/setup.py | 22 ++++++ 8 files changed, 217 insertions(+) create mode 100644 examples/propcache/README.md create mode 100644 examples/propcache/cached_property_basics/code.py create mode 100644 examples/propcache/cached_property_basics/config.toml create mode 100644 examples/propcache/cached_property_basics/setup.py create mode 100644 examples/propcache/order.json create mode 100644 examples/propcache/under_cached_property/code.py create mode 100644 examples/propcache/under_cached_property/config.toml create mode 100644 examples/propcache/under_cached_property/setup.py diff --git a/examples/propcache/README.md b/examples/propcache/README.md new file mode 100644 index 0000000..51a868b --- /dev/null +++ b/examples/propcache/README.md @@ -0,0 +1,18 @@ +# propcache 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/propcache/cached_property_basics/code.py b/examples/propcache/cached_property_basics/code.py new file mode 100644 index 0000000..e288666 --- /dev/null +++ b/examples/propcache/cached_property_basics/code.py @@ -0,0 +1,57 @@ +""" +A first look at propcache. + +`propcache.api.cached_property` works just like the standard library's +`functools.cached_property`: the decorated method runs once per +instance, and its result is then stored on the instance and returned +directly on subsequent accesses. propcache is a faster, C-accelerated +implementation aimed at hot code paths. + +See https://propcache.readthedocs.io for full documentation. +""" +from IPython.core.display import display, HTML + +heading("A weather station with an expensive computation") +note( + "Imagine each `WeatherStation` reads many raw temperature samples " + "and we want a daily average. The average never changes for a " + "given station, so we cache it the first time it's asked for." +) + + +class WeatherStation: + """A station whose daily average is computed at most once.""" + + def __init__(self, name, samples): + self.name = name + self.samples = samples + self.compute_count = 0 + + @cached_property + def daily_average(self): + """Pretend this is an expensive aggregation over many samples.""" + self.compute_count += 1 + return sum(self.samples) / len(self.samples) + + +station = WeatherStation( + "Reykjavik", + samples=[1.2, 0.8, -0.4, 2.1, 3.0, 1.7, 0.9, -0.2, 1.5, 2.4], +) + +note("First access computes the value:") +display(f"daily_average = {station.daily_average:.2f}") +display(f"compute_count = {station.compute_count}") + +note("Subsequent accesses return the cached value without recomputing:") +for _ in range(5): + _ = station.daily_average +display(f"compute_count after 5 more reads = {station.compute_count}") + +note( + "The cached value lives in the instance's __dict__, " + "so deleting it forces the next access to recompute." +) +del station.__dict__["daily_average"] +_ = station.daily_average +display(f"compute_count after cache invalidation = {station.compute_count}") diff --git a/examples/propcache/cached_property_basics/config.toml b/examples/propcache/cached_property_basics/config.toml new file mode 100644 index 0000000..b070b3d --- /dev/null +++ b/examples/propcache/cached_property_basics/config.toml @@ -0,0 +1 @@ +packages = ["propcache"] diff --git a/examples/propcache/cached_property_basics/setup.py b/examples/propcache/cached_property_basics/setup.py new file mode 100644 index 0000000..fce9f9c --- /dev/null +++ b/examples/propcache/cached_property_basics/setup.py @@ -0,0 +1,46 @@ +""" +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): + """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) + + +# propcache provides a fast drop-in replacement for +# functools.cached_property. Both decorators live in propcache.api. +from propcache.api import cached_property diff --git a/examples/propcache/order.json b/examples/propcache/order.json new file mode 100644 index 0000000..3e97241 --- /dev/null +++ b/examples/propcache/order.json @@ -0,0 +1,4 @@ +[ + "cached_property_basics", + "under_cached_property" +] diff --git a/examples/propcache/under_cached_property/code.py b/examples/propcache/under_cached_property/code.py new file mode 100644 index 0000000..d7e52a8 --- /dev/null +++ b/examples/propcache/under_cached_property/code.py @@ -0,0 +1,68 @@ +# --------------------------------------------------------------------- +# under_cached_property: store cached values in `self._cache` +# --------------------------------------------------------------------- + +heading("When you want a separate cache dict: under_cached_property") +note( + "under_cached_property behaves like " + "cached_property, but it stores results in " + "self._cache instead of self.__dict__. " + "It also disallows __set__, so the property is " + "read-only. This is handy on classes that use " + "__slots__ or that want a single, easily " + "inspectable place to clear cached state." +) + + +class Order: + """A small order whose totals are cached in `self._cache`.""" + + __slots__ = ("items", "tax_rate", "_cache") + + def __init__(self, items, tax_rate): + self.items = items # list of (name, unit_price, quantity) + self.tax_rate = tax_rate + # under_cached_property requires this attribute to exist. + self._cache = {} + + @under_cached_property + def subtotal(self): + return sum(price * qty for _, price, qty in self.items) + + @under_cached_property + def total(self): + return round(self.subtotal * (1 + self.tax_rate), 2) + + +order = Order( + items=[ + ("Notebook", 4.50, 3), + ("Pen", 1.20, 10), + ("Stapler", 8.99, 1), + ], + tax_rate=0.08, +) + +note("Asking for the total computes and caches both properties:") +display(f"total = {order.total}") +display(f"_cache contents = {order._cache}") + +note( + "Because the cache is just a dict on the instance, you can clear " + "specific entries to invalidate them, e.g. after changing the " + "tax rate:" +) +order.tax_rate = 0.10 +order._cache.pop("total", None) +display(f"new total = {order.total}") +display(f"_cache contents = {order._cache}") + +note( + "Trying to assign to an under_cached_property raises " + "AttributeError, which protects the cached value " + "from accidental overwrites:" +) +try: + order.subtotal = 999.99 +except AttributeError as exc: + display(f"AttributeError: {exc}") diff --git a/examples/propcache/under_cached_property/config.toml b/examples/propcache/under_cached_property/config.toml new file mode 100644 index 0000000..b070b3d --- /dev/null +++ b/examples/propcache/under_cached_property/config.toml @@ -0,0 +1 @@ +packages = ["propcache"] diff --git a/examples/propcache/under_cached_property/setup.py b/examples/propcache/under_cached_property/setup.py new file mode 100644 index 0000000..53df3f9 --- /dev/null +++ b/examples/propcache/under_cached_property/setup.py @@ -0,0 +1,22 @@ +"""Lightweight setup for the second example -- no IPython shim needed.""" +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 propcache.api import cached_property, under_cached_property From 97ea154297cd8ad474f222b3a84177a4eb05175c Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 11 Jun 2026 12:25:55 +0100 Subject: [PATCH 2/2] Fix imports. --- examples/propcache/cached_property_basics/code.py | 4 ++++ examples/propcache/cached_property_basics/setup.py | 4 ---- examples/propcache/under_cached_property/code.py | 3 +++ examples/propcache/under_cached_property/setup.py | 2 -- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/propcache/cached_property_basics/code.py b/examples/propcache/cached_property_basics/code.py index e288666..ef2f075 100644 --- a/examples/propcache/cached_property_basics/code.py +++ b/examples/propcache/cached_property_basics/code.py @@ -10,6 +10,10 @@ See https://propcache.readthedocs.io for full documentation. """ from IPython.core.display import display, HTML +# propcache provides a fast drop-in replacement for +# functools.cached_property. Both decorators live in propcache.api. +from propcache.api import cached_property + heading("A weather station with an expensive computation") note( diff --git a/examples/propcache/cached_property_basics/setup.py b/examples/propcache/cached_property_basics/setup.py index fce9f9c..07879f9 100644 --- a/examples/propcache/cached_property_basics/setup.py +++ b/examples/propcache/cached_property_basics/setup.py @@ -40,7 +40,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -# propcache provides a fast drop-in replacement for -# functools.cached_property. Both decorators live in propcache.api. -from propcache.api import cached_property diff --git a/examples/propcache/under_cached_property/code.py b/examples/propcache/under_cached_property/code.py index d7e52a8..52e1cd1 100644 --- a/examples/propcache/under_cached_property/code.py +++ b/examples/propcache/under_cached_property/code.py @@ -2,6 +2,9 @@ # under_cached_property: store cached values in `self._cache` # --------------------------------------------------------------------- +from propcache.api import cached_property, under_cached_property + + heading("When you want a separate cache dict: under_cached_property") note( "under_cached_property behaves like " diff --git a/examples/propcache/under_cached_property/setup.py b/examples/propcache/under_cached_property/setup.py index 53df3f9..6476ec9 100644 --- a/examples/propcache/under_cached_property/setup.py +++ b/examples/propcache/under_cached_property/setup.py @@ -18,5 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -from propcache.api import cached_property, under_cached_property