diff --git a/examples/munch/README.md b/examples/munch/README.md new file mode 100644 index 0000000..96f228e --- /dev/null +++ b/examples/munch/README.md @@ -0,0 +1,18 @@ +# munch 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/munch/default_munch/code.py b/examples/munch/default_munch/code.py new file mode 100644 index 0000000..a02744a --- /dev/null +++ b/examples/munch/default_munch/code.py @@ -0,0 +1,66 @@ +# --------------------------------------------------------------------- +# DefaultMunch and DefaultFactoryMunch: graceful handling of missing keys. +# --------------------------------------------------------------------- +from munch import Munch, DefaultMunch, DefaultFactoryMunch + + +heading("DefaultMunch: a sentinel for missing attributes") +note( + "A regular Munch raises AttributeError for unknown keys. " + "DefaultMunch instead returns a value of your choosing, which is " + "perfect for optional configuration fields." +) + +# A common pattern: use a sentinel object so you can tell "missing" apart +# from a legitimate None value. +MISSING = object() + +config = DefaultMunch.fromDict( + { + "host": "localhost", + "port": 5432, + "database": {"name": "shop", "user": "ada"}, + }, + MISSING, +) + +note(f"config.host → {config.host}") +note(f"config.database.user → {config.database.user}") +note(f"config.database.password is MISSING → {config.database.password is MISSING}") +note(f"config.tls_cert is MISSING → {config.tls_cert is MISSING}") + +heading("DefaultFactoryMunch: build values on demand") +note( + "When you'd rather create a fresh value for each missing key (think " + "collections.defaultdict), reach for DefaultFactoryMunch. Below we " + "tally word frequencies in a tiny corpus." +) + +word_counts = DefaultFactoryMunch(int) +poem = ( + "the sea the sky the gull " + "the wind the wave the gull" +) +for word in poem.split(): + word_counts[word] += 1 + +# Attribute-style access works on accumulated keys. +note(f"word_counts.the → {word_counts.the}") +note(f"word_counts.gull → {word_counts.gull}") +note(f"Unseen word starts at zero: word_counts.dolphin = {word_counts.dolphin}") + +display(dict(word_counts), append=True) + +# A factory can be any zero-argument callable. Here, lists for grouping. +inbox = DefaultFactoryMunch(list) +messages = [ + ("ada", "lunch?"), + ("grace", "shipped the build"), + ("ada", "see you at 1"), + ("linus", "patch attached"), +] +for sender, body in messages: + inbox[sender].append(body) + +note("Messages grouped by sender:") +display(dict(inbox), append=True) diff --git a/examples/munch/default_munch/config.toml b/examples/munch/default_munch/config.toml new file mode 100644 index 0000000..d3c7ac1 --- /dev/null +++ b/examples/munch/default_munch/config.toml @@ -0,0 +1 @@ +packages = ["munch"] diff --git a/examples/munch/default_munch/setup.py b/examples/munch/default_munch/setup.py new file mode 100644 index 0000000..83a6e26 --- /dev/null +++ b/examples/munch/default_munch/setup.py @@ -0,0 +1,21 @@ +"""Setup for the DefaultMunch example.""" + +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/munch/dot_access_basics/code.py b/examples/munch/dot_access_basics/code.py new file mode 100644 index 0000000..2bbdf46 --- /dev/null +++ b/examples/munch/dot_access_basics/code.py @@ -0,0 +1,70 @@ +""" +Getting started with Munch: a dict that you can also poke at with dots. + +A Munch behaves exactly like a regular dictionary, but its keys can be +accessed (and assigned) as attributes. Handy for config objects, parsed +JSON, or anything where `config["database"]["host"]` starts to feel +clunky compared to `config.database.host`. + +Docs and source: https://github.com/Infinidat/munch +""" + +from IPython.core.display import display, HTML + +# Package imports for this example. +from munch import Munch, munchify, unmunchify + + +heading("1. A Munch is just a dict with attribute access") +note( + "Build a small profile for an imaginary coffee-shop customer. " + "Notice how we can mix bracket-style and dot-style access freely." +) + +customer = Munch() +customer.name = "Ada Lovelace" +customer.favorite_drink = "flat white" +customer["loyalty_points"] = 142 + +note(f"customer.name → {customer.name}") +note(f'customer["favorite_drink"] → {customer["favorite_drink"]}') +note(f"customer.loyalty_points → {customer.loyalty_points}") + +# Munch is a real dict subclass, so all the usual methods work. +note(f"Keys: {list(customer.keys())}") +note(f"isinstance(customer, dict) → {isinstance(customer, dict)}") + +heading("2. Nesting works naturally") +note( + "Assign a Munch as a value and you can chain dot access all the " + "way down." +) + +customer.address = Munch(city="London", postcode="EC1A 1BB") +note(f"customer.address.city → {customer.address.city}") +note(f"customer.address.postcode → {customer.address.postcode}") + +heading("3. Convert to/from plain dicts with munchify / unmunchify") +note( + "Got a nested dict from somewhere (parsed JSON, a config file)? " + "Pass it through munchify and the whole tree becomes dot-accessible." +) + +raw_order = { + "order_id": "A-1042", + "line_items": [ # renamed from "items" + {"name": "croissant", "qty": 2}, + {"name": "flat white", "qty": 1}, + ], + "totals": {"subtotal": 8.50, "tax": 0.68}, +} + +order = munchify(raw_order) +note(f"order.order_id → {order.order_id}") +note(f"order.line_items[0].name → {order.line_items[0].name}") +note(f"order.totals.subtotal → {order.totals.subtotal}") + +# Going back is just as easy. +plain = unmunchify(order) +note(f"unmunchify gives back a plain dict: {type(plain).__name__}") +display(plain, append=True) diff --git a/examples/munch/dot_access_basics/config.toml b/examples/munch/dot_access_basics/config.toml new file mode 100644 index 0000000..d3c7ac1 --- /dev/null +++ b/examples/munch/dot_access_basics/config.toml @@ -0,0 +1 @@ +packages = ["munch"] diff --git a/examples/munch/dot_access_basics/setup.py b/examples/munch/dot_access_basics/setup.py new file mode 100644 index 0000000..b4f3ee1 --- /dev/null +++ b/examples/munch/dot_access_basics/setup.py @@ -0,0 +1,41 @@ +""" +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) diff --git a/examples/munch/json_round_trip/code.py b/examples/munch/json_round_trip/code.py new file mode 100644 index 0000000..08d8fd1 --- /dev/null +++ b/examples/munch/json_round_trip/code.py @@ -0,0 +1,60 @@ +# --------------------------------------------------------------------- +# Munch + JSON: a comfortable way to work with API-shaped data. +# --------------------------------------------------------------------- +from munch import Munch, munchify, unmunchify +import json + + +heading("Parsing JSON into a Munch") +note( + "Imagine this JSON came back from a weather API. We parse it as " + "usual, then munchify so we can navigate the response with dots " + "instead of a thicket of brackets and quotes." +) + +api_response_text = """ +{ + "location": {"city": "Reykjavik", "country": "IS"}, + "current": {"temperature_c": 4.2, "wind_kph": 22.0, "condition": "cloudy"}, + "forecast": [ + {"day": "Mon", "high_c": 5, "low_c": -1}, + {"day": "Tue", "high_c": 6, "low_c": 0}, + {"day": "Wed", "high_c": 3, "low_c": -2} + ] +} +""" + +raw = json.loads(api_response_text) +weather = munchify(raw) + +note(f"weather.location.city → {weather.location.city}") +note( + f"weather.current.temperature_c → " + f"{weather.current.temperature_c} °C" +) + +# Iterate over a list of nested Munches just like a list of dicts. +forecast_lines = [ + f"{day.day}: {day.low_c}° to {day.high_c}°" + for day in weather.forecast +] +note("Three-day forecast: " + " · ".join(forecast_lines)) + +heading("Serializing back out with toJSON()") +note( + "Every Munch has a toJSON() helper. It produces a JSON string just " + "like json.dumps would, so a Munch can travel through any JSON-aware " + "boundary unchanged." +) + +# Mutate via dot access, then dump. +weather.current.condition = "snow" +weather.forecast.append(munchify({"day": "Thu", "high_c": 2, "low_c": -3})) + +dumped = weather.toJSON() +note("Round-tripped JSON (truncated):") +display(HTML(f"
{dumped[:200]}...
"), append=True) + +# And of course, it's still a dict, so json.dumps works directly too. +also_dumped = json.dumps(weather, indent=2) +note(f"json.dumps and toJSON agree: {json.loads(dumped) == json.loads(also_dumped)}") diff --git a/examples/munch/json_round_trip/config.toml b/examples/munch/json_round_trip/config.toml new file mode 100644 index 0000000..d3c7ac1 --- /dev/null +++ b/examples/munch/json_round_trip/config.toml @@ -0,0 +1 @@ +packages = ["munch"] diff --git a/examples/munch/json_round_trip/setup.py b/examples/munch/json_round_trip/setup.py new file mode 100644 index 0000000..138fc83 --- /dev/null +++ b/examples/munch/json_round_trip/setup.py @@ -0,0 +1,21 @@ +"""Setup for the JSON round-trip example.""" + +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/munch/order.json b/examples/munch/order.json new file mode 100644 index 0000000..ee5f39f --- /dev/null +++ b/examples/munch/order.json @@ -0,0 +1,5 @@ +[ + "dot_access_basics", + "json_round_trip", + "default_munch" +]