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.
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
strcsregister, 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,
strcswill fallback to the original creator that would be used.Note
The return of this is a
strcs.ConvertFunctionwhich 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.ConvertFunctionform:a = strcs.Ann[T](creator=my_function) return a.adjusted_creator(creator, register, typ, type_cache)
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
from typing import Annotated
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: Annotated[int, strcs.Ann(MathsAnnotation(addition=20), do_maths)]
@attrs.define
class Holder:
once: Thing
twice: Annotated[Thing, MathsAnnotation(multiplication=2)]
thrice: 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.AdjustedMetaandstrcs.AdjustedCreator.This object takes in a
metaand acreator(both are optional) and will adjust the meta and/or creator based on those values.The creator object may be any
strcs.ConvertDefinitioncallable and will be used as a creator override if provided.The meta object may be either an object satisfying
strcs.AdjustableMetaor an instance ofstrcs.MetaAnnotationorstrcs.MergedMetaAnnotation.- class MetaAnnotation
A class representing information that may be added into into the meta object used at creation time.
When
strcssees this object in atyping.Annotationfor the type of a field, it will create astrcs.Ann(meta=instance)and theadjusted_metahook 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 from typing import Annotated 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: 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
strcssees this object in atyping.Annotationfor the type of a field, it will create astrcs.Ann(meta=instance)and theadjusted_metahook will add all the fields on the instance onto the meta object, overriding any fields with the same key.Usage looks like:
import attrs from typing import Annotated 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: 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.AdjustedMetaandstrcs.AdjustedCreatorused to override a value with something found in the meta object.Usage looks like:
import attrs from typing import Annotated import strcs reg = strcs.CreateRegister() creator = reg.make_decorator() class Magic: def incantation(self) -> str: return "abracadabra!" @attrs.define class Wizard: magic: Annotated[Magic, strcs.FromMeta("magic")] wizard = reg.create(Wizard, meta=reg.meta({"magic": Magic()})) assert wizard.magic.incantation() == "abracadabra!"