Module dataclass_baseclass
DataClass - inheritable contagious base class.
Instead of (endless?) @dataclass
decorating.
Usage
class A(DataClass): # it's a dataclass
class B(A): # it's a dataclass too
as opposed to:
@dataclass
class A(): ... # it's a dataclass
class B(A): ... # it's *not* a dataclass, needs decorating
@dataclass
class B(A): ... # now it's a dataclass
Also:
class B(DataClass, A): ... # all properties from A are dataclassed
as opposed to:
class A(): ...
@dataclass
class B(A): ... # no properties from A are dataclassed
Instantiation
class C(DataClass):
a: str
b: str
defaults: Data = {"a": "A", "b": "B"}
c = C(defaults, a="a", b="b")
or just:
c = C(a="a", b="b")
Classes
class DataClass
-
Expand source code
class DataClass(metaclass=DataClassMeta): """Base dataclass class. Includes `DataClassLoadProtocol`. It is recommended that all non-property additions to the class be underscored, to minimise conflict possibility. """ _frozen: ClassVar[bool] """Set to true if the class is frozen (a subclass of `DataClassFrozen`)""" # For some reason classmethods declared here are ignored, and have to be # added in the Meta. @classmethod # Has to return Any, otherwise cannot declare attributes as # attr: some_type = DataClass._field(...) def _field(cls, **kwargs: Any) -> Any: ... # pragma: no cover @classmethod def _fields( # type: ignore[empty-body] cls, ) -> tuple[Field[object], ...]: ... # pragma: no cover __pdoc__["DataClass._field"] = True __pdoc__["DataClass._fields"] = True __pdoc__["DataClass._frozen"] = True
Base dataclass class.
Includes
DataClassLoadProtocol
.It is recommended that all non-property additions to the class be underscored, to minimise conflict possibility.
Class variables
var _frozen : ClassVar[bool]
-
Set to true if the class is frozen (a subclass of
DataClassFrozen
)
Static methods
def _field(**kwargs: Any) ‑> dataclasses.Field[object]
-
DataClass field creator.
A convenience for calling dataclasses.field()
Returns
Field
def _fields() ‑> tuple[dataclasses.Field[object], ...]
-
DataClass fields.
A convenience for calling dataclasses.fields()
Returns
tuple[Field, …]
class DataClassFrozen
-
Expand source code
class DataClassFrozen(DataClass, dataclass_params={"frozen": True}): """Base dataclass class, frozen version."""
Base dataclass class, frozen version.
Ancestors
Inherited members
class DataClassLoadProtocol (*args, **kwargs)
-
Expand source code
class DataClassLoadProtocol(Protocol): """Add loader to DataClass family. For proper resolution of embedded properties of `DataClass` type. Can be validating or not. Validating loaders may or may not support strict and loose modes, ie strict type compliance or implicit conversion if possible. Default loader is `DataClassLoader.load` """ _loader: ClassVar[Callable[[type[Self], Data, bool], Self]] = ( DataClassLoader.load # type: ignore[assignment] ) """Class attribute, actual loader function Parameters: cls (type(DataClass)): Load into this class spec data (dict): strict (bool): Load in strict mode, ie no implicit type conversion, optional """ @classmethod def _load( cls, defaults: Data = {}, strict: bool = False, /, **kwargs: Data ) -> Self: """Load data into this class Classmethod. `defaults` is a convenience mechanism, so you can load either a dict or with kwargs, or both. See `DataClassMeta`. Parameters: defaults (dict): Data to load, overriden by kwargs Returns: Self: Loaded class instance """ data = {**defaults, **kwargs} return cls._loader(cls, data, strict) __pdoc__["DataClassLoadProtocol._load"] = True __pdoc__["DataClassLoadProtocol._loader"] = True
Add loader to DataClass family.
For proper resolution of embedded properties of
DataClass
type. Can be validating or not. Validating loaders may or may not support strict and loose modes, ie strict type compliance or implicit conversion if possible.Default loader is
DataClassLoader.load()
Ancestors
- typing.Protocol
- typing.Generic
Static methods
def _load(defaults: Data = {}, strict: bool = False, /, **kwargs: Data) ‑> Self
-
Load data into this class
Classmethod.
defaults
is a convenience mechanism, so you can load either a dict or with kwargs, or both. SeeDataClassMeta
.Parameters
defaults (dict): Data to load, overriden by kwargs
Returns
Self
- Loaded class instance
def _loader(dc: type[DataClassT], data: Data, strict: bool = False) ‑> ~DataClassT
-
Load data.
Classmethod, suitable for
DataClassLoader.load()
property.Parameters
dc (type): Load into this
DataClass
spec data (dict):Returns
DataClass instance
class DataClassLoader (dataclass: type[DataClassT])
-
Expand source code
class DataClassLoader(Generic[DataClassT]): """Minimal? version of DataClass non-validating recursive loader. Parameters: dataclass (type): data (dict): Usage: ``` DataClassLoader.load(SomeDataClassDerivative, data) ``` or ``` loader = DataClassLoader(SomeDataClassDerivative) data_obj = loader.load_data(data) ``` """ dataclass: type[DataClassT] """`DataClass` derivative, the load into container spec""" @classmethod def load( cls, dc: type[DataClassT], data: Data, strict: bool = False ) -> DataClassT: """Load data. Classmethod, suitable for `DataClassLoadProtocol._loader` property. Parameters: dc (type): Load into this `DataClass` spec data (dict): Returns: DataClass instance """ return cls(dc).load_data(data, strict) def __init__(self, dataclass: type[DataClassT]) -> None: self.dataclass = dataclass def load_data(self, data: Data, strict: bool = False) -> DataClassT: """Load data into instance. Parameters: data (dict): Returns: DataClass instance Raises: ValueError """ if strict is True: raise ValueError("strict mode not supported") def convert(k: str, v: Any) -> Any: if k in self.dataclass.__annotations__: k_type = self.dataclass.__annotations__[k] if DataClassMeta.is_metaclass(k_type) and not isinstance( v, k_type ): return self.load(k_type, v, strict) return v return self.dataclass(**{k: convert(k, v) for k, v in data.items()})
Minimal? version of DataClass non-validating recursive loader.
Parameters
dataclass (type): data (dict):
Usage:
DataClassLoader.load(SomeDataClassDerivative, data)
or
loader = DataClassLoader(SomeDataClassDerivative) data_obj = loader.load_data(data)
Ancestors
- typing.Generic
Class variables
var dataclass : type[~DataClassT]
-
DataClass
derivative, the load into container spec
Static methods
def load(dc: type[DataClassT], data: Data, strict: bool = False) ‑> ~DataClassT
-
Load data.
Classmethod, suitable for
DataClassLoader.load()
property.Parameters
dc (type): Load into this
DataClass
spec data (dict):Returns
DataClass instance
Methods
def load_data(self, data: Data, strict: bool = False) ‑> ~DataClassT
-
Expand source code
def load_data(self, data: Data, strict: bool = False) -> DataClassT: """Load data into instance. Parameters: data (dict): Returns: DataClass instance Raises: ValueError """ if strict is True: raise ValueError("strict mode not supported") def convert(k: str, v: Any) -> Any: if k in self.dataclass.__annotations__: k_type = self.dataclass.__annotations__[k] if DataClassMeta.is_metaclass(k_type) and not isinstance( v, k_type ): return self.load(k_type, v, strict) return v return self.dataclass(**{k: convert(k, v) for k, v in data.items()})
Load data into instance.
Parameters
data (dict):
Returns
DataClass instance
Raises
ValueError
class DataClassMeta (*args, **kwargs)
-
Expand source code
@dataclass_transform() class DataClassMeta(_ProtocolMeta): """DataClass metaclass. Turns class with properties into `dataclass` `dataclass` can only inherit from another dataclass. This is not a problem for vertical inheritance, but may pose a challenge for horizontal (multiple) inheritance, eg. adding protocols. In those cases a non-dataclass mix-in is cloned, and the infected clone is used instead. It mandates kw_only = True, which means that classes can only be instantiated with keyword args. For that reason, we can modify class instantiator footprint to accept both dict and kwargs. Usage: ``` class C(metaclass=DataClassMeta, dataclass_params={}): ... defaults: Data = {} c = C(defaults, a="a", b="b" ...) ``` """ @staticmethod def is_metaclass(cls: type[Any]) -> bool: return issubclass(type(cls), DataClassMeta) @staticmethod def __new__( metacls: type, name: str, bases: tuple[type, ...], defs: dict[str, Any], /, dataclass_params: Data | None = None, **kwargs: Any, ) -> type[DataclassInstance]: if dataclass_params is None: dataclass_params = {} assert "kw_only" not in dataclass_params, "kw_only is not negotiable" dc_params = {} frozen: bool | None = None for b in reversed(bases): if is_dataclass(b): dcp = getattr(b, _PARAMS) dc_params.update({s: getattr(dcp, s) for s in dcp.__slots__}) if dcp.frozen: frozen = True if "frozen" in dataclass_params: assert ( dataclass_params["frozen"] is not None ), "Frozen should be set" frozen = dataclass_params["frozen"] else: if frozen is None: frozen = False dataclass_params["frozen"] = frozen dc_params.update(dataclass_params) dc_params["kw_only"] = True # Frozen and non-frozen (fresh?) dataclasses dont mix. # We need to align them, recursively. def munge_base(cls: type) -> type[DataclassInstance]: if is_dataclass(cls): dcp = getattr(cls, _PARAMS) if dcp.frozen is frozen: return cls flds = [ ( f.name, ( munge_base(t) if DataClassMeta.is_metaclass(t) else t ), f, ) for f, t in [ (f, cast(type[Any], f.type)) for f in fields(cls) ] ] return make_dataclass(cls.__name__, flds, **dc_params) cls_copy = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) return dataclass(cls_copy, **dc_params) def _field(cls, /, **kwargs: Any) -> Field[object]: """DataClass field creator. A convenience for calling dataclasses.field() Returns: Field """ return field(**kwargs) def _fields(cls) -> tuple[Field[object], ...]: """DataClass fields. A convenience for calling dataclasses.fields() Returns: tuple[Field, ...] """ return fields(cls) dc_bases = tuple([munge_base(b) for b in bases]) defs["_frozen"] = frozen defs["_field"] = classmethod(_field) defs["_fields"] = classmethod(_fields) cls = super().__new__(metacls, name, dc_bases, defs, **kwargs) # type: ignore[misc] # Monkey patching for pleasure and profit. dataclasses._get_field = our_get_field # type: ignore[attr-defined] try: return dataclass(cls, **dc_params) except TypeError as e: raise TypeError(*e.args, cls, dc_bases, defs, dc_params) from e finally: dataclasses._get_field = orig_get_field # type: ignore[attr-defined] def __call__(cls, defaults: Data = {}, /, **kwargs) -> DataClass: data = {**defaults, **kwargs} return super().__call__(**data)
DataClass metaclass.
Turns class with properties into
dataclass
dataclass
can only inherit from another dataclass. This is not a problem for vertical inheritance, but may pose a challenge for horizontal (multiple) inheritance, eg. adding protocols. In those cases a non-dataclass mix-in is cloned, and the infected clone is used instead.It mandates kw_only = True, which means that classes can only be instantiated with keyword args. For that reason, we can modify class instantiator footprint to accept both dict and kwargs.
Usage:
class C(metaclass=DataClassMeta, dataclass_params={}): ... defaults: Data = {} c = C(defaults, a="a", b="b" ...)
Ancestors
- typing._ProtocolMeta
- abc.ABCMeta
- builtins.type
Static methods
def is_metaclass(cls: type[Any]) ‑> bool
-
Expand source code
@staticmethod def is_metaclass(cls: type[Any]) -> bool: return issubclass(type(cls), DataClassMeta)