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 the strcs.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 and strcs.AdjustedCreator.

This object takes in a meta and a creator (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 of strcs.MetaAnnotation or strcs.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 a typing.Annotation for the type of a field, it will create a strcs.Ann(meta=instance) and the adjusted_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 as annotation.

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 a typing.Annotation for the type of a field, it will create a strcs.Ann(meta=instance) and the adjusted_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 and strcs.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!"