attrs integration#
This extension requires that the attrs package is installed. If not done
externally in your project, it can be requested with the attrs extra:
python -m pip install "pintext[attrs]"
pintext.attrs.field() mirrors attrs.field() and adds a units
argument, which attaches units to a field:
>>> import attrs, pint, pintext
>>> from pintext.attrs import field
>>> ureg = pintext.get_unit_registry()
>>> @attrs.define
... class MyClass:
... value = field(units=ureg.km)
>>> MyClass(1.0)
MyClass(value=1.0 km)
Note
If units is unset, pintext.attrs.field() behaves exactly like
attrs.field().
Unitless values are automatically wrapped. If a Pint quantity is passed, its units are checked; when they are compatible in the sense of Pintext, the value is assigned unchanged:
>>> MyClass(1.0 * ureg.m)
MyClass(value=1.0 m)
Incompatible units make the built-in validator raise a
UnitsError:
>>> MyClass(1.0 * ureg.s)
Traceback (most recent call last):
...
pintext.exceptions.UnitsError: Cannot convert from 'second' to 'kilometer': incompatible units 'second' used to set field 'value' (allowed: 'kilometer').
By default, conversion and validation also apply on assignment:
>>> o = MyClass(1.0)
>>> o
MyClass(value=1.0 km)
>>> o.value = 1.0 * ureg.s
Traceback (most recent call last):
...
pintext.exceptions.UnitsError: Cannot convert from 'second' to 'kilometer': incompatible units 'second' used to set field 'value' (allowed: 'kilometer').
>>> o.value = 1.0 * ureg.m
>>> o
MyClass(value=1.0 m)
>>> o.value = 1.0
>>> o
MyClass(value=1.0 km)
Note
To opt out, pass attrs.setters.NO_OP:
>>> @attrs.define
... class AnotherClass:
... value = field(units=ureg.km, on_setattr=attrs.setters.NO_OP)
>>> o = AnotherClass(1.0)
>>> o
AnotherClass(value=1.0 km)
>>> o.value = 1.0
>>> o
AnotherClass(value=1.0)
Passing on_setattr=None is not equivalent: None means “defer to
the class-level setting”, and attrs.define() installs a
convert-and-validate pipeline of its own, so conversion still happens.
>>> @attrs.define
... class AnotherClass:
... value = field(units=ureg.km, on_setattr=None)
>>> o = AnotherClass(1.0)
>>> o.value = 1.0
>>> o
AnotherClass(value=1.0 km)
None is however what you need for frozen classes, which reject any
on_setattr at all:
>>> @attrs.frozen
... class AnotherClass:
... value = field(units=ureg.m)
Traceback (most recent call last):
...
ValueError: Frozen classes can't use on_setattr.
>>> @attrs.frozen
... class AnotherClass:
... value = field(units=ureg.m, on_setattr=None)
Fields with units also get a repr suited to displaying quantities. The
original one is restored by passing repr=True:
>>> @attrs.define
... class AnotherClass:
... value = field(units=ureg.km, repr=True)
>>> AnotherClass(1.0)
AnotherClass(value=<Quantity(1.0, 'kilometer')>)
Unit contexts#
The units argument accepts a UnitGenerator, so a field can read
its units from a unit context. Units are then resolved
at instantiation time:
>>> uctx = pintext.UnitContext({"length": ureg.m})
>>> @attrs.define
... class MyClass:
... value = field(units=uctx.deferred("length"))
>>> MyClass(1.0)
MyClass(value=1.0 m)
>>> with uctx.override(length="km"):
... MyClass(1.0)
MyClass(value=1.0 km)
Validators and converters#
Under the hood, field() composes a converter
(ensure_units() in deferred mode) and a validator
(has_compatible_units()). Both can be used directly to
customize field behaviour further. See pintext.attrs for details.