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/munch/README.md
Original file line number Diff line number Diff line change
@@ -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.
66 changes: 66 additions & 0 deletions examples/munch/default_munch/code.py
Original file line number Diff line number Diff line change
@@ -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 &rarr; <strong>{config.host}</strong>")
note(f"config.database.user &rarr; <strong>{config.database.user}</strong>")
note(f"config.database.password is MISSING &rarr; {config.database.password is MISSING}")
note(f"config.tls_cert is MISSING &rarr; {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 &rarr; <strong>{word_counts.the}</strong>")
note(f"word_counts.gull &rarr; <strong>{word_counts.gull}</strong>")
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)
1 change: 1 addition & 0 deletions examples/munch/default_munch/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["munch"]
21 changes: 21 additions & 0 deletions examples/munch/default_munch/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


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

70 changes: 70 additions & 0 deletions examples/munch/dot_access_basics/code.py
Original file line number Diff line number Diff line change
@@ -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 &rarr; <strong>{customer.name}</strong>")
note(f'customer["favorite_drink"] &rarr; <strong>{customer["favorite_drink"]}</strong>')
note(f"customer.loyalty_points &rarr; <strong>{customer.loyalty_points}</strong>")

# Munch is a real dict subclass, so all the usual methods work.
note(f"Keys: {list(customer.keys())}")
note(f"isinstance(customer, dict) &rarr; {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 &rarr; <strong>{customer.address.city}</strong>")
note(f"customer.address.postcode &rarr; <strong>{customer.address.postcode}</strong>")

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 &rarr; <strong>{order.order_id}</strong>")
note(f"order.line_items[0].name &rarr; <strong>{order.line_items[0].name}</strong>")
note(f"order.totals.subtotal &rarr; <strong>{order.totals.subtotal}</strong>")

# Going back is just as easy.
plain = unmunchify(order)
note(f"unmunchify gives back a plain dict: {type(plain).__name__}")
display(plain, append=True)
1 change: 1 addition & 0 deletions examples/munch/dot_access_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["munch"]
41 changes: 41 additions & 0 deletions examples/munch/dot_access_basics/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
60 changes: 60 additions & 0 deletions examples/munch/json_round_trip/code.py
Original file line number Diff line number Diff line change
@@ -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 &rarr; <strong>{weather.location.city}</strong>")
note(
f"weather.current.temperature_c &rarr; "
f"<strong>{weather.current.temperature_c} &deg;C</strong>"
)

# Iterate over a list of nested Munches just like a list of dicts.
forecast_lines = [
f"{day.day}: {day.low_c}&deg; to {day.high_c}&deg;"
for day in weather.forecast
]
note("Three-day forecast: " + " &middot; ".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"<pre>{dumped[:200]}...</pre>"), 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)}")
1 change: 1 addition & 0 deletions examples/munch/json_round_trip/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["munch"]
21 changes: 21 additions & 0 deletions examples/munch/json_round_trip/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


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

5 changes: 5 additions & 0 deletions examples/munch/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"dot_access_basics",
"json_round_trip",
"default_munch"
]