Unit contexts#
A unit context answers the question “what units does this bare number carry?” at the moment the number is interpreted, rather than at the moment the code reading it was written.
All examples on this page use the default unit registry, available as ureg:
>>> import pintext
>>> ureg = pintext.get_unit_registry()
Unit generators#
The building block is UnitGenerator: a small callable that stores
units and returns them when called.
>>> generator = pintext.UnitGenerator(ureg.m)
>>> generator()
<Unit('meter')>
Stored units can be modified:
>>> generator.units = ureg.s
>>> generator()
<Unit('second')>
Anything that accepts units in Pintext also accepts a generator. Because the generator is evaluated at each use, replacing its units changes the outcome of every site that holds a reference to it — this is the indirection the whole package is built on.
>>> generator.units = ureg.m
>>> converter = pintext.ensure_units(default_units=generator)
>>> converter(1.0)
<Quantity(1.0, 'meter')>
>>> generator.units = ureg.km
>>> converter(1.0)
<Quantity(1.0, 'kilometer')>
Temporary override#
Assigning to units is permanent. The UnitGenerator.override()
context manager applies a temporary change and restores the previous value on
exit:
>>> generator = pintext.UnitGenerator(ureg.m)
>>> with generator.override(ureg.km):
... generator()
<Unit('kilometer')>
>>> generator()
<Unit('meter')>
Override values may be given as strings, interpreted against the registry of the currently stored units:
>>> with generator.override("mile"):
... generator()
<Unit('mile')>
Note
override() mutates the generator in-place. It is
convenient but not thread-safe: two threads overriding the same generator
will interfere.
Composed unit generators#
The UnitGenerator constructor accepts any callable, which makes it
possible to derive units from other generators:
>>> length = pintext.UnitGenerator(ureg.m)
>>> time = pintext.UnitGenerator(ureg.s)
>>> speed = pintext.UnitGenerator(lambda: length() / time())
>>> speed()
<Unit('meter / second')>
Overriding a component propagates to the composed generator:
>>> with length.override(ureg.km), time.override(ureg.hour):
... speed()
<Unit('kilometer / hour')>
Collecting generators in a context#
UnitContext manages a structured collection of unit generators. The
simplest definition uses a string-keyed dictionary:
>>> uctx = pintext.UnitContext({"length": pintext.UnitGenerator(ureg.m)})
Units passed directly are turned into generators automatically:
>>> uctx = pintext.UnitContext({"length": ureg.m})
>>> uctx.deferred("length")
UnitGenerator(units=<Unit('meter')>)
Additional entries are added with register():
>>> uctx.register("time", ureg.s)
>>> uctx.get_all()
{'length': <Unit('meter')>, 'time': <Unit('second')>}
get() evaluates a single entry:
>>> uctx.get("length")
<Unit('meter')>
Note
get() and register() are aliased
with square brackets:
>>> uctx["time"] = ureg.ms
>>> uctx["time"]
<Unit('millisecond')>
>>> uctx["time"] = pintext.UnitGenerator(ureg.s)
>>> uctx["time"]
<Unit('second')>
get() returns evaluated units (a snapshot). To keep the
indirection, ask for the generator itself with deferred():
>>> uctx.deferred("length")
UnitGenerator(units=<Unit('meter')>)
This is the object to hand to ensure_units(), to
pintext.attrs.field() or to pintext.pydantic.quantity().
Temporary override#
UnitContext.override() overrides several registered generators at once,
using a dictionary:
>>> with uctx.override({"length": ureg.mile, "time": ureg.hour}):
... ureg.Quantity(1.0, "km/hour").to(uctx.get("length") / uctx.get("time"))
<Quantity(0.621371192, 'mile / hour')>
or keyword arguments:
>>> with uctx.override(length=ureg.mile, time=ureg.hour):
... ureg.Quantity(1.0, "km/hour").to(uctx.get("length") / uctx.get("time"))
<Quantity(0.621371192, 'mile / hour')>
Just like UnitGenerator, values may be strings:
>>> with uctx.override(length="mile", time="hour"):
... ureg.Quantity(1.0, "km/hour").to(uctx.get("length") / uctx.get("time"))
<Quantity(0.621371192, 'mile / hour')>
Non-string context keys#
Registry keys need not be strings. A string-valued enumeration is a common choice:
>>> import enum
>>> class PhysicalQuantity(enum.Enum):
... LENGTH = "length"
... SPEED = "speed"
... TIME = "time"
Its constructor doubles as a converter:
>>> PhysicalQuantity(PhysicalQuantity.LENGTH)
<PhysicalQuantity.LENGTH: 'length'>
>>> PhysicalQuantity("length")
<PhysicalQuantity.LENGTH: 'length'>
Declaring it as the context’s key_converter keeps strings usable, which
also keeps the keyword-argument form of override() working:
>>> uctx = pintext.UnitContext(key_converter=PhysicalQuantity)
>>> uctx.register(PhysicalQuantity.LENGTH, ureg.m)
>>> uctx.register("time", ureg.s)
>>> uctx.deferred(PhysicalQuantity.TIME)
UnitGenerator(units=<Unit('second')>)
>>> uctx.register(PhysicalQuantity.SPEED, pintext.UnitGenerator(
... lambda: uctx.get(PhysicalQuantity.LENGTH) /
... uctx.get(PhysicalQuantity.TIME)
... ))
>>> with uctx.override(length=ureg.km, time=ureg.hour):
... uctx.get("speed")
<Unit('kilometer / hour')>
Specifying units with strings#
UnitContext can interpret string values as Pint units and build
generators from them. The registry used is set by the ureg constructor
argument; if unset, the registry returned by get_unit_registry() is
used.
>>> uctx = pintext.UnitContext({"length": "m", "time": "s"}, interpret_str=True)
>>> uctx.get_all()
{'length': <Unit('meter')>, 'time': <Unit('second')>}
Warning
Pintext’s default registry is generally safe to use as it is Pint’s application registry. However, interpreting units against it can have unintended consequences: units from two different registries cannot be combined.
>>> other_ureg = pint.UnitRegistry()
>>> uctx.get("length") / other_ureg.m
Traceback (most recent call last):
...
ValueError: Cannot operate with Unit and Unit of different registries.