Skip to main content

Mirino

Mirino records attribute and item accesses instead of evaluating them. Reading person.address.city builds an expression that remembers the path, and that path can be read, written, printed or checked later, against any object.

Mirino is used by the Bolinette project to describe mapping profiles, to name the field an error came from, and to record the calls a mock expects.

from dataclasses import dataclass
from mirino import ExpressionTree


@dataclass
class Address:
    city: str


@dataclass
class Person:
    address: Address


expr = ExpressionTree.new().address.city
person = Person(Address("Lyon"))

assert str(expr) == "$.address.city"
assert ExpressionTree.get_value(expr, person) == "Lyon"

ExpressionTree.set_value(expr, person, "Paris")
assert person.address.city == "Paris"

Installation

$ pip install mirino  # or use your preferred package manager

Requirements

Mirino requires Python 3.13 (or newer) and no other dependencies.

Concepts

ExpressionTree.new() returns a RootNode, the start of every expression. A root formats as $, or as the origin it was given, which is usually the type or the object the path will be read on.

Every attribute access on a node returns an AttributeNode and every subscript returns an ElementNode, and neither of them reads anything. Both keep a reference to the node they came from, so an expression is a chain from its last access back to its root.

from mirino import AttributeNode, ElementNode, ExpressionTree

assert str(ExpressionTree.new()) == "$"
assert str(ExpressionTree.new("Person")) == "Person"

expr = ExpressionTree.new().address["city"]
assert isinstance(expr, ElementNode)
assert isinstance(ExpressionTree.new().address, AttributeNode)
assert str(expr) == "$.address['city']"

A node intercepts every attribute access, so an expression has no methods of its own: expr.get_value would simply record one more step. Everything is done from the outside through ExpressionTree, a namespace of static functions that cannot be instantiated.

Reading and writing

get_value walks the recorded path on a real object and returns what it finds, set_value assigns the last step, and get_attribute returns the name or the key of that last step. The root evaluates to the object itself, and cannot be assigned or named.

from mirino import ExpressionTree

person = {"name": "Bob", "tags": ["a", "b"]}
root = ExpressionTree.new()

assert ExpressionTree.get_value(root, person) is person
assert ExpressionTree.get_value(root["name"], person) == "Bob"
assert ExpressionTree.get_value(root["tags"][1], person) == "b"
assert ExpressionTree.get_attribute(root["tags"]) == "tags"

ExpressionTree.set_value(root["name"], person, "Alice")
assert person["name"] == "Alice"

Attribute access goes through getattr and setattr, item access through [], so an expression works on anything that supports them. Assigning to a root raises an ExpressionError, and so does asking a root for its attribute name.

Formatting

format renders the path, and is what str() and repr() use, which is what makes an expression readable in an error message. max_depth keeps only the last steps, so a deep path can be shown relative to something else than its root.

from mirino import ExpressionTree

expr = ExpressionTree.new().a.b.c

assert ExpressionTree.format(expr) == "$.a.b.c"
assert ExpressionTree.format(expr, max_depth=2) == "b.c"
assert ExpressionTree.format(expr, max_depth=1) == "c"
assert repr(expr) == "<AttributeNode: $.a.b.c>"

Item accesses count as one step too, and a string key is quoted while any other key is printed as it is.

Validating an expression

An expression built by a caller, typically from a lambda, is not necessarily a plain chain of accesses. ensure_attribute_chain walks it from the last step down to the root and raises an AttributeChainError when it finds a node that is not an attribute or an item access. With max_depth, the chain must also be exactly that many steps long, or a MaxDepthExpressionError is raised.

from mirino import ExpressionTree

ExpressionTree.ensure_attribute_chain(ExpressionTree.new().a["b"].c)
ExpressionTree.ensure_attribute_chain(ExpressionTree.new().a, max_depth=1)  # exactly one step

ExpressionTree.new().a.b with max_depth=1 is too deep, and ExpressionTree.new().a with max_depth=2 reaches its root too early. Both raise, and the message holds the whole expression, not the step that failed.

Reference

Nodes

Node Built by
RootNode ExpressionTree.new(origin)
AttributeNode expr.attr
ElementNode expr[key]
ChildNode Base of the last two

Errors

All errors derive from mirino.errors.MirinoError, and carry the expression they were raised for.

Error Raised when
ExpressionError A node does not support the operation, such as assigning to a root
MaxDepthExpressionError An expression is deeper than the depth ensure_attribute_chain allows
AttributeChainError An expression is not a plain chain of attribute and item accesses

License

Mirino is released under the MIT license, see LICENSE.txt.

Metadata

Release files for mirino 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mirino 0.1.0
File Size Uploaded
mirino-0.1.0.tar.gz 5.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mirino 0.1.0
File Interpreter ABI Platform
mirino-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 12.1 kB

Release files / mirino-0.1.0.tar.gz

Download URL mirino-0.1.0.tar.gz
Size 5.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a2f19c04dd0b5ec3206db675a0947160c704407b182b414e6c4231826dd866a7
BLAKE2b-256 checksum
How to use checksums
095ea76fdc7ae638cfdcb1d959510328f8aa20b8c8540c4ec6a698d34a0f369f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / mirino-0.1.0-py3-none-any.whl

Download URL mirino-0.1.0-py3-none-any.whl
Size 6.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d102e0eab45387687d80c611369455072dda490f13e9e3596001a29aea1c4005
BLAKE2b-256 checksum
How to use checksums
ca42fde2b6395a7baf7950336a980b6849fa27902642c1d9a2edbbd60cd6511c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page