Skip to content

Commit

Permalink
Minor changes
Browse files Browse the repository at this point in the history
  • Loading branch information
rnag committed Nov 13, 2024
1 parent 6d86722 commit ee59238
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 107 deletions.
68 changes: 34 additions & 34 deletions dataclass_wizard/dumpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from decimal import Decimal
from enum import Enum
# noinspection PyProtectedMember,PyUnresolvedReferences
from typing import Type, List, Dict, Any, NamedTupleMeta, Optional, Callable
from typing import Type, List, Dict, Any, NamedTupleMeta, Optional, Callable, Collection
from uuid import UUID

from .abstractions import AbstractDumper
Expand All @@ -35,7 +35,8 @@
ExplicitNull, NoneType, JSONObject,
DD, LSQ, E, U, LT, NT, T
)
from .utils.code_builder import CodeBuilder
from .utils.function_builder import FunctionBuilder
from .utils.dataclass_compat import _set_new_attribute
from .utils.string_conv import to_camel_case


Expand Down Expand Up @@ -201,9 +202,11 @@ def get_dumper(cls=None, create=True) -> Type[DumpMixin]:
return set_class_dumper(cls, DumpMixin)


def asdict(obj: T,
*, cls=None, dict_factory=dict,
exclude: List[str] = None, **kwargs) -> JSONObject:
def asdict(o: T,
*, cls=None,
dict_factory=dict,
exclude: 'Collection[str] | None' = None,
**kwargs) -> JSONObject:
# noinspection PyUnresolvedReferences
"""Return the fields of a dataclass instance as a new dictionary mapping
field names to field values.
Expand Down Expand Up @@ -236,14 +239,14 @@ class C:
# if not _is_dataclass_instance(obj):
# raise TypeError("asdict() should be called on dataclass instances")

cls = cls or type(obj)
cls = cls or type(o)

try:
dump = _CLASS_TO_DUMP_FUNC[cls]
except KeyError:
dump = dump_func_for_dataclass(cls)

return dump(obj, dict_factory, exclude, **kwargs)
return dump(o, dict_factory, exclude, **kwargs)


def dump_func_for_dataclass(cls: Type[T],
Expand Down Expand Up @@ -312,38 +315,33 @@ def dump_func_for_dataclass(cls: Type[T],

_locals = {
'config': config,
'field_to_default': field_to_default,
'asdict': _asdict_inner,
'hooks': hooks,
'cls_to_asdict': nested_cls_to_dump_func,
}

# TODO Unsure if dataclasses uses globals()?
_globals = {

'T': T,
'ExplicitNull': ExplicitNull,
'LOG': LOG,
}

# Initialize CodeBuilder
cb = CodeBuilder()
# Initialize FuncBuilder
fn_gen = FunctionBuilder()

# Code for `cls_asdict`
with cb.function('cls_asdict',
['obj:T',
'dict_factory=dict',
"exclude:'list[str]|None'=None",
f'skip_defaults:bool={meta.skip_defaults}'],
return_type='JSONObject'):
with fn_gen.function('cls_asdict',
['obj:T',
'dict_factory=dict',
"exclude:'list[str]|None'=None",
f'skip_defaults:bool={meta.skip_defaults}'],
return_type='JSONObject'):

_pre_as_dict_method = getattr(cls_dumper, '__pre_as_dict__', None)
if _pre_as_dict_method is not None:
_locals['__pre_as_dict__'] = _pre_as_dict_method
cb.add_line('__pre_as_dict__(obj)')
fn_gen.add_line('__pre_as_dict__(obj)')

# Initialize result list to hold field mappings
cb.add_line("result = []")
fn_gen.add_line("result = []")

if field_names:

Expand Down Expand Up @@ -382,28 +380,28 @@ def dump_func_for_dataclass(cls: Type[T],
field_assignments.append(f" result.append(('{json_field}',"
f"asdict(obj.{field},dict_factory,hooks,config,cls_to_asdict)))")

with cb.if_block('exclude is None'):
cb.add_line('='.join(skip_field_assignments) + '=False')
with cb.else_block():
cb.add_line(';'.join(exclude_assignments))
with fn_gen.if_('exclude is None'):
fn_gen.add_line('='.join(skip_field_assignments) + '=False')
with fn_gen.else_():
fn_gen.add_line(';'.join(exclude_assignments))

if skip_default_assignments:
with cb.if_block('skip_defaults'):
cb.add_lines(*skip_default_assignments)
with fn_gen.if_('skip_defaults'):
fn_gen.add_lines(*skip_default_assignments)

cb.add_lines(*field_assignments)
fn_gen.add_lines(*field_assignments)

# Return the final dictionary result
if meta.tag:
cb.add_line("result = dict_factory(result)")
cb.add_line(f"result[{tag_key!r}] = {meta.tag!r}")
fn_gen.add_line("result = dict_factory(result)")
fn_gen.add_line(f"result[{tag_key!r}] = {meta.tag!r}")
# Return the result with the tag added
cb.add_line("return result")
fn_gen.add_line("return result")
else:
cb.add_line("return dict_factory(result)")
fn_gen.add_line("return dict_factory(result)")

# Compile the code into a dynamic string
functions = cb.create_functions(locals=_locals, globals=_globals)
functions = fn_gen.create_functions(locals=_locals, globals=_globals)

cls_asdict = functions['cls_asdict']

Expand All @@ -412,6 +410,8 @@ def dump_func_for_dataclass(cls: Type[T],
# In any case, save the dump function for the class, so we don't need to
# run this logic each time.
if is_main_class:
if hasattr(cls, 'to_dict'):
_set_new_attribute(cls, 'to_dict', asdict_func)
_CLASS_TO_DUMP_FUNC[cls] = asdict_func
else:
nested_cls_to_dump_func[cls] = asdict_func
Expand Down
93 changes: 48 additions & 45 deletions dataclass_wizard/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
PyRequired, PyNotRequired,
M, N, T, E, U, DD, LSQ, NT
)
from .utils.code_builder import CodeBuilder
from .utils.function_builder import FunctionBuilder
# noinspection PyProtectedMember
from .utils.dataclass_compat import _set_new_attribute
from .utils.string_conv import to_snake_case
from .utils.type_conv import (
as_bool, as_str, as_datetime, as_date, as_time, as_int, as_timedelta
Expand Down Expand Up @@ -589,6 +591,7 @@ def load_func_for_dataclass(

# TODO dynamically generate for multiple nested classes at once

# Tuple describing the fields of this dataclass.
cls_fields = dataclass_fields(cls)

# Get the loader for the class, or create a new one as needed.
Expand Down Expand Up @@ -628,41 +631,38 @@ def load_func_for_dataclass(

_locals = {
'cls': cls,
'config': config,
'py_case': cls_loader.transform_json_field,
'field_to_parser': field_to_parser,
'json_to_field': json_to_field,
'ExplicitNull': ExplicitNull,
}

# TODO Unsure if dataclasses uses globals()?
_globals = {
'cls_fields': cls_fields,
'LOG': LOG,
'MissingData': MissingData,
'MissingFields': MissingFields,
'UnknownJSONKey': UnknownJSONKey,
}

# Initialize the CodeBuilder
cb = CodeBuilder()
# Initialize the FuncBuilder
fn_gen = FunctionBuilder()

with cb.function('cls_fromdict', ['o']):
with fn_gen.function('cls_fromdict', ['o']):

# Need to create a separate dictionary to copy over the constructor
# args, as we don't want to mutate the original dictionary object.
cb.add_line('init_kwargs = {}')
fn_gen.add_line('init_kwargs = {}')
# This try-block is here in case the object `o` is None.
with cb.try_block():
with fn_gen.try_():
# Loop over the dictionary object
with cb.for_block('json_key in o'):
with fn_gen.for_('json_key in o'):

with cb.try_block():
with fn_gen.try_():
# Get the resolved dataclass field name
cb.add_line("field_name = json_to_field[json_key]")
fn_gen.add_line("field = json_to_field[json_key]")

with cb.except_block(KeyError):
cb.add_line('# Lookup Field for JSON Key')
with fn_gen.except_(KeyError):
fn_gen.add_line('# Lookup Field for JSON Key')
# Determines the dataclass field which a JSON key should map to.
# Note this logic only runs the initial time, i.e. the first time
# we encounter the key in a JSON object.
Expand All @@ -672,65 +672,66 @@ def load_func_for_dataclass(
# config for the class.

# Short path: an identical-cased field name exists for the JSON key
with cb.if_block('json_key in field_to_parser'):
cb.add_line("field_name = json_to_field[json_key] = json_key")
with fn_gen.if_('json_key in field_to_parser'):
fn_gen.add_line("field = json_to_field[json_key] = json_key")

with cb.else_block():
with fn_gen.else_():
# Transform JSON field name (typically camel-cased) to the
# snake-cased variant which is convention in Python.
cb.add_line("py_field = py_case(json_key)")
fn_gen.add_line("py_field = py_case(json_key)")

with cb.try_block():
with fn_gen.try_():
# Do a case-insensitive lookup of the dataclass field, and
# cache the mapping, so we have it for next time
cb.add_line("field_name "
"= json_to_field[json_key] "
"= field_to_parser.get_key(py_field)")
fn_gen.add_line("field "
"= json_to_field[json_key] "
"= field_to_parser.get_key(py_field)")

with cb.except_block(KeyError):
with fn_gen.except_(KeyError):
# Else, we see an unknown field in the dictionary object
cb.add_line("field_name = json_to_field[json_key] = ExplicitNull")
cb.add_line("LOG.warning('JSON field %r missing from dataclass schema, "
"class=%r, parsed field=%r',json_key,cls,py_field)")
fn_gen.add_line("field = json_to_field[json_key] = ExplicitNull")
fn_gen.add_line("LOG.warning('JSON field %r missing from dataclass schema, "
"class=%r, parsed field=%r',json_key,cls,py_field)")
# Raise an error here (if needed)
if meta.raise_on_unknown_json_key:
cb.add_line("raise UnknownJSONKey(json_key, o, cls, cls_fields) from None")
_globals['UnknownJSONKey'] = UnknownJSONKey
fn_gen.add_line("raise UnknownJSONKey(json_key, o, cls, cls_fields) from None")

# Exclude JSON keys that don't map to any fields.
with cb.if_block('field_name is not ExplicitNull'):
with fn_gen.if_('field is not ExplicitNull'):

with cb.try_block():
with fn_gen.try_():
# Note: pass the original cased field to the class constructor;
# don't use the lowercase result from `py_case`
cb.add_line("init_kwargs[field_name] = field_to_parser[field_name](o[json_key])")
fn_gen.add_line("init_kwargs[field] = field_to_parser[field](o[json_key])")

with cb.except_block(ParseError, 'e'):
with fn_gen.except_(ParseError, 'e'):
# We run into a parsing error while loading the field value;
# Add additional info on the Exception object before re-raising it.
cb.add_line("e.class_name, e.field_name, e.json_object = cls, field_name, o")
cb.add_line("raise")
fn_gen.add_line("e.class_name, e.field_name, e.json_object = cls, field, o")
fn_gen.add_line("raise")

with cb.except_block(TypeError):
with fn_gen.except_(TypeError):
# If the object `o` is None, then raise an error with the relevant info included.
with cb.if_block('o is None'):
cb.add_line("raise MissingData(cls) from None")
with fn_gen.if_('o is None'):
fn_gen.add_line("raise MissingData(cls) from None")
# Check if the object `o` is some other type than what we expect -
# for example, we could be passed in a `list` type instead.
with cb.if_block('not isinstance(o, dict)'):
cb.add_line("e = TypeError('Incorrect type for field')")
cb.add_line("raise ParseError(e, o, dict, cls, desired_type=dict) from None")
with fn_gen.if_('not isinstance(o, dict)'):
fn_gen.add_line("e = TypeError('Incorrect type for field')")
fn_gen.add_line("raise ParseError(e, o, dict, cls, desired_type=dict) from None")

# Else, just re-raise the error.
cb.add_line("raise")
fn_gen.add_line("raise")

# Now pass the arguments to the constructor method, and return the new dataclass instance.
# If there are any missing fields, we raise them here.
with cb.try_block():
cb.add_line("return cls(**init_kwargs)")
with cb.except_block(TypeError, 'e'):
cb.add_line("raise MissingFields(e, o, cls, init_kwargs, cls_fields) from None")
with fn_gen.try_():
fn_gen.add_line("return cls(**init_kwargs)")
with fn_gen.except_(TypeError, 'e'):
fn_gen.add_line("raise MissingFields(e, o, cls, init_kwargs, cls_fields) from None")

functions = cb.create_functions(
functions = fn_gen.create_functions(
locals=_locals, globals=_globals
)

Expand All @@ -739,6 +740,8 @@ def load_func_for_dataclass(
# Save the load function for the main dataclass, so we don't need to run
# this logic each time.
if is_main_class:
if hasattr(cls, 'from_dict'):
_set_new_attribute(cls, 'from_dict', cls_fromdict)
_CLASS_TO_LOAD_FUNC[cls] = cls_fromdict

return cls_fromdict
Loading

0 comments on commit ee59238

Please sign in to comment.