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/propcache/README.md
Original file line number Diff line number Diff line change
@@ -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.
61 changes: 61 additions & 0 deletions examples/propcache/cached_property_basics/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>__dict__</code>, "
"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}")
1 change: 1 addition & 0 deletions examples/propcache/cached_property_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["propcache"]
42 changes: 42 additions & 0 deletions examples/propcache/cached_property_basics/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


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

4 changes: 4 additions & 0 deletions examples/propcache/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"cached_property_basics",
"under_cached_property"
]
71 changes: 71 additions & 0 deletions examples/propcache/under_cached_property/code.py
Original file line number Diff line number Diff line change
@@ -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(
"<code>under_cached_property</code> behaves like "
"<code>cached_property</code>, but it stores results in "
"<code>self._cache</code> instead of <code>self.__dict__</code>. "
"It also disallows <code>__set__</code>, so the property is "
"read-only. This is handy on classes that use "
"<code>__slots__</code> 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 <code>under_cached_property</code> raises "
"<code>AttributeError</code>, which protects the cached value "
"from accidental overwrites:"
)
try:
order.subtotal = 999.99
except AttributeError as exc:
display(f"AttributeError: {exc}")
1 change: 1 addition & 0 deletions examples/propcache/under_cached_property/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["propcache"]
20 changes: 20 additions & 0 deletions examples/propcache/under_cached_property/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


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