Skip to content

Commit

Permalink
minor refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
rnag committed Jan 18, 2025
1 parent 7a202bd commit 7c92e8e
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 62 deletions.
3 changes: 2 additions & 1 deletion dataclass_wizard/v1/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class KeyCase(Enum):
Defines transformations for string keys, commonly used for mapping JSON keys to dataclass fields.
Key transformations:
- `CAMEL`: Converts snake_case to camelCase.
Example: `my_field_name` -> `myFieldName`
- `PASCAL`: Converts snake_case to PascalCase (UpperCamelCase).
Expand All @@ -42,7 +43,7 @@ class KeyCase(Enum):
Example: `My-Field-Name` -> `my_field_name` (cached for future lookups)
By default, no transformation is applied:
Example: `MY_FIELD_NAME` -> `MY_FIELD_NAME`
* Example: `MY_FIELD_NAME` -> `MY_FIELD_NAME`
"""
# Key casing options
CAMEL = C = FuncWrapper(to_camel_case) # Convert to `camelCase`
Expand Down
108 changes: 56 additions & 52 deletions dataclass_wizard/v1/loaders.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,61 @@
from __future__ import annotations

import collections.abc as abc
import dataclasses

from base64 import b64decode
from collections import defaultdict, deque
from dataclasses import is_dataclass, MISSING, Field
from datetime import datetime, time, date, timedelta
from dataclasses import is_dataclass, Field, MISSING
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from enum import Enum
from pathlib import Path
# noinspection PyUnresolvedReferences,PyProtectedMember
from typing import (
Any, Type, Dict, List, Tuple, Iterable, Sequence, Union,
NamedTupleMeta, SupportsFloat, AnyStr, Text, Callable,
Optional, Literal, Annotated, NamedTuple, cast,
)
from typing import Any, Callable, Literal, NamedTuple, cast
from uuid import UUID

from .decorators import (setup_recursive_safe_function,
setup_recursive_safe_function_for_generic,
process_patterned_date_time)
from .decorators import (process_patterned_date_time,
setup_recursive_safe_function,
setup_recursive_safe_function_for_generic)
from .enums import KeyAction, KeyCase
from .models import Extras, TypeInfo, PatternBase
from .models import Extras, PatternBase, TypeInfo
from ..abstractions import AbstractLoaderGenerator
from ..bases import BaseLoadHook, AbstractMeta, META
from ..class_helper import (
v1_dataclass_field_to_alias, CLASS_TO_LOAD_FUNC, dataclass_fields, get_meta, is_subclass_safe,
DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD,
dataclass_init_fields, dataclass_field_to_default, create_meta, dataclass_init_field_names,
)
from ..bases import AbstractMeta, BaseLoadHook, META
from ..class_helper import (create_meta,
dataclass_fields,
dataclass_field_to_default,
dataclass_init_fields,
dataclass_init_field_names,
get_meta,
is_subclass_safe,
v1_dataclass_field_to_alias,
CLASS_TO_LOAD_FUNC,
DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD)
from ..constants import CATCH_ALL, TAG, PY311_OR_ABOVE, PACKAGE_NAME
from ..errors import (ParseError, MissingFields, UnknownKeysError,
MissingData, JSONWizardError)
from ..loader_selection import get_loader, fromdict
from ..errors import (JSONWizardError,
MissingData,
MissingFields,
ParseError,
UnknownKeysError)
from ..loader_selection import fromdict, get_loader
from ..log import LOG
from ..type_def import (
DefFactory, NoneType, JSONObject,
PyLiteralString,
T,
)
from ..type_def import DefFactory, JSONObject, NoneType, PyLiteralString, T
# noinspection PyProtectedMember
from ..utils.dataclass_compat import _set_new_attribute
from ..utils.function_builder import FunctionBuilder
from ..utils.object_path import v1_safe_get
from ..utils.string_conv import possible_json_keys
from ..utils.type_conv import (
as_datetime_v1, as_date_v1, as_time_v1,
as_int_v1, as_timedelta, TRUTHY_VALUES,
)
from ..utils.typing_compat import (
is_typed_dict, get_args, is_annotated,
eval_forward_ref_if_needed, get_origin_v2, is_union,
get_keys_for_typed_dict, is_typed_dict_type_qualifier,
as_datetime_v1, as_date_v1, as_int_v1,
as_time_v1, as_timedelta, TRUTHY_VALUES,
)
from ..utils.typing_compat import (eval_forward_ref_if_needed,
get_args,
get_keys_for_typed_dict,
get_origin_v2,
is_annotated,
is_typed_dict,
is_typed_dict_type_qualifier,
is_union)


# Atomic immutable types which don't require any recursive handling and for which deepcopy
Expand Down Expand Up @@ -121,15 +125,15 @@ def load_to_int(tp: TypeInfo, extras: Extras):
"""
Generate code to load a value into an integer field.
Current logic to parse (an annotated) `int` returns:
- `v` --> `v` is an ``int` or similarly annotated type.
- `int(v)` --> `v` is a `str` value of either a decimal
integer (e.g. '123') or a non-fractional
float value (e.g. `42.0`).
- `as_int(v)` --> `v` is a non-fractional `float`, or in case
of "less common" types / scenarios. Note that
empty strings and `None` (e.g. null values)
are not supported.
Current logic to parse (an annotated) ``int`` returns:
- ``v`` --> ``v`` is an ``int`` or similarly annotated type.
- ``int(v)`` --> ``v`` is a ``str`` value of either a decimal
integer (e.g. ``'123'``) or a non-fractional
float value (e.g. ``42.0``).
- ``as_int(v)`` --> ``v`` is a non-fractional ``float``, or in case
of "less common" types / scenarios. Note that
empty strings and ``None`` (e.g. null values)
are not supported.
"""
tn = tp.type_name(extras)
Expand Down Expand Up @@ -359,7 +363,7 @@ def load_to_dict(cls, tp: TypeInfo, extras: Extras):
@classmethod
def load_to_defaultdict(cls, tp: TypeInfo, extras: Extras):
v, k_next, v_next, i_next = tp.v_and_next_k_v()
default_factory: 'DefFactory | None'
default_factory: DefFactory | None

try:
kt, vt = tp.args
Expand All @@ -384,11 +388,11 @@ def load_to_typed_dict(cls, tp: TypeInfo, extras: Extras):

result_list = []
# TODO set __annotations__?
annotations = tp.origin.__annotations__
td_annotations = tp.origin.__annotations__

# Set required keys for the `TypedDict`
for k in req_keys:
field_tp = annotations[k]
field_tp = td_annotations[k]
field_name = repr(k)
string = cls.get_string_for_annotation(
tp.replace(origin=field_tp,
Expand All @@ -403,7 +407,7 @@ def load_to_typed_dict(cls, tp: TypeInfo, extras: Extras):

# Set optional keys for the `TypedDict` (if they exist)
for k in opt_keys:
field_tp = annotations[k]
field_tp = td_annotations[k]
field_name = repr(k)
string = cls.get_string_for_annotation(
tp.replace(origin=field_tp, i=2, index=None), extras)
Expand Down Expand Up @@ -651,7 +655,7 @@ def load_to_time(tp: TypeInfo, extras: Extras):

@staticmethod
def _load_to_date(tp: TypeInfo, extras: Extras,
cls: 'Union[type[date], type[datetime]]'):
cls: type[date] | type[datetime]):
o = tp.v()
tn = tp.type_name(extras, bound=cls)
tp_date_or_datetime = cast('type[date]', tp.origin)
Expand Down Expand Up @@ -892,7 +896,7 @@ def setup_default_loader(cls=LoadMixin):

def check_and_raise_missing_fields(
_locals, o, cls,
fields: 'Union[tuple[Field, ...], None]'):
fields: tuple[Field, ...] | None):

if fields is None: # named tuple
nt_tp = cast(NamedTuple, cls)
Expand Down Expand Up @@ -932,10 +936,10 @@ def check_and_raise_missing_fields(

def load_func_for_dataclass(
cls: type,
extras: 'Extras | None' = None,
extras: Extras | None = None,
loader_cls=LoadMixin,
base_meta_cls: type = AbstractMeta,
) -> Optional[Callable[[JSONObject], T]]:
) -> Callable[[JSONObject], T] | None:

# Tuple describing the fields of this dataclass.
fields = dataclass_fields(cls)
Expand Down Expand Up @@ -1013,7 +1017,7 @@ def load_func_for_dataclass(
extras['cls'] = cls
extras['cls_name'] = cls_name

key_case: 'V1LetterCase | None' = cls_loader.transform_json_field
key_case: KeyCase | None = cls_loader.transform_json_field
auto_key_case = key_case is KeyCase.AUTO

field_to_aliases = v1_dataclass_field_to_alias(cls)
Expand All @@ -1036,7 +1040,7 @@ def load_func_for_dataclass(

on_unknown_key = meta.v1_on_unknown_key

catch_all_field: 'str | None' = field_to_aliases.pop(CATCH_ALL, None)
catch_all_field: str | None = field_to_aliases.pop(CATCH_ALL, None)
has_catch_all = catch_all_field is not None

if has_catch_all:
Expand Down
105 changes: 105 additions & 0 deletions dataclass_wizard/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,108 @@ def __init__(self,
self.dump_alias = dump_alias
self.skip = skip
self.path = path


Alias.__doc__ = """
Maps one or more JSON key names to a dataclass field.
This function acts as an alias for ``dataclasses.field(...)``, with additional
support for associating a field with one or more JSON keys. It can be used
to customize serialization and deserialization behavior, including handling
keys with varying cases or alternative names.
The mapping is case-sensitive, meaning that JSON keys must match exactly
(e.g., "myField" will not match "myfield"). If multiple keys are provided,
the first one is used as the default for serialization.
Args:
all (str): One or more JSON key names to associate with the dataclass field.
load (str | Sequence[str] | None): Key(s) to use for deserialization.
Defaults to ``all`` if not specified.
dump (str | None): Key to use for serialization. Defaults to the first key in ``all``.
skip (bool): If True, the field is excluded during serialization. Defaults to False.
default (Any): Default value for the field. Cannot be used with ``default_factory``.
default_factory (Callable[[], Any]): A callable to generate the default value.
Cannot be used with `default`.
init (bool): Whether the field is included in the generated ``__init__`` method. Defaults to True.
repr (bool): Whether the field appears in the ``__repr__`` output. Defaults to True.
hash (bool): Whether the field is included in the ``__hash__`` method. Defaults to None.
compare (bool): Whether the field is included in comparison methods. Defaults to True.
metadata (dict): Additional metadata for the field. Defaults to None.
kw_only (bool): If True, the field is keyword-only. Defaults to False.
Returns:
Field: A dataclass field with additional mappings to one or more JSON keys.
Examples:
**Example 1**: Mapping multiple key names to a field.
>>> from dataclasses import dataclass
>>> from dataclass_wizard.v1 import Alias
>>> @dataclass
>>> class Example:
>>> my_field: str = Alias('key1', 'key2', default="default_value")
**Example 2**: Skipping a field during serialization.
>>> from dataclass_wizard.v1 import Alias
>>> my_field: str = Alias('key', skip=True)
"""

AliasPath.__doc__ = """
Creates a dataclass field mapped to one or more nested JSON paths.
This function acts as an alias for ``dataclasses.field(...)``, with additional
functionality to associate a field with one or more nested JSON paths,
including complex or deeply nested structures.
The mapping is case-sensitive, meaning that JSON keys must match exactly
(e.g., "myField" will not match "myfield"). Nested paths can include dot
notations or bracketed syntax for accessing specific indices or keys.
Args:
all (PathType | str): One or more nested JSON paths to associate with
the dataclass field (e.g., ``a.b.c`` or ``a["nested"]["key"]``).
load (PathType | str | None): Path(s) to use for deserialization.
Defaults to ``all`` if not specified.
dump (PathType | str | None): Path(s) to use for serialization.
Defaults to ``all`` if not specified.
skip (bool): If True, the field is excluded during serialization. Defaults to False.
default (Any): Default value for the field. Cannot be used with ``default_factory``.
default_factory (Callable[[], Any]): A callable to generate the default value.
Cannot be used with ``default``.
init (bool): Whether the field is included in the generated ``__init__`` method. Defaults to True.
repr (bool): Whether the field appears in the ``__repr__`` output. Defaults to True.
hash (bool): Whether the field is included in the ``__hash__`` method. Defaults to None.
compare (bool): Whether the field is included in comparison methods. Defaults to True.
metadata (dict): Additional metadata for the field. Defaults to None.
kw_only (bool): If True, the field is keyword-only. Defaults to False.
Returns:
Field: A dataclass field with additional mapping to one or more nested JSON paths.
Examples:
**Example 1** -- Mapping multiple nested paths to a field:
>>> from dataclasses import dataclass
>>> from dataclass_wizard.v1 import AliasPath
>>> @dataclass
>>> class Example:
>>> my_str: str = AliasPath('a.b.c.1', 'x.y["-1"].z', default="default_value")
>>> # Maps nested paths ('a', 'b', 'c', 1) and ('x', 'y', '-1', 'z')
>>> # to the `my_str` attribute. '-1' is treated as a literal string key,
>>> # not an index, for the second path.
**Example 2** -- Using ``Annotated``:
>>> from typing import Annotated
>>> my_str: Annotated[str, AliasPath('my."7".nested.path.-321')]
"""

Field.__doc__ = """
Alias to a :class:`dataclasses.Field`, but one which also represents a
mapping of one or more JSON key names to a dataclass field.
See the docs on the :func:`Alias` and :func:`AliasPath` for more info.
"""
2 changes: 1 addition & 1 deletion dataclass_wizard/v1/models.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class Field(_Field):
Alias to a :class:`dataclasses.Field`, but one which also represents a
mapping of one or more JSON key names to a dataclass field.
See the docs on the :func:`json_field` function for more info.
See the docs on the :func:`Alias` and :func:`AliasPath` for more info.
"""
__slots__ = ('load_alias',
'dump_alias',
Expand Down
16 changes: 8 additions & 8 deletions docs/dataclass_wizard.environ.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ dataclass\_wizard.environ.dumpers module
:undoc-members:
:show-inheritance:

dataclass\_wizard.environ.env\_wizard module
--------------------------------------------

.. automodule:: dataclass_wizard.environ.env_wizard
:members:
:undoc-members:
:show-inheritance:

dataclass\_wizard.environ.loaders module
----------------------------------------

Expand All @@ -36,6 +28,14 @@ dataclass\_wizard.environ.lookups module
:undoc-members:
:show-inheritance:

dataclass\_wizard.environ.wizard module
---------------------------------------

.. automodule:: dataclass_wizard.environ.wizard
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

Expand Down
Loading

0 comments on commit 7c92e8e

Please sign in to comment.