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.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
init
|
bool | str
|
Generate |
True
|
repr
|
bool | str
|
Generate |
True
|
eq
|
bool | str
|
Generate |
True
|
order
|
bool | str
|
Generate |
False
|
hash
|
bool | str
|
Generate |
None
|
unsafe_hash
|
bool
|
Always generate |
False
|
frozen
|
bool
|
Disable |
False
|
match_args
|
bool | str
|
Generate |
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 |
False
|
weakref_slot
|
bool
|
Generate a weakref slot in |
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 |
"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.
|
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 |
repr |
bool | str
|
Generate |
eq |
bool | str
|
Generate |
order |
bool | str
|
Generate |
hash |
bool | str
|
Generate |
unsafe_hash |
bool
|
Always generate |
frozen |
bool
|
Disable |
match_args |
bool | str
|
Generate |
kw_only |
bool
|
Make all fields keyword-only by default. |
positional_only |
bool
|
Make all fields positional-only by default. |
slots |
bool
|
Generate |
weakref_slot |
bool
|
Generate a weakref slot in |
factory |
bool
|
Use field type as factory if none is provided. |
mutable_default |
str
|
What to do with a mutable default such as |
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.
|
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__
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 |
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 |
{}
|
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
Build a Magic class from a plain class.
Takes the same options as Magic. Use this when inheritance is
not an option.
fields
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
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
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 |
asdict
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.
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.
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 |
astuple
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.
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 |
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
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
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 |
is_magic
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
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. |