diff --git a/examples/lazy-object-proxy/README.md b/examples/lazy-object-proxy/README.md new file mode 100644 index 0000000..57771a5 --- /dev/null +++ b/examples/lazy-object-proxy/README.md @@ -0,0 +1,18 @@ +# lazy-object-proxy 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/lazy-object-proxy/deferred_construction/code.py b/examples/lazy-object-proxy/deferred_construction/code.py new file mode 100644 index 0000000..c321695 --- /dev/null +++ b/examples/lazy-object-proxy/deferred_construction/code.py @@ -0,0 +1,68 @@ +""" +Deferred construction with `lazy_object_proxy.Proxy`. + +A `Proxy` wraps a zero-argument callable (the "factory") and looks +and feels like the eventual return value, but the factory is only +invoked the first time the proxy is actually used. After that the +result is cached and the proxy transparently forwards every +attribute access, method call, and operator to the real object. + +Docs: https://python-lazy-object-proxy.readthedocs.io/ +""" +from IPython.core.display import display, HTML + +import lazy_object_proxy + + +heading("A proxy that pretends to be an expensive report") +note( + "Imagine loading a large report from disk or the network. We " + "don't want to pay that cost unless someone actually reads it." +) + +# A counter we can inspect to see when (and how often) the factory runs. +build_calls = {"count": 0} + + +def build_quarterly_report(): + """Pretend to do expensive work and return a dict 'report'.""" + build_calls["count"] += 1 + return { + "quarter": "Q3", + "revenue": 184_500.00, + "top_product": "Aurora Notebook", + "units_sold": 1273, + } + + +# Wrap the factory. No work happens here. +report = lazy_object_proxy.Proxy(build_quarterly_report) + +note( + f"Proxy created. Factory call count so far: " + f"{build_calls['count']}" +) + +# `__resolved__` tells us whether the factory has been called yet. +note(f"Has the proxy been resolved? {report.__resolved__}") + +# First real use: indexing triggers the factory exactly once. +heading("First touch triggers the factory", level=3) +note(f"Top product: {report['top_product']}") +note( + f"Factory call count after first use: " + f"{build_calls['count']}" +) +note(f"Resolved now? {report.__resolved__}") + +# Subsequent uses reuse the cached object; the factory is NOT called again. +heading("Subsequent uses reuse the cached value", level=3) +note(f"Revenue: ${report['revenue']:,.2f}") +note(f"Units sold: {report['units_sold']}") +note( + f"Factory call count after several uses: " + f"{build_calls['count']} (still 1)" +) + +# The proxy is indistinguishable from the underlying dict for most purposes. +note(f"isinstance(report, dict)? {isinstance(report, dict)}") diff --git a/examples/lazy-object-proxy/deferred_construction/config.toml b/examples/lazy-object-proxy/deferred_construction/config.toml new file mode 100644 index 0000000..89c494b --- /dev/null +++ b/examples/lazy-object-proxy/deferred_construction/config.toml @@ -0,0 +1 @@ +packages = ["lazy-object-proxy"] diff --git a/examples/lazy-object-proxy/deferred_construction/setup.py b/examples/lazy-object-proxy/deferred_construction/setup.py new file mode 100644 index 0000000..050df73 --- /dev/null +++ b/examples/lazy-object-proxy/deferred_construction/setup.py @@ -0,0 +1,36 @@ +"""Shim setup for the first example: full IPython compatibility shim.""" +import sys +import types +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__, + ) + + +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/lazy-object-proxy/lazy_imports_and_configs/code.py b/examples/lazy-object-proxy/lazy_imports_and_configs/code.py new file mode 100644 index 0000000..fb5164f --- /dev/null +++ b/examples/lazy-object-proxy/lazy_imports_and_configs/code.py @@ -0,0 +1,79 @@ +# --------------------------------------------------------------------- +# Two practical patterns: lazy imports and a lazy settings registry. +# --------------------------------------------------------------------- + +import lazy_object_proxy + + +heading("Pattern 1: lazy imports") +note( + "Wrap a module import in a Proxy so the cost of importing is " + "paid only when the module is first touched. Useful for heavy " + "optional dependencies." +) + + +def _import_json(): + """Factory that imports and returns the json module.""" + import json + return json + + +# `json_lazy` looks and behaves like the json module, but isn't loaded yet. +json_lazy = lazy_object_proxy.Proxy(_import_json) + +note(f"Resolved before use? {json_lazy.__resolved__}") + +payload = {"city": "Lisbon", "temp_c": 21, "skies": "clear"} +encoded = json_lazy.dumps(payload) # triggers the import +note(f"Encoded payload: {encoded}") +note(f"Resolved after use? {json_lazy.__resolved__}") + + +heading("Pattern 2: a registry of lazily-built settings") +note( + "Each entry is a Proxy wrapping a builder. Iterating the " + "registry's keys is cheap; only the entries you read get built." +) + +build_log = [] + + +def make_builder(name, value): + """Return a zero-arg factory that 'builds' a settings record.""" + def _build(): + build_log.append(name) + return {"name": name, "value": value, "ready": True} + return _build + + +settings = { + "database": lazy_object_proxy.Proxy( + make_builder("database", "postgres://example/db") + ), + "cache": lazy_object_proxy.Proxy( + make_builder("cache", "redis://example/0") + ), + "search": lazy_object_proxy.Proxy( + make_builder("search", "https://search.example/api") + ), +} + +note(f"Registry keys (no builders run yet): {list(settings)}") +note(f"Builders run so far: {build_log}") + +# Touch only the 'cache' entry. The other two stay un-built. +cache_value = settings["cache"]["value"] +note(f"cache value: {cache_value}") +note(f"Builders run after reading 'cache': {build_log}") + +# Reading 'cache' again does not rebuild; the proxy caches its result. +_ = settings["cache"]["ready"] +note(f"Builders run after re-reading 'cache': {build_log}") + +# Now touch 'database'; 'search' remains untouched. +note(f"database url: {settings['database']['value']}") +note(f"Final build log: {build_log}") + +resolved = {k: v.__resolved__ for k, v in settings.items()} +note(f"Per-entry resolved status: {resolved}") diff --git a/examples/lazy-object-proxy/lazy_imports_and_configs/config.toml b/examples/lazy-object-proxy/lazy_imports_and_configs/config.toml new file mode 100644 index 0000000..89c494b --- /dev/null +++ b/examples/lazy-object-proxy/lazy_imports_and_configs/config.toml @@ -0,0 +1 @@ +packages = ["lazy-object-proxy"] diff --git a/examples/lazy-object-proxy/lazy_imports_and_configs/setup.py b/examples/lazy-object-proxy/lazy_imports_and_configs/setup.py new file mode 100644 index 0000000..b38ee6c --- /dev/null +++ b/examples/lazy-object-proxy/lazy_imports_and_configs/setup.py @@ -0,0 +1,19 @@ +"""Setup for example 2. Mirrors example 1's namespace, no IPython shim.""" +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) diff --git a/examples/lazy-object-proxy/order.json b/examples/lazy-object-proxy/order.json new file mode 100644 index 0000000..214adcf --- /dev/null +++ b/examples/lazy-object-proxy/order.json @@ -0,0 +1,4 @@ +[ + "deferred_construction", + "lazy_imports_and_configs" +]