Annotations
Developers may target customization for specific fields by annotating their types
with objects that strcs
knows how to use.
This is done by taking advantage of the typing.Annotated
functionality that
has existed since Python 3.9
There are two interfaces that strcs
will look for when determining if a
typing.Annotation
value may be used to customize creation.
- protocol strcs.AdjustableMeta
An interface used to modify the meta object when creating a field.
The hook is provided the original meta object, the type that needs to be created, and the current type cache.
This hook must return the meta object to use. Modifications to the meta object must be made on a clone of the meta object.
The new Meta will persist for any transformation that occurs below the field.
This protocol is runtime checkable.
Classes that implement this protocol must have the following methods / attributes:
- adjusted_meta(meta: strcs.Meta, typ: strcs.Type[T], type_cache: strcs.TypeCache) strcs.Meta
- protocol strcs.AdjustableCreator
An interface used to modify the creator used when creating a field.
The hook is provided the original creator function that would be used, the current
strcs
register, the type that needs to be created, and the current type cache.This hook must return either a creator function or None.
If None is returned,
strcs
will fallback to the original creator that would be used.Note
The return of this is a
strcs.ConvertFunction
which is a normalised form of the creator functions that developers interact with. It is recommended to do the following to convert such a function into thestrcs.ConvertFunction
form:a = strcs.Ann[T](creator=my_function) return a.adjusted_creator(creator, register, typ, type_cache)
This protocol is runtime checkable.
Classes that implement this protocol must have the following methods / attributes:
- adjusted_creator(creator: strcs.ConvertFunction[T] | None, register: strcs.CreateRegister, typ: strcs.Type[T], type_cache: strcs.TypeCache) strcs.ConvertFunction[T] | None
strcs
will also recognise instances of strcs.MetaAnnotation
and
strcs.MergedMetaAnnotation
and turn those into instances of strcs.Ann
,
which is a concrete implementation of both AdjustableMeta
and AdjustableCreator
.
Callable objects will also be turned into a strcs.Ann
by treating the callable
object as a creator override.
import attrs
import typing as tp
import strcs
reg = strcs.CreateRegister()
creator = reg.make_decorator()
@attrs.define(frozen=True)
class MathsAnnotation(strcs.MergedMetaAnnotation):
addition: int | None = None
multiplication: int | None = None
def do_maths(value: int, /, addition: int = 0, multiplication: int = 1) -> int:
return (value + addition) * multiplication
@attrs.define
class Thing:
val: tp.Annotated[int, strcs.Ann(MathsAnnotation(addition=20), do_maths)]
@attrs.define
class Holder:
once: Thing
twice: tp.Annotated[Thing, MathsAnnotation(multiplication=2)]
thrice: tp.Annotated[Thing, MathsAnnotation(multiplication=3)]
@creator(Thing)
def create_thing(value: object) -> dict | None:
if not isinstance(value, int):
return None
return {"val": value}
@creator(Holder)
def create_holder(value: object) -> dict:
if not isinstance(value, int):
return None
return {"once": value, "twice": value, "thrice": value}
holder = reg.create(Holder, 33)
assert isinstance(holder, Holder)
assert attrs.asdict(holder) == {"once": {"val": 53}, "twice": {"val": 106}, "thrice": {"val": 159}}
Note
it is a good idea to set a default value when retrieving multiple values
from meta that have the same type. In the example above addition
and
multiplication
are both ints and to force strcs
to match by name a
default is specified. Otherwise if only addition or multiplication are in meta
then they will both be set to the value of the one that is found.
- class strcs.Ann(meta: strcs.MetaAnnotation | strcs.MergedMetaAnnotation | strcs.AdjustableMeta[T] | None = None, creator: strcs.ConvertDefinition[T] | None = None)
A concrete implementation of both
strcs.AdjustedMeta
andstrcs.AdjustedCreator
.This object takes in a
meta
and acreator
(both are optional) and will adjust the meta and/or creator based on those values.The creator object may be any
strcs.ConvertDefinition
callable and will be used as a creator override if provided.The meta object may be either an object satisfying
strcs.AdjustableMeta
or an instance ofstrcs.MetaAnnotation
orstrcs.MergedMetaAnnotation
.- class MetaAnnotation
A class representing information that may be added into into the meta object used at creation time.
When
strcs
sees this object in atyping.Annotation
for the type of a field, it will create astrcs.Ann(meta=instance)
and theadjusted_meta
hook will add a__call_defined_annotation__
property to the meta object holding the instance so that it can be asked for in the creator asannotation
.Usage looks like:
import attrs import typing as tp import strcs reg = strcs.CreateRegister() creator = reg.make_decorator() @attrs.define(frozen=True) class MyAnnotation(strcs.MetaAnnotation): one: int two: int @attrs.define class MyKls: key: tp.Annotated[str, MyAnnotation(one=1, two=2)] @creator(MyKls) def create_mykls(value: object, /, annotation: MyAnnotation) -> dict | None: if not isinstance(value, str): return None return {"key": f"{value}-{annotation.one}-{annotation.two}"}
- class MergedMetaAnnotation
A class representing information that may be merged into into the meta object used at creation time.
When
strcs
sees this object in atyping.Annotation
for the type of a field, it will create astrcs.Ann(meta=instance)
and theadjusted_meta
hook will add all the fields on the instance onto the meta object, overriding any fields with the same key.Usage looks like:
import attrs import typing as tp import strcs reg = strcs.CreateRegister() creator = reg.make_decorator() @attrs.define(frozen=True) class MyAnnotation(strcs.MergedMetaAnnotation): one: int two: int @attrs.define class MyKls: key: tp.Annotated[str, MyAnnotation(one=1, two=2)] @creator(MyKls) def create_mykls(value: object, /, one: int = 0, two: int = 0) -> dict | None: if not isinstance(value, str): return None return {"key": f"{value}-{one}-{two}"}
Optional keys are not added to meta if they are not set:
@attrs.define(frozen=True) class MyAnnotation(strcs.MergedMetaAnnotation): one: int | None = None two: int | None = None @creator(MyKls) def create_mykls(value: object, /, one: int = 0, two: int = 0) -> dict | None: if not isinstance(value, str): return None # one and two will be zero each instead of None when MyKls # is annotated with either of those not set respectively return {"key": f"{value}-{one}-{two}"}
- class strcs.FromMeta(pattern: str)
An implementation of both
strcs.AdjustedMeta
andstrcs.AdjustedCreator
used to override a value with something found in the meta object.Usage looks like:
import attrs import typing as tp import strcs reg = strcs.CreateRegister() creator = reg.make_decorator() class Magic: def incantation(self) -> str: return "abracadabra!" @attrs.define class Wizard: magic: tp.Annotated[Magic, strcs.FromMeta("magic")] wizard = reg.create(Wizard, meta=reg.meta({"magic": Magic()})) assert wizard.magic.incantation() == "abracadabra!"