Source code for dataclass_wizard.bases_meta
"""
Ideally should be in the `bases` module, however we'll run into a Circular
Import scenario if we move it there, since the `loaders` and `dumpers` modules
both import directly from `bases`.
"""
from datetime import datetime, date
from typing import Type, Optional, Dict, Union
from .abstractions import AbstractJSONWizard
from .bases import AbstractMeta, M
from .class_helper import (
_META_INITIALIZER, _META,
get_outer_class_name, get_class_name, create_new_class,
json_field_to_dataclass_field, dataclass_field_to_json_field
)
from .decorators import try_with_load
from .dumpers import get_dumper
from .enums import LetterCase, DateTimeTo
from .errors import ParseError
from .loaders import get_loader
from .log import LOG
from .type_def import E
from .utils.type_conv import date_to_timestamp, as_enum
# global flag to determine if debug mode was ever enabled
_debug_was_enabled = False
[docs]
class BaseJSONWizardMeta(AbstractMeta):
"""
Superclass definition for the `JSONWizard.Meta` inner class.
See the implementation of the :class:`AbstractMeta` class for the
available config that can be set, as well as for descriptions on any
implemented methods.
"""
__slots__ = ()
@classmethod
def _init_subclass(cls):
"""
Hook that should ideally be run whenever the `Meta` class is
sub-classed.
"""
outer_cls_name = get_outer_class_name(cls, raise_=False)
# We can retrieve the outer class name using `__qualname__`, but it's
# not easy to find the class definition itself. The simplest way seems
# to be to create a new callable (essentially a class method for the
# outer class) which will later be called by the base enclosing class.
#
# Note that this relies on the observation that the
# `__init_subclass__` method of any inner classes are run before the
# one for the outer class.
if outer_cls_name is not None:
_META_INITIALIZER[outer_cls_name] = cls.bind_to
else:
# The `Meta` class is defined as an outer class. Emit a warning
# here, just so we can ensure awareness of this special case.
LOG.warning('The %r class is not declared as an Inner Class, so '
'these are global settings that will apply to all '
'JSONSerializable sub-classes.', get_class_name(cls))
# Copy over global defaults to the :class:`AbstractMeta`
for attr in AbstractMeta.fields_to_merge:
setattr(AbstractMeta, attr, getattr(cls, attr, None))
if cls.json_key_to_field:
AbstractMeta.json_key_to_field = cls.json_key_to_field
# Create a new class of `Type[W]`, and then pass `create=False` so
# that we don't create new loader / dumper for the class.
new_cls = create_new_class(cls, (AbstractJSONWizard, ))
cls.bind_to(new_cls, create=False)
[docs]
@classmethod
def bind_to(cls, dataclass: Type, create=True, is_default=True):
cls_loader = get_loader(dataclass, create=create)
cls_dumper = get_dumper(dataclass, create=create)
if cls.debug_enabled:
global _debug_was_enabled
if not _debug_was_enabled:
_debug_was_enabled = True
LOG.setLevel('DEBUG')
LOG.info('DEBUG Mode is enabled')
# Decorate all hooks so they format more helpful messages
# on error.
load_hooks = cls_loader.__LOAD_HOOKS__
for typ in load_hooks:
load_hooks[typ] = try_with_load(load_hooks[typ])
if cls.json_key_to_field:
add_for_both = cls.json_key_to_field.pop('__all__', None)
json_field_to_dataclass_field(dataclass).update(
cls.json_key_to_field
)
if add_for_both:
dataclass_to_json_field = dataclass_field_to_json_field(
dataclass)
# We unfortunately can't use a dict comprehension approach, as
# we don't know if there are multiple JSON keys mapped to a
# single dataclass field. So to be safe, we should only set
# the first JSON key mapped to each dataclass field.
for json_key, field in cls.json_key_to_field.items():
if field not in dataclass_to_json_field:
dataclass_to_json_field[field] = json_key
if cls.marshal_date_time_as:
enum_val = cls._as_enum_safe('marshal_date_time_as', DateTimeTo)
if enum_val is DateTimeTo.TIMESTAMP:
# Update dump hooks for the `datetime` and `date` types
cls_dumper.dump_with_datetime = lambda o, *_: round(o.timestamp())
cls_dumper.dump_with_date = lambda o, *_: date_to_timestamp(o)
cls_dumper.register_dump_hook(
datetime, cls_dumper.dump_with_datetime)
cls_dumper.register_dump_hook(
date, cls_dumper.dump_with_date)
elif enum_val is DateTimeTo.ISO_FORMAT:
# noop; the default dump hook for `datetime` and `date`
# already serializes using this approach.
pass
if cls.key_transform_with_load:
cls_loader.transform_json_field = cls._as_enum_safe(
'key_transform_with_load', LetterCase)
if cls.key_transform_with_dump:
cls_dumper.transform_dataclass_field = cls._as_enum_safe(
'key_transform_with_dump', LetterCase)
# Finally, if needed, save the meta config for the outer class. This
# will allow us to access this config as part of the JSON load/dump
# process if needed.
if is_default:
# Check if the dataclass already has a Meta config; if so, we need to
# copy over special attributes so they don't get overwritten.
if dataclass in _META:
_META[dataclass] &= cls
else:
_META[dataclass] = cls
@classmethod
def _as_enum_safe(cls, name: str, base_type: Type[E]) -> Optional[E]:
"""
Attempt to return the value for class attribute :attr:`attr_name` as
a :type:`base_type`.
:raises ParseError: If we are unable to convert the value of the class
attribute to an Enum of type `base_type`.
"""
try:
return as_enum(getattr(cls, name), base_type)
except ParseError as e:
# We run into a parsing error while loading the enum; Add
# additional info on the Exception object before re-raising it
e.class_name = get_class_name(cls)
e.field_name = name
raise
# noinspection PyPep8Naming
[docs]
def LoadMeta(*, debug_enabled: bool = False,
recursive: bool = True,
raise_on_unknown_json_key: bool = False,
json_key_to_field: Dict[str, str] = None,
key_transform: Union[LetterCase, str] = None,
tag: str = None) -> M:
"""
Helper function to setup the ``Meta`` Config for the JSON load
(de-serialization) process, which is intended for use alongside the
``fromdict`` helper function.
For descriptions on what each of these params does, refer to the `Docs`_
below, or check out the :class:`AbstractMeta` definition (I want to avoid
duplicating the descriptions for params here).
Examples::
>>> LoadMeta(key_transform='CAMEL').bind_to(MyClass)
>>> fromdict(MyClass, {"myStr": "value"})
.. _Docs: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/meta.html
"""
# Set meta attributes here.
base_dict = {
'__slots__': (),
'raise_on_unknown_json_key': raise_on_unknown_json_key,
'key_transform_with_load': key_transform,
'json_key_to_field': json_key_to_field,
'debug_enabled': debug_enabled,
'recursive': recursive,
'tag': tag,
}
# Create a new subclass of :class:`AbstractMeta`
# noinspection PyTypeChecker
return type('Meta', (BaseJSONWizardMeta, ), base_dict)
# noinspection PyPep8Naming
[docs]
def DumpMeta(*, debug_enabled: bool = False,
recursive: bool = True,
marshal_date_time_as: Union[DateTimeTo, str] = None,
key_transform: Union[LetterCase, str] = None,
tag: str = None,
skip_defaults: bool = False) -> M:
"""
Helper function to setup the ``Meta`` Config for the JSON dump
(serialization) process, which is intended for use alongside the
``asdict`` helper function.
For descriptions on what each of these params does, refer to the `Docs`_
below, or check out the :class:`AbstractMeta` definition (I want to avoid
duplicating the descriptions for params here).
Examples::
>>> DumpMeta(key_transform='CAMEL').bind_to(MyClass)
>>> asdict(MyClass, {"myStr": "value"})
.. _Docs: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/meta.html
"""
# Set meta attributes here.
base_dict = {
'__slots__': (),
'marshal_date_time_as': marshal_date_time_as,
'key_transform_with_dump': key_transform,
'skip_defaults': skip_defaults,
'debug_enabled': debug_enabled,
'recursive': recursive,
'tag': tag,
}
# Create a new subclass of :class:`AbstractMeta`
# noinspection PyTypeChecker
return type('Meta', (BaseJSONWizardMeta, ), base_dict)