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)
| File | Size | Uploaded | |
|---|---|---|---|
| mirino-0.1.0.tar.gz | 5.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|