Source code for dataclass_wizard.bases

from abc import ABCMeta, abstractmethod
from typing import Callable, Type, Dict, Optional, ClassVar, Union, TypeVar

from .constants import TAG
from .decorators import cached_class_property
from .enums import DateTimeTo, LetterCase, LetterCasePriority
from .models import Condition
from .type_def import FrozenKeys, EnvFileType
from .v1.enums import KeyAction, KeyCase


# Create a generic variable that can be 'AbstractMeta', or any subclass.
# Full word as `M` is already defined in another module
META_ = TypeVar('META_', bound='AbstractMeta')
# Use `Type` here explicitly, because we will never have an `META_` object.
META = Type[META_]


[docs] class ABCOrAndMeta(ABCMeta): """ Metaclass to add class-level :meth:`__or__` and :meth:`__and__` methods to a base class of type :type:`M`. Ref: - https://stackoverflow.com/q/15008807/10237506 - https://stackoverflow.com/a/57351066/10237506 """ def __or__(cls: META, other: META) -> META: """ Merge two Meta configs. Priority will be given to the source config present in `cls`, e.g. the first operand in the '|' expression. Use case: Merge the Meta configs for two separate dataclasses into a single, unified Meta config. """ src = cls src_dict = src.__dict__ other_dict = other.__dict__ base_dict = {'__slots__': ()} # Set meta attributes here. if src is AbstractMeta or src is AbstractEnvMeta: # Here we can't use `src` because the `bind_to` method isn't # defined on the abstract class. Use `other` instead, which # *will* be a concrete subclass of `AbstractMeta`. src = other # noinspection PyTypeChecker for k in src.fields_to_merge: if k in other_dict: base_dict[k] = other_dict[k] else: # noinspection PyTypeChecker for k in src.fields_to_merge: if k in src_dict: base_dict[k] = src_dict[k] elif k in other_dict: base_dict[k] = other_dict[k] # This mapping won't be updated. Use the src by default. for k in src.__special_attrs__: if k in src_dict: base_dict[k] = src_dict[k] new_cls_name = src.__name__ # Check if the type of the class we want to create is # `JSONWizard.Meta` or a subclass. If so, we want to avoid the # mandatory `__init_subclass__` call that gets invoked when creating # a new class, so use the superclass type instead. if src.__is_inner_meta__: # In a reversed MRO, the inheritance tree looks like this: # |___ object -> AbstractMeta -> BaseJSONWizardMeta -> ... # So here, we want to choose the third-to-last class in the list. # noinspection PyUnresolvedReferences src = src.__mro__[-3] # noinspection PyTypeChecker return type(new_cls_name, (src, ), base_dict) def __and__(cls: META, other: META) -> META: """ Merge the `other` Meta config into the first one, i.e. `cls`. This operation does not create a new class, but instead it modifies the source config `cls` in-place; the source will be the first operand in the '&' expression. Use case: Merge a separate Meta config (for a single dataclass) with the first config. """ other_dict = other.__dict__ # Set meta attributes here. # noinspection PyTypeChecker for k in cls.all_fields: if k in other_dict: setattr(cls, k, other_dict[k]) return cls
[docs] class AbstractMeta(metaclass=ABCOrAndMeta): """ Base class definition for the `JSONWizard.Meta` inner class. """ __slots__ = () # A list of class attributes that are exclusive to the Meta config. # When merging two Meta configs for a class, these are the only # attributes which will *not* be merged. __special_attrs__ = frozenset({ 'recursive', 'json_key_to_field', 'v1_field_to_alias', 'tag', }) # Class attribute which enables us to detect a `JSONWizard.Meta` subclass. __is_inner_meta__ = False # Enable Debug mode for more verbose log output. # # This setting can be a `bool`, `int`, or `str`: # - `True` enables debug mode with default verbosity. # - A `str` or `int` specifies the minimum log level (e.g., 'DEBUG', 10). # # Debug mode provides additional helpful log messages, including: # - Logging unknown JSON keys encountered during `from_dict` or `from_json`. # - Detailed error messages for invalid types during unmarshalling. # # Note: Enabling Debug mode may have a minor performance impact. # # @deprecated and will be removed in V1 - Use `v1_debug` instead. debug_enabled: ClassVar['bool | int | str'] = False # When enabled, a specified Meta config for the main dataclass (i.e. the # class on which `from_dict` and `to_dict` is called) will cascade down # and be merged with the Meta config for each *nested* dataclass; note # that during a merge, priority is given to the Meta config specified on # each class. # # The default behavior is True, so the Meta config (if provided) will # apply in a recursive manner. recursive: ClassVar[bool] = True # True to support cyclic or self-referential dataclasses. For example, # the type of a dataclass field in class `A` refers to `A` itself. # # See https://github.com/rnag/dataclass-wizard/issues/62 for more details. recursive_classes: ClassVar[bool] = False # True to raise an class:`UnknownJSONKey` when an unmapped JSON key is # encountered when `from_dict` or `from_json` is called; an unknown key is # one that does not have a known mapping to a dataclass field. # # The default is to only log a "warning" for such cases, which is visible # when `v1_debug` is true and logging is properly configured. raise_on_unknown_json_key: ClassVar[bool] = False # A customized mapping of JSON keys to dataclass fields, that is used # whenever `from_dict` or `from_json` is called. # # Note: this is in addition to the implicit field transformations, like # "myStr" -> "my_str" # # If the reverse mapping is also desired (i.e. dataclass field to JSON # key), then specify the "__all__" key as a truthy value. If multiple JSON # keys are specified for a dataclass field, only the first one provided is # used in this case. json_key_to_field: ClassVar[Dict[str, str]] = None # How should :class:`time` and :class:`datetime` objects be serialized # when converted to a Python dictionary object or a JSON string. marshal_date_time_as: ClassVar[Union[DateTimeTo, str]] = None # How JSON keys should be transformed to dataclass fields. # # Note that this only applies to keys which are to be set on dataclass # fields; other fields such as the ones for `TypedDict` or `NamedTuple` # sub-classes won't be similarly transformed. key_transform_with_load: ClassVar[Union[LetterCase, str]] = None # How dataclass fields should be transformed to JSON keys. # # Note that this only applies to dataclass fields; other fields such as # the ones for `TypedDict` or `NamedTuple` sub-classes won't be similarly # transformed. key_transform_with_dump: ClassVar[Union[LetterCase, str]] = None # The field name that identifies the tag for a class. # # When set to a value, an :attr:`TAG` field will be populated in the # dictionary object in the dump (serialization) process. When loading # (or de-serializing) a dictionary object, the :attr:`TAG` field will be # used to load the corresponding dataclass, assuming the dataclass field # is properly annotated as a Union type, ex.: # my_data: Union[Data1, Data2, Data3] tag: ClassVar[str] = None # The dictionary key that identifies the tag field for a class. This is # only set when the `tag` field or the `auto_assign_tags` flag is enabled # in the `Meta` config for a dataclass. # # Defaults to '__tag__' if not specified. tag_key: ClassVar[str] = TAG # Auto-assign the class name as a dictionary "tag" key, for any dataclass # fields which are in a `Union` declaration, ex.: # my_data: Union[Data1, Data2, Data3] auto_assign_tags: ClassVar[bool] = False # Determines whether we should we skip / omit fields with default values # (based on the `default` or `default_factory` argument specified for # the :func:`dataclasses.field`) in the serialization process. skip_defaults: ClassVar[bool] = False # Determines the :class:`Condition` to skip / omit dataclass # fields in the serialization process. skip_if: ClassVar[Condition] = None # Determines the condition to skip / omit fields with default values # (based on the `default` or `default_factory` argument specified for # the :func:`dataclasses.field`) in the serialization process. skip_defaults_if: ClassVar[Condition] = None # Enable opt-in to the "experimental" major release `v1` feature. # This feature offers optimized performance for de/serialization. # Defaults to False. v1: ClassVar[bool] = False # Enable Debug mode for more verbose log output. # # This setting can be a `bool`, `int`, or `str`: # - `True` enables debug mode with default verbosity. # - A `str` or `int` specifies the minimum log level (e.g., 'DEBUG', 10). # # Debug mode provides additional helpful log messages, including: # - Logging unknown JSON keys encountered during `from_dict` or `from_json`. # - Detailed error messages for invalid types during unmarshalling. # # Note: Enabling Debug mode may have a minor performance impact. v1_debug: ClassVar['bool | int | str'] = False # Specifies the letter case used to match JSON keys when mapping them # to dataclass fields. # # This setting determines how dataclass fields are transformed to match # the expected case of JSON keys during lookup. It does not affect keys # in `TypedDict` or `NamedTuple` subclasses. # # By default, JSON keys are assumed to be in `snake_case`, and fields # are matched directly without transformation. # # The setting is case-insensitive and supports shorthand assignment, # such as using the string 'C' instead of 'CAMEL'. # # If set to `A` or `AUTO`, all valid key casing transforms are attempted # at runtime, and the result is cached for subsequent lookups. v1_key_case: ClassVar[Union[KeyCase, str]] = None # A custom mapping of dataclass fields to their JSON aliases (keys) used # during deserialization (`from_dict` or `from_json`) and serialization # (`to_dict` or `to_json`). # # This mapping overrides default behavior, including implicit field-to-key # transformations (e.g., "my_field" -> "myField"). # # By default, the reverse mapping (JSON alias to field) is applied during # serialization, unless explicitly overridden. v1_field_to_alias: ClassVar[Dict[str, str]] = None # Defines the action to take when an unknown JSON key is encountered during # `from_dict` or `from_json` calls. An unknown key is one that does not map # to any dataclass field. # # Valid options are: # - `"ignore"` (default): Silently ignore unknown keys. # - `"warn"`: Log a warning for each unknown key. Requires `v1_debug` # to be `True` and properly configured logging. # - `"raise"`: Raise an `UnknownKeyError` for the first unknown key encountered. v1_on_unknown_key: ClassVar[KeyAction] = None # Unsafe: Enables parsing of dataclasses in unions without requiring # the presence of a `tag_key`, i.e., a dictionary key identifying the # tag field in the input. Defaults to False. v1_unsafe_parse_dataclass_in_union: ClassVar[bool] = False # noinspection PyMethodParameters @cached_class_property def all_fields(cls) -> FrozenKeys: """Return a list of all class attributes""" return frozenset(AbstractMeta.__annotations__) # noinspection PyMethodParameters @cached_class_property def fields_to_merge(cls) -> FrozenKeys: """Return a list of class attributes, minus `__special_attrs__`""" return cls.all_fields - cls.__special_attrs__
[docs] @classmethod @abstractmethod def bind_to(cls, dataclass: Type, create=True, is_default=True): """ Initialize hook which applies the Meta config to `dataclass`, which is typically a subclass of :class:`JSONWizard`. :param dataclass: A class which has been decorated by the `@dataclass` decorator; typically this is a sub-class of :class:`JSONWizard`. :param create: When true, a separate loader/dumper will be created for the class. If disabled, this will access the root loader/dumper, so modifying this should affect global settings across all dataclasses that use the JSON load/dump process. :param is_default: When enabled, the Meta will be cached as the default Meta config for the dataclass. Defaults to true. """
[docs] class AbstractEnvMeta(metaclass=ABCOrAndMeta): """ Base class definition for the `EnvWizard.Meta` inner class. """ __slots__ = () # A list of class attributes that are exclusive to the Meta config. # When merging two Meta configs for a class, these are the only # attributes which will *not* be merged. __special_attrs__ = frozenset({ 'debug_enabled', 'env_var_to_field', }) # Class attribute which enables us to detect a `EnvWizard.Meta` subclass. __is_inner_meta__ = False # True to enable Debug mode for additional (more verbose) log output. # # For example, a message is logged with the environment variable that is # mapped to each attribute. # # This also results in more helpful messages during error handling, which # can be useful when debugging the cause when values are an invalid type # (i.e. they don't match the annotation for the field) when unmarshalling # a environ variable values to attributes in an EnvWizard subclass. # # Note there is a minor performance impact when DEBUG mode is enabled. debug_enabled: ClassVar[bool] = False # When enabled, a specified Meta config for the main dataclass (i.e. the # class on which `from_dict` and `to_dict` is called) will cascade down # and be merged with the Meta config for each *nested* dataclass; note # that during a merge, priority is given to the Meta config specified on # each class. # # The default behavior is True, so the Meta config (if provided) will # apply in a recursive manner. recursive: ClassVar[bool] = True # `True` to load environment variables from an `.env` file, or a # list/tuple of dotenv files. # # This can also be set to a path to a custom dotenv file, for example: # `path/to/.env.prod` # # Simply passing in a filename such as `.env.prod` will search the current # directory, as well as any parent folders (working backwards to the root # directory), until it locates the given file. # # If multiple files are passed in, later files in the list/tuple will take # priority over earlier files. # # For example, in below the '.env.last' file takes priority over '.env': # env_file = '.env', '.env.last' env_file: ClassVar[EnvFileType] = None # Prefix for all environment variables. Defaults to `None`. env_prefix: ClassVar[str] = None # secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`. secrets_dir: ClassVar['EnvFileType | Sequence[EnvFileType]'] = None # The nested env values delimiter. Defaults to `None`. # env_nested_delimiter: ClassVar[str] = None # A customized mapping of field in the `EnvWizard` subclass to its # corresponding environment variable to search for. # # Note: this is in addition to the implicit field transformations, like # "myStr" -> "my_str" field_to_env_var: ClassVar[Dict[str, str]] = None # The letter casing priority to use when looking up Env Var Names. # # The default is `SCREAMING_SNAKE_CASE`. key_lookup_with_load: ClassVar[Union[LetterCasePriority, str]] = LetterCasePriority.SCREAMING_SNAKE # How `EnvWizard` fields (variables) should be transformed to JSON keys. # # The default is 'snake_case'. key_transform_with_dump: ClassVar[Union[LetterCase, str]] = LetterCase.SNAKE # Determines whether we should we skip / omit fields with default values # in the serialization process. skip_defaults: ClassVar[bool] = False # Determines the :class:`Condition` to skip / omit dataclass # fields in the serialization process. skip_if: ClassVar[Condition] = None # Determines the condition to skip / omit fields with default values # (based on the `default` or `default_factory` argument specified for # the :func:`dataclasses.field`) in the serialization process. skip_defaults_if: ClassVar[Condition] = None # noinspection PyMethodParameters @cached_class_property def all_fields(cls) -> FrozenKeys: """Return a list of all class attributes""" return frozenset(AbstractEnvMeta.__annotations__) # noinspection PyMethodParameters @cached_class_property def fields_to_merge(cls) -> FrozenKeys: """Return a list of class attributes, minus `__special_attrs__`""" return cls.all_fields - cls.__special_attrs__
[docs] @classmethod @abstractmethod def bind_to(cls, env_class: Type, create=True, is_default=True): """ Initialize hook which applies the Meta config to `env_class`, which is typically a subclass of :class:`EnvWizard`. :param env_class: A sub-class of :class:`EnvWizard`. :param create: When true, a separate loader/dumper will be created for the class. If disabled, this will access the root loader/dumper, so modifying this should affect global settings across all dataclasses that use the JSON load/dump process. :param is_default: When enabled, the Meta will be cached as the default Meta config for the dataclass. Defaults to true. """
[docs] class BaseLoadHook: """ Container class for type hooks. """ __slots__ = () __LOAD_HOOKS__: ClassVar[Dict[Type, Callable]] = None def __init_subclass__(cls): super().__init_subclass__() # (Re)assign the dict object so we have a fresh copy per class cls.__LOAD_HOOKS__ = {}
[docs] @classmethod def register_load_hook(cls, typ: Type, func: Callable): """Registers the hook for a type, on the default loader by default.""" cls.__LOAD_HOOKS__[typ] = func
[docs] @classmethod def get_load_hook(cls, typ: Type) -> Optional[Callable]: """Retrieves the hook for a type, if one exists.""" return cls.__LOAD_HOOKS__.get(typ)
[docs] class BaseDumpHook: """ Container class for type hooks. """ __slots__ = () __DUMP_HOOKS__: ClassVar[Dict[Type, Callable]] = None def __init_subclass__(cls): super().__init_subclass__() # (Re)assign the dict object so we have a fresh copy per class cls.__DUMP_HOOKS__ = {}
[docs] @classmethod def register_dump_hook(cls, typ: Type, func: Callable): """Registers the hook for a type, on the default dumper by default.""" cls.__DUMP_HOOKS__[typ] = func
[docs] @classmethod def get_dump_hook(cls, typ: Type) -> Optional[Callable]: """Retrieves the hook for a type, if one exists.""" return cls.__DUMP_HOOKS__.get(typ)