Skip to content

bagof.magic

Classes that build themselves from your type hints.

Everything here is re-exported from the modules beside this one; import it from bagof.magic rather than from those, which are free to move.

Classes

Magic

The base class for classes built from their type hints.

A class that inherits from Magic receives a generated __init__, __repr__ and __eq__, among other methods. The options below are class keyword arguments, and subclasses inherit them. Most options that generate a method also accept a string in place of True, and the generated method is then bound under that name.

Example

class Point(Magic, frozen=True):
    x: float
    y: float

Parameters:

Name Type Description Default
init bool | str

Generate __init__.

True
repr bool | str | ShowIf

Generate __repr__. A field is shown while it holds a value, so a field that the constructor does not take, and that has no default, is left out until something sets it. A test such as HideIfNone() or ShowIf(bool) is applied to every field.

True
eq bool | str

Generate __eq__. Two objects are equal when the same fields hold values and those values match.

True
order bool | str

Generate __lt__, __le__, __gt__ and __ge__. Given a name, the < comparison is generated under that name.

False
hash bool | str

Generate __hash__. With None, whether the method is generated is decided from the other options.

None
unsafe_hash bool

Generate __hash__ even when the class is mutable.

False
frozen bool

Refuse to assign or delete a field after construction.

False
match_args bool | str

Generate __match_args__, for structural pattern matching.

False
kw_only bool

Make every field keyword-only, unless the field says otherwise.

False
positional_only bool

Make every field positional-only, unless the field says otherwise.

False
slots bool

Generate __slots__, so that instances have no __dict__.

False
weakref_slot bool

Add a __weakref__ slot, so that the instances of a class with slots=True can be weakly referenced.

False
factory bool

Use a field's type as its factory when no factory is given.

False
mutable_default str

What to do with a mutable default such as x: list = []. "factory" gives each instance its own copy, "raise" refuses the class, and "allow" shares one object between the instances.

"factory"
convert bool

Use a field's type as its converter when no converter is given.

False
validate bool

Use a field's type as its validator when no validator is given.

False
convert_defaults bool

Convert a value that came from a field's own default.

True
validate_defaults bool

Validate a value that came from a field's own default.

True
unresolved_hints str

What to do when a type hint still names something undefined the first time a field needs it. "warn" says so once, "raise" turns it into an error, and "ignore" says nothing.

"warn"
alias str | sequence | bool

The default input names of the fields, the first of which is preferred. True adds the names of the enabled properties, and False keeps a leading underscore in the input name.

required
property str | sequence | mapping | bool

The default forwarding properties of the stored instance fields. True exposes the public name, "readonly" exposes it read-only, and "all" exposes every input alias. Names given explicitly can each choose their own access.

False
mapping bool

Implement the Mapping protocol. Only a field that holds a value is a key.

False
override bool | str | list

Resolve the settings of inherited fields again from this class. override=True applies to every inherited field, and a setting name or a list of names limits it to those settings.

False
polymorphic bool | str

Build one of this class's subclasses instead of this class, chosen from the arguments of the call. With "strict", a call that matches no subclass is refused.

False
pin_discriminant str

What a subclass does with a field that it matches on. "pin" gives the field the matched value as its default, "classvar" makes it a class attribute, and "keep" leaves it as the subclass wrote it. Adding "+narrow" also narrows the field's type to what the subclass stands for and rejects any other value, which gives "pin+narrow", "classvar+narrow" and "keep+narrow". "narrow" on its own means "pin+narrow". A field that chooses its own mode with Pin[T, mode], Narrow[T] or NoPin[T] keeps it.

"pin"
reverse bool

Collect the fields from the bases in reverse order, so that a subclass's own fields come before the inherited ones.

False
doc bool | str

Add the table of fields to the class docstring.

True

MetaMagic

Bases: ABCMeta

The metaclass that builds a Magic class.

MetaMagic rarely needs to be named directly. A class inherits from Magic, or is decorated with magic, instead.

Parameters:

Name Type Description Default
name str

The name of the class being defined.

required
bases tuple[type, ...]

The base classes of the class being defined.

required
namespace dict

The namespace of the class being defined.

required

Other Parameters:

Name Type Description
init bool | str

Generate __init__.

repr bool | str | ShowIf

Generate __repr__. A field is shown while it holds a value, so a field that the constructor does not take, and that has no default, is left out until something sets it. A test such as HideIfNone() or ShowIf(bool) is applied to every field.

eq bool | str

Generate __eq__. Two objects are equal when the same fields hold values and those values match.

order bool | str

Generate __lt__, __le__, __gt__ and __ge__. Given a name, the < comparison is generated under that name.

hash bool | str

Generate __hash__. With None, whether the method is generated is decided from the other options.

unsafe_hash bool

Generate __hash__ even when the class is mutable.

frozen bool

Refuse to assign or delete a field after construction.

match_args bool | str

Generate __match_args__, for structural pattern matching.

kw_only bool

Make every field keyword-only, unless the field says otherwise.

positional_only bool

Make every field positional-only, unless the field says otherwise.

slots bool

Generate __slots__, so that instances have no __dict__.

weakref_slot bool

Add a __weakref__ slot, so that the instances of a class with slots=True can be weakly referenced.

factory bool

Use a field's type as its factory when no factory is given.

mutable_default str

What to do with a mutable default such as x: list = []. "factory" gives each instance its own copy, "raise" refuses the class, and "allow" shares one object between the instances.

convert bool

Use a field's type as its converter when no converter is given.

validate bool

Use a field's type as its validator when no validator is given.

convert_defaults bool

Convert a value that came from a field's own default.

validate_defaults bool

Validate a value that came from a field's own default.

unresolved_hints str

What to do when a type hint still names something undefined the first time a field needs it. "warn" says so once, "raise" turns it into an error, and "ignore" says nothing.

alias str | sequence | bool

The default input names of the fields, the first of which is preferred. True adds the names of the enabled properties, and False keeps a leading underscore in the input name.

property str | sequence | mapping | bool

The default forwarding properties of the stored instance fields. True exposes the public name, "readonly" exposes it read-only, and "all" exposes every input alias. Names given explicitly can each choose their own access.

mapping bool

Implement the Mapping protocol. Only a field that holds a value is a key.

override bool | str | list

Resolve the settings of inherited fields again from this class. override=True applies to every inherited field, and a setting name or a list of names limits it to those settings.

polymorphic bool | str

Build one of this class's subclasses instead of this class, chosen from the arguments of the call. With "strict", a call that matches no subclass is refused.

pin_discriminant str

What a subclass does with a field that it matches on. "pin" gives the field the matched value as its default, "classvar" makes it a class attribute, and "keep" leaves it as the subclass wrote it. Adding "+narrow" also narrows the field's type to what the subclass stands for and rejects any other value, which gives "pin+narrow", "classvar+narrow" and "keep+narrow". "narrow" on its own means "pin+narrow". A field that chooses its own mode with Pin[T, mode], Narrow[T] or NoPin[T] keeps it.

reverse bool

Collect the fields from the bases in reverse order, so that a subclass's own fields come before the inherited ones.

doc bool | str

Add the table of fields to the class docstring.

Returns:

Name Type Description
cls type

The class being defined.

Methods:

__getitem__
__getitem__(parameters: Any) -> Any

Fill in a generic Magic class's type parameters.

Box[int] on a class written class Box(Magic, Generic[T]) returns a real subclass with T replaced by int throughout its fields, so Box[int]("1") converts and validates exactly as class IntBox(Box[int]) does. A subscription that leaves a type variable free, as in Pair[int, S], returns the ordinary typing alias, and so does a class with a variadic or ParamSpec type parameter.

A polymorphic class still chooses which subclass to build. The subclass is chosen from the arguments, and it comes back with the same parameters filled in.

register_polymorph
register_polymorph(target: MaybeMissing[type] = MISSING, on: Mapping[str, Any] | None = None, priority: int = 0, **constraints: Any) -> type | Callable[[type], type]

Build target instead of this class, for these argument values.

A registration does what class Sub(Base, on=...) does, after the class has been written. It suits a class from another module, or a class whose constraints are only known at run time. A registration affects only what is built after it, and instances that already exist are not changed.

Only this class builds target, and a subclass of target written later with on= is reached through target. A subclass written before the registration was placed according to the registrations that existed at the time, so a class should be registered before its subclasses are written. A strict target refuses a direct call that contradicts the first registration made for it.

Without target, the method returns a decorator, which registers the class it decorates and returns that class:

@Base.register_polymorph(mode="diminished")
class Diminished(Base):
    ...

Parameters:

Name Type Description Default
target type

The subclass to build. It must be a subclass of this class other than the class itself. Without it, the method returns a decorator that registers the class it decorates.

MISSING
on dict

What target stands for, as a mapping from field names to the values they must take. It accepts the same constraints as the on= class keyword.

None
priority int

Which subclass wins when two of them match. The higher priority wins, and priority is compared before any other rule.

0
**constraints Any

The constraints as keyword arguments, which is the shorter spelling for the usual case: Chord.register_polymorph(Diminished, mode="diminished"). A field named on, target or priority has to be given through on=.

{}

Returns:

Name Type Description
target type

The registered class, so that the method can be used as a decorator. When target is left out, the result is the decorator itself.

ShowIf

ShowIf(predicate: Callable[[Any], Any], /, **kwargs: Any)

Bases: Repr

Show a field in __repr__ only while a test of its value passes.

ShowIf(test) calls test(value) and shows the field when the result is true. On a dict-like class it also works as a key setting, as in Key(ShowIf(test)), where it leaves the key out while the test fails.

After the test, ShowIf takes the same keywords as field(), so it can be written as a field's value: notes: str = ShowIf(bool, default=""). A test given such keywords also declares the field, and so it cannot be used as a repr or key setting on its own.

How it lowers

>>> ShowIf(bool)
ShowIf(<class 'bool'>)
>>> ShowIf[int, bool]
typing.Annotated[int, ShowIf(<class 'bool'>)]
>>> Repr(ShowIf(bool))
Repr(repr=ShowIf(<class 'bool'>))

In a class

>>> class Order(Magic):
...     item: str
...     notes: ShowIf[str, bool] = ""
...
>>> Order("tea")
Order(item='tea')
>>> Order("tea", "no sugar")
Order(item='tea', notes='no sugar')

HideIf

HideIf(predicate: Callable[[Any], Any], /, **kwargs: Any)

Bases: ShowIf

Hide a field from __repr__ while a test of its value passes.

HideIf is the opposite of ShowIf: HideIf(test) hides the field when test(value) is true. Like ShowIf, it takes the keywords of field() after the test.

In a class

>>> class Retry(Magic):
...     attempts: HideIf[int, lambda n: n < 0] = -1
...
>>> Retry()
Retry()
>>> Retry(3)
Retry(attempts=3)

HideIfNone

HideIfNone(**kwargs: Any)

Bases: HideIf

Hide a field from __repr__ while it holds None.

The class can be written wherever an instance can, so Field(repr=HideIfNone) is the same as Field(repr=HideIfNone()). Given as a class setting, as in class C(Magic, repr=HideIfNone()), it applies to every field.

It takes the keywords of field(), so it can be written as a field's value: nickname: Optional[str] = HideIfNone(default=None).

In a class

>>> class Person(Magic):
...     name: str
...     nickname: HideIfNone[tx.Optional[str]] = None
...
>>> Person("Margaret")
Person(name='Margaret')
>>> Person("Margaret", "Peggy")
Person(name='Margaret', nickname='Peggy')

HideIfDefault

HideIfDefault(**kwargs: Any)

Bases: HideIf

Hide a field from __repr__ while it holds its default.

A value counts as the default when it is the default object or compares equal to it, and a value that cannot be compared with the default is shown. The field needs a plain default to compare with, so a default built by a factory is refused, because each instance has its own.

Given as a class setting, as in class C(Magic, repr=HideIfDefault()), it applies to every field that has a plain default, and the other fields are always shown.

It takes the keywords of field(), so it can be written as a field's value together with the default it compares with: method: str = HideIfDefault(default="GET").

In a class

>>> class Request(Magic):
...     url: str
...     method: HideIfDefault[str] = "GET"
...
>>> Request("/home")
Request(url='/home')
>>> Request("/home", "POST")
Request(url='/home', method='POST')

HIDE_IF_NONE

HIDE_IF_NONE(key: str | None = None)

Bases: HideIfNone

The original spelling of HideIfNone, which is still accepted.

As a key setting, HIDE_IF_NONE("name") also renames the key, as Key("name", HideIfNone()) does.

Functions:

magic

magic(**kwargs) -> Callable[[type], type]
magic(cls: type, **kwargs) -> type

Build a Magic class from a plain class.

The decorator takes the same options as Magic and builds the same class, without adding Magic to its bases. It is written as @magic on its own, or as @magic(frozen=True) with options, and it suits a class that cannot inherit from Magic.

fields

fields(cls: type, *, var: bool | None = False, **filters: Any) -> tuple[Field, ...]

Return a Magic class's fields as a tuple.

ClassVar and InitVar are excluded by default. Use var=True to get those pseudo-fields, or var=None to include all fields. Filters and field order are the same as for iterfields.

Example

>>> class Counter(Magic):
...     count: int
...     scale: InitVar[int] = 1
...     unit: ClassVar[str] = "clicks"
>>> [f.name for f in fields(Counter)]
['count']
>>> [f.name for f in fields(Counter, var=True)]
['scale', 'unit']
>>> [f.name for f in fields(Counter, var=True, init=False)]
['unit']
>>> [f.name for f in fields(Counter, var=None, init=True)]
['count', 'scale']

Parameters:

Name Type Description Default
cls type

A Magic class (not an instance). A plain class has no fields.

required
var bool or None

Select concrete fields (False), pseudo-fields (True), or both (None).

False
**filters Any

Public Field attributes to match by equality. All must match.

{}

Returns:

Name Type Description
fields tuple[Field, ...]

Matching fields in declaration order, or an empty tuple for a class that Magic did not build.

iterfields

iterfields(cls: type, *, var: bool | None = False, **filters: Any) -> Iterator[Field]

Iterate over a Magic class's fields, optionally filtering them.

Only concrete fields are included by default. Use var=True for pseudo-fields (ClassVar and InitVar), or var=None for both kinds. Additional filters match public Field attributes by equality, and every filter must match. For example, iterfields(cls, var=True, init=True) selects only init variables.

Parameters:

Name Type Description Default
cls type

A Magic class (not an instance). A plain class has no fields.

required
var bool or None

Select concrete fields (False), pseudo-fields (True), or both (None).

False
**filters Any

Public Field attributes to match, such as init, frozen, convert, name, type or default. None is an ordinary filter value here; only var=None disables a filter.

{}

Returns:

Name Type Description
fields Iterator[Field]

Matching fields in declaration order, including inherited ones. The field objects are the ones held by the class, not copies.

Raises:

Type Description
TypeError

If a filter names no public field attribute, or var is not True, False or None.

dictfields

dictfields(cls: type, *, by: Literal['name', 'public_name'] = 'public_name', var: bool | None = False, **filters: Any) -> dict[str, Field]

Return a Magic class's fields as a dictionary.

Keys are public names by default: the names the constructor takes, including aliases and names with a leading underscore removed. Use by="name" for variable names from the class body. Filters and field order are the same as for iterfields.

Example

>>> class Point(Magic):
...     _x: int
...     y: int
>>> list(dictfields(Point))
['x', 'y']
>>> list(dictfields(Point, by="name"))
['_x', 'y']
>>> dictfields(Point)["x"].type
<class 'int'>

Parameters:

Name Type Description Default
cls type

A Magic class (not an instance). A plain class has no fields.

required
by (name, public_name)

Which field attribute to use as the dictionary key.

"name"
var bool or None

Select concrete fields (False), pseudo-fields (True), or both (None).

False
**filters Any

Public Field attributes to match by equality. All must match.

{}

Returns:

Name Type Description
fields dict[str, Field]

Matching fields in declaration order, or an empty dict for a class that Magic did not build.

fields_dict

fields_dict(cls: type, *, by: Literal['name', 'public_name'] = 'public_name', var: bool | None = False, **filters: Any) -> dict[str, Field]

Compatibility name for dictfields.

Accepts the same filters, with by="public_name" as its default to preserve existing behavior. Prefer dictfields in new code.

asdict

asdict(obj: Any) -> dict[str, Any]

Return the fields of a Magic instance as a plain dict.

A field that holds another Magic instance is turned into a dict of that instance's fields, and so on at every level. Magic instances inside lists, tuples and dicts are turned into dicts too, and every other value is returned as it is.

The keys are the names that the constructor takes. For a field with an alias, or a field whose name starts with an underscore, the key differs from the name used in the class body.

Example

>>> class Point(Magic):
...     x: int
...     y: int
...
>>> asdict(Point(1, 2))
{'x': 1, 'y': 2}

Nested Magic objects become dicts

>>> class Point(Magic):
...     x: int
...     y: int
...
>>> class Line(Magic):
...     start: Point
...     end: Point
...
>>> asdict(Line(Point(0, 0), Point(1, 2)))
{'start': {'x': 0, 'y': 0}, 'end': {'x': 1, 'y': 2}}
>>> class Path(Magic):
...     points: list
...
>>> asdict(Path([Point(0, 0), Point(1, 2)]))
{'points': [{'x': 0, 'y': 0}, {'x': 1, 'y': 2}]}

A field with no value is left out

A field that the constructor does not take, and that has no default, holds nothing until something sets it. Such a field is absent from the result, in the way that an optional key is absent from a dict, and it appears as soon as it is given a value:

>>> class Draft(Magic):
...     title: str
...     slug: NoInit[str]
...
>>> draft = Draft("Ada")
>>> asdict(draft)
{'title': 'Ada'}
>>> draft.slug = "ada"
>>> asdict(draft)
{'title': 'Ada', 'slug': 'ada'}

astuple raises an error for such a field instead. A key says which field a value belongs to and a position does not, so a tuple can only be read correctly while every field is in it.

Not the same as dict(obj)

An instance of a class written with mapping=True can be passed to dict directly. The result covers only the fields that are keys, under their key names, whereas asdict covers every field. Both leave out a field that holds no value.

>>> class Row(Magic, mapping=True):
...     name: str
...     age: NotKey[int]
...
>>> dict(Row("ada", 36))
{'name': 'ada'}
>>> asdict(Row("ada", 36))
{'name': 'ada', 'age': 36}

Parameters:

Name Type Description Default
obj Magic

An instance of a Magic class.

required

Returns:

Name Type Description
values dict[str, any]

Every field that holds a value, in field order, leaving out ClassVar and InitVar fields.

astuple

astuple(obj: Any) -> tuple[Any, ...]

Return the field values of a Magic instance as a tuple, in field order.

As in asdict, a nested Magic instance is turned into a tuple of its own field values, and every other value is returned as it is.

Example

>>> class Point(Magic):
...     x: int
...     y: int
...
>>> astuple(Point(1, 2))
(1, 2)

A field with no value is an error here

asdict and dict(obj) leave such a field out, but astuple raises an error. A position only has a meaning while every field is in the tuple. If one field were dropped, every field after it would move up, and the same index would stand for different fields in different instances, with nothing in the tuple to show it.

Parameters:

Name Type Description Default
obj Magic

An instance of a Magic class.

required

Returns:

Name Type Description
values tuple

The value of every field, in field order, leaving out ClassVar and InitVar fields.

Raises:

Type Description
AttributeError

If a field has never been given a value. A field that the constructor does not take, and that has no default, holds a value only once something sets it.

replace

replace(obj: Any, **changes: Any) -> Any

Return a copy of a Magic instance with some of its values changed.

The copy is built by calling the class again, so conversion, validation and the __pre_init__ and __post_init__ hooks all run, and a replaced value is checked exactly as an original one is. Every field that is not mentioned is carried over unchanged. replace also works on a frozen class, which is where it is most useful.

Each change is named after an argument that the constructor takes. Any of a field's input aliases can be used, but giving the same field under two names is an error.

Example

>>> class Point(Magic, frozen=True):
...     x: int
...     y: int
...
>>> replace(Point(1, 2), y=20)
Point(x=1, y=20)

Two kinds of field cannot be carried over

A field written as NoInit[...] is not a constructor argument, so the copy receives whatever the class gives it, rather than the value that obj holds. An InitVar is passed to the constructor and not kept, so there is nothing to read back from obj. Such a field is either given again or left to its default.

A __post_init__ that derives a field runs again

The copy starts from the values as they are stored. A hook that derives one field from another therefore derives it a second time, from the value that it already derived, and replace with no changes at all can then return a different value:

>>> class Priced(Magic):
...     total: float
...     vat: InitVar[float] = 0.2
...
...     def __post_init__(self, arguments):
...         self.total = self.total * (1 + arguments.vat)
...
>>> order = Priced(100.0)
>>> order
Priced(total=120.0)
>>> replace(order)
Priced(total=144.0)

A hook that only checks its arguments, or that fills in a field the constructor does not take, is unaffected.

A converter runs again too

The same applies to conversion. The values passed back to the constructor have already been converted once, so a converter that does not return its own output unchanged changes the value each time:

>>> def double(value):
...     return value * 2
...
>>> class Doubled(Magic):
...     x: ConvertTo[int, double] = 3
...
>>> Doubled()
Doubled(x=6)
>>> replace(Doubled())
Doubled(x=12)

A converter that comes from a type hint is almost always safe here, because int("7") and int(7) are both 7, so the question arises only for a converter written by hand. dataclasses.replace and attrs.evolve behave in the same way, for the same reason: the copy is a new construction, not a copy of the stored data.

Where this matters, conversion can be turned off for the fields concerned, or the converter can be written so that running it twice gives the same result as running it once.

Parameters:

Name Type Description Default
obj Magic

An instance of a Magic class.

required
**changes any

New values, by constructor argument name.

{}

Returns:

Name Type Description
copy Magic

A new instance of the same class.

Raises:

Type Description
TypeError

If obj is not an instance of a Magic class, if a change names something that the constructor does not take, or if the class has an InitVar with no default and no value is given for it.

is_magic

is_magic(obj: Any) -> bool

Return whether a class, or the class of an object, was built by Magic.

The answer is the same for a class and for its instances. A class counts whichever way it was built, by inheriting from Magic or by decorating a plain class with magic.

Example

>>> class Point(Magic):
...     x: int
...
>>> is_magic(Point), is_magic(Point(1))
(True, True)
>>> is_magic(int), is_magic(3)
(False, False)

Parameters:

Name Type Description Default
obj any

A class, or any object at all.

required

Returns:

Name Type Description
is_magic bool

Whether the class, or the object's class, is a Magic class.