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..ef2f075
--- /dev/null
+++ b/examples/propcache/cached_property_basics/code.py
@@ -0,0 +1,61 @@
+"""
+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
+# 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(
+ "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..07879f9
--- /dev/null
+++ b/examples/propcache/cached_property_basics/setup.py
@@ -0,0 +1,42 @@
+"""
+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) + 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..52e1cd1 --- /dev/null +++ b/examples/propcache/under_cached_property/code.py @@ -0,0 +1,71 @@ +# --------------------------------------------------------------------- +# 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 "
+ "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..6476ec9
--- /dev/null
+++ b/examples/propcache/under_cached_property/setup.py
@@ -0,0 +1,20 @@
+"""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) +