PEP 636 - “Structural pattern matching” - is about the match expression that was added to Python 3.10, and it is much more powerful than the familiar C-style switch-case. (Yet not as powerful as Rust’s match, but that’s a different story.)
If you’re using modern ops, chances are your charm is on jammy (22.04) or later, which means you have Python 3.10+. Yay!
A few words about framework engineering before we begin
Currently, the operator framework enables “routing” events using framework.observe() calls from __init__. The charm’s __init__ is the “event registration grounds”, and the framework “emits” the events thereafter. This enables many-to-many mapping between events and event handlers. The framework also guarantees that the order of observe() statements prescribes the order of the event handlers called, so code ordering is straightforward. If charming hadn’t taken the trajectory of observing events from within charm libraries, then code execution order would not have been such a pain, but that’s a different story.
Another typical “registration” approach is by sprinkling decorators. Charming with an alternative observer framework could have looked like this:
@framework.observe("relation-changed", "ingress")
@framework.observe("relation-changed", "certificates")
def on_lots_has_changed(event):
...
This also enables many-to-many mapping from events to event handlers, but in this case code ordering is a bit more confusing, and brittle, if you reorder your methods. So I think the observe() methods we currently have were a better choice than decorators, for the purpose of charming.
So how does Python’s match-case block compare to a block of observe() statements? If used in the trivial C-style switch-case, they would be functionally equivalent. If used as intended, then it would no longer be many-to-many (which could be a good thing!). Some wild charmers may even think to combine the two together, but that’s a different story.
This story is about observe()-free event routing using match-case expressions.
JujuContext
Among other things, operator/1313 helped clearing up the line between juju and the operator framework. Now we can from jujucontext import JujuContext and ctx = JujuContext.from_environ() to get access to all the raw data from which the operator framework scaffolds everything.
PEP 636: Capturing sub-patterns and wildcards
Imagine JujuContext had a List[str] representation for events:
event: List[str] = JujuContext.from_environ().event_as_list()
Then “reconciler with exceptions” could look like this:
match event:
# VM charm
case ["install"]:
snap.install("...")
case ["remove"]:
snap.remove("...")
# K8s charm
case ["pebble-ready", ("grafana" | "litestream") as c]:
Container(c).push(certs)
case _:
reconcile(event)
PEP 636: Matching objects
Imagine JujuContext had a dataclass representation for events:
@dataclass
class RelationEvent:
kind: SomeStrEnum # with CHANGED = "changed", etc.
relation: str
# etc.
event = JujuContext.from_environ().event_as_dataclass()
Then we’d need less string literals:
match event:
case RelationEvent(RelKind.CREATED, "certificates"):
# stop pebble plan until certs are in
case RelationEvent(RelKind.CHANGED, relation):
# do something with all _other_ `relation-changed` events
PEP 636: Mappings
Imagine JujuContext had a json representation for events:
event = JujuContext.from_environ().event_as_json()
Then we could have explicit granularity on action routing:
match event:
case {"name": "export-action", "compression": c, "filename": fn}
# compress using algo `c` to filename `fn`
case {"name": "clone-action", "from": src, "to": dst} if src == dst
# fail the event because src == dst
case {"name": "secret-changed", "key1": val1, "key2": val2}
# Whatever secret it was, if it has those matching
# key-val pairs then let's do something
Additional context
- Charms are woken up by a single event at a time, i.e. charms are “one-shot” commands, not a long running process, so the observer pattern has a very narrow job in charms.
- When a match is found, the other matches are not executed, and this is a key subtlety an difference from the trivial many-to-many of
observe()calls. match-caseexpressions can be used multiple times along the routing path of an event.