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) + 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) 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) + 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" +]