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

Base class for data structures driven by type hints.

Inherit from Magic to get a generated __init__, __repr__ and __eq__. Options are class keyword arguments and are inherited by subclasses.

Example

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

Parameters:

Name Type Description Default
init bool | str

Generate __init__ method.

True
repr bool | str

Generate __repr__ method. A field is shown while it holds a value. A field the constructor does not take, with no default, is left out until something sets it.

True
eq bool | str

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

True
order bool | str

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

False
hash bool | str

Generate __hash__ method. If None, decide automatically.

None
unsafe_hash bool

Always generate __hash__ method.

False
frozen bool

Disable __setattr__ and __delattr__.

False
match_args bool | str

Generate __match_args__ for pattern matching.

False
kw_only bool

Make all fields keyword-only by default.

False
positional_only bool

Make all fields positional-only by default.

False
slots bool

Generate __slots__ and remove __dict__.

False
weakref_slot bool

Generate a weakref slot in __slots__.

False
factory bool

Use field type as factory if none is provided.

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, "allow" shares one object between instances.

"factory"
convert bool

Use field type as converter if none is provided.

False
validate bool

Use field type as validator if none is provided.

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 names something still undefined the first time a field needs it. "warn" says so once, "raise" turns it into an error, "ignore" says nothing.

"warn"
mapping bool

Implement the Mapping protocol. Only a field holding a value is a key.

False
override bool | str | list

Resolve an inherited field's settings again from this class. override=True applies to all inherited fields. A name or list of names applies to those settings only.

False
polymorphic bool | str

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

False
pin_discriminant str

What a subclass does with a field it matches on exactly. "pin" gives it a default, "classvar" makes it a class attribute, "keep" leaves it alone.

"pin"
reverse bool

Use the reverse MRO order to determine field order.

False
doc bool | str

Add field documentation to class docstring.

True

MetaMagic

Bases: ABCMeta

Metaclass that builds a Magic class.

Most users never name MetaMagic directly. Inherit from Magic instead, or use the @magic decorator.

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__ method.

repr bool | str

Generate __repr__ method. A field is shown while it holds a value. A field the constructor does not take, with no default, is left out until something sets it.

eq bool | str

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

order bool | str

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

hash bool | str

Generate __hash__ method. If None, decide automatically.

unsafe_hash bool

Always generate __hash__ method.

frozen bool

Disable __setattr__ and __delattr__.

match_args bool | str

Generate __match_args__ for pattern matching.

kw_only bool

Make all fields keyword-only by default.

positional_only bool

Make all fields positional-only by default.

slots bool

Generate __slots__ and remove __dict__.

weakref_slot bool

Generate a weakref slot in __slots__.

factory bool

Use field type as factory if none is provided.

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, "allow" shares one object between instances.

convert bool

Use field type as converter if none is provided.

validate bool

Use field type as validator if none is provided.

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 names something still undefined the first time a field needs it. "warn" says so once, "raise" turns it into an error, "ignore" says nothing.

mapping bool

Implement the Mapping protocol. Only a field holding a value is a key.

override bool | str | list

Resolve an inherited field's settings again from this class. override=True applies to all inherited fields. A name or list of names applies to those settings only.

polymorphic bool | str

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

pin_discriminant str

What a subclass does with a field it matches on exactly. "pin" gives it a default, "classvar" makes it a class attribute, "keep" leaves it alone.

reverse bool

Use the reverse MRO order to determine field order.

doc bool | str

Add field documentation to 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 (Pair[int, S]), or a class with no type parameters to fill, is handed back as the ordinary typing alias.

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

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

This is the same thing as class Sub(Base, on={...}), said after the fact -- for a class you did not write, or one whose constraints are only known at run time. Registering later only affects what is built later; instances that already exist are untouched.

Parameters:

Name Type Description Default
target type

The subclass to build. It must be a subclass of this class, and not this class itself.

required
on dict

What target stands for: field names against the values they must take. The same shapes as the on= class keyword.

None
priority int

Which subclass wins when two match equally well. Higher wins, and it is looked at before anything else.

0
**constraints Any

A friendlier spelling of on, for the usual case: Chord.register_polymorph(Diminished, mode="diminished"). Use on= for a field named on, target or priority.

{}

Returns:

Name Type Description
target type

What was registered, so this can be used as a decorator.

HIDE_IF_NONE

HIDE_IF_NONE(key: str | None = None)

Bases: SHOW_ATTR

Sentinel for Field.repr / Field.key: include the field in the generated __repr__ / dict-like interface only when its value is not None at runtime, instead of unconditionally.

class C(Magic):
    x: Annotated[Optional[int], Field(repr=HIDE_IF_NONE)]

repr(C(None))  # "C()"
repr(C(5))     # "C(x=5)"

Functions:

magic

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

Build a Magic class from a plain class.

Takes the same options as Magic. Use this when inheritance is not an option.

fields

fields(cls: type) -> tuple[Field]

Return the fields of a Magic class as a tuple.

Only concrete fields are included. ClassVar and InitVar fields are left out. A class that Magic never built has no fields, so the result is an empty tuple.

Parameters:

Name Type Description Default
cls type

A Magic class (not an instance).

required

Returns:

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

The class's concrete fields, in declaration order.

fields_dict

fields_dict(cls: type) -> dict[str, Field]

Get the fields of a Magic class, keyed by name.

The same fields fields returns, in the same order, keyed by the name each one is known by outside the class -- the name the constructor takes, which for an aliased or underscored field is not the name the class body uses.

Example

>>> class Point(Magic):
...     x: int
...     y: int
...
>>> list(fields_dict(Point))
['x', 'y']
>>> fields_dict(Point)["x"].type
<class 'int'>

Parameters:

Name Type Description Default
cls type

The class to get the fields of.

required

Returns:

Name Type Description
fields dict[str, Field]

All concrete fields (that are not ClassVar or InitVar). A class that Magic never built simply has none, so the answer is an empty dict -- the same as fields gives. asdict, astuple and replace need a real instance and say so instead.

asdict

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

Get an object's fields as a plain dict.

A field holding another Magic object is turned into a dict of its fields, and so on all the way down. Everything else is returned as-is.

Keys are the names the constructor takes, which for an aliased or underscored field is not the name the class body uses.

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 the constructor does not take, and that has no default, holds nothing until something sets it -- so it is simply absent, the way an optional key is absent from a dict. It comes back 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 is the one that insists instead: a key says which field it belongs to, a position does not, so a tuple is only readable while every field is in it.

Not the same as dict(obj)

A class written with mapping=True can be passed to dict directly, and that covers the fields marked as keys, under the key names they were given. asdict covers every field. The two agree about a field with no value: neither shows one.

>>> 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 concrete field (not ClassVar or InitVar) that is holding a value, in field order.

astuple

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

Get an object's field values as a tuple, in field order.

Like asdict, a nested Magic instance is turned into a tuple of its own fields. Everything else is returned as-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; this one says so. A position only means anything while every field is in the tuple: drop one and everything after it moves up, so the same index would stand for a different field from one instance to the next, 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 concrete field (not ClassVar or InitVar), in field order.

Raises:

Type Description
AttributeError

If a field has never been given a value. A field the constructor does not take, with no default, is only set if something sets it by hand.

replace

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

Copy an object, changing some of its values.

The copy is built by calling the class again, so conversion, validation and the __pre_init__ / __post_init__ hooks all run on the way in: a replaced value is checked exactly as an original one is. Anything you do not mention is carried over unchanged. It works on a frozen class, which is where it is most useful.

Name each change after the argument the constructor takes, which for an aliased or underscored field is not the name the class body uses.

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 gets whatever the class gives it rather than the value obj holds. An InitVar is passed in and not kept, so there is nothing to read back off obj: give it again, or leave it to its default.

A __post_init__ that derives a field runs again

The copy starts from the values as they are stored, so a hook that works one field out from another works it out a second time -- from the already worked-out value. replace with no changes at all can then come back different:

>>> 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 going back in have already been converted once, so a converter that does not give the same answer for its own output 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 -- int("7") and int(7) are both 7 -- so this is a question for one you wrote yourself. dataclasses.replace and attrs.evolve behave the same way, for the same reason: the copy is a construction, not a copy of the bytes.

Where that matters, turn conversion off for the fields it affects, or write the converter so that running it twice is the same 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 the constructor does not take; or if the class has an InitVar with no default and no value was given for it.

is_magic

is_magic(obj: Any) -> bool

Say whether something was built by Magic.

Answers for a class or for one of its instances, and for a class built either way -- 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.