Skip to content

Commit

Permalink
Merge branch 'main' into fix/literal-parser-can-read-from-file/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
rnag authored Nov 4, 2024
2 parents b05e1da + e7a5601 commit 546eda2
Show file tree
Hide file tree
Showing 18 changed files with 304 additions and 83 deletions.
36 changes: 36 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@
History
=======

0.25.0 (2024-11-03)
-------------------

**Features and Improvements**

* Add support for `pathlib.Path`_. Thanks to :user:`assafge` in :pr:`79`.

.. _pathlib.Path: https://docs.python.org/3/library/pathlib.html#basic-use

0.24.1 (2024-11-03)
-------------------

* Resolve ``mypy`` typing issues. Thanks to :user:`AdiNar` in :pr:`64`.

0.24.0 (2024-11-03)
-------------------

**Features and Improvements**

* :pr:`125`: add support for ``typing.Required``, ``NotRequired``

**Bugfixes**

* Fixed by :pr:`125`: Annotating ``TypedDict`` field with one of ``Required`` or ``NotRequired`` wrappers introduced in Python 3.11, no longer raises a ``TypeError``
-- credits to :user:`claui`.

0.23.0 (2024-09-18)
-------------------

* :pr:`94`: Allows the ability to define keys in JSON/dataclass
that do not undergo transformation -- credits to :user:`cquick01`.

* ``LetterCase.NONE`` - Performs no conversion on strings.

* ex: `MY_FIELD_NAME` -> `MY_FIELD_NAME`

0.22.3 (2024-01-29)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion dataclass_wizard/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'with initial values. Construct a dataclass schema with ' \
'JSON input.'
__url__ = 'https://github.com/rnag/dataclass-wizard'
__version__ = '0.22.3'
__version__ = '0.25.0'
__author__ = 'Ritvik Nag'
__author_email__ = '[email protected]'
__license__ = 'Apache 2.0'
Expand Down
10 changes: 5 additions & 5 deletions dataclass_wizard/abstractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from decimal import Decimal
from typing import (
Any, Type, TypeVar, Union, List, Tuple, Dict, SupportsFloat, AnyStr,
Text, Sequence, Iterable
Text, Sequence, Iterable, Generic
)

from .models import Extras
from .type_def import (
DefFactory, FrozenKeys, ListOfJSONObject, JSONObject, Encoder,
M, N, T, NT, E, U, DD, LSQ
M, N, T, TT, NT, E, U, DD, LSQ
)


Expand Down Expand Up @@ -88,7 +88,7 @@ def list_to_json(cls: Type[W],


@dataclass
class AbstractParser(ABC):
class AbstractParser(ABC, Generic[T, TT]):
"""
Abstract parsers, which will ideally act as dispatchers to route objects
to the `load` or `dump` hook methods responsible for transforming the
Expand Down Expand Up @@ -119,7 +119,7 @@ class AbstractParser(ABC):
# This is usually the underlying base type of the annotation (for example,
# for `List[str]` it will be `list`), though in some cases this will be
# the annotation itself.
base_type: Type[T]
base_type: T

def __contains__(self, item) -> bool:
"""
Expand All @@ -130,7 +130,7 @@ def __contains__(self, item) -> bool:
return type(item) is self.base_type

@abstractmethod
def __call__(self, o: Any):
def __call__(self, o: Any) -> TT:
"""
Parse object `o`
"""
Expand Down
12 changes: 6 additions & 6 deletions dataclass_wizard/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@


# Create a generic variable that can be 'AbstractMeta', or any subclass.
M = TypeVar('M', bound='AbstractMeta')
# Use `Type` here explicitly, because we will never have an `M` object.
M = Type[M]
META = M # alias, since `M` is already defined in another module
# 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_]


class ABCOrAndMeta(ABCMeta):
Expand All @@ -24,7 +24,7 @@ class ABCOrAndMeta(ABCMeta):
- https://stackoverflow.com/a/57351066/10237506
"""

def __or__(cls: M, other: M) -> M:
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.
Expand Down Expand Up @@ -73,7 +73,7 @@ def __or__(cls: M, other: M) -> M:
# noinspection PyTypeChecker
return type(new_cls_name, (src, ), base_dict)

def __and__(cls: M, other: M) -> M:
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
Expand Down
6 changes: 3 additions & 3 deletions dataclass_wizard/bases_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Type, Optional, Dict, Union

from .abstractions import AbstractJSONWizard
from .bases import AbstractMeta, M
from .bases import AbstractMeta, META
from .class_helper import (
_META_INITIALIZER, _META,
get_outer_class_name, get_class_name, create_new_class,
Expand Down Expand Up @@ -176,7 +176,7 @@ def LoadMeta(*, debug_enabled: bool = False,
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:
tag: str = None) -> META:
"""
Helper function to setup the ``Meta`` Config for the JSON load
(de-serialization) process, which is intended for use alongside the
Expand Down Expand Up @@ -216,7 +216,7 @@ def DumpMeta(*, debug_enabled: bool = False,
marshal_date_time_as: Union[DateTimeTo, str] = None,
key_transform: Union[LetterCase, str] = None,
tag: str = None,
skip_defaults: bool = False) -> M:
skip_defaults: bool = False) -> META:
"""
Helper function to setup the ``Meta`` Config for the JSON dump
(serialization) process, which is intended for use alongside the
Expand Down
16 changes: 8 additions & 8 deletions dataclass_wizard/class_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Dict, Tuple, Type, Union, Callable, Optional, Any

from .abstractions import W, AbstractLoader, AbstractDumper, AbstractParser
from .bases import M, AbstractMeta
from .bases import AbstractMeta, META
from .models import JSONField, JSON, Extras, _PatternedDT
from .type_def import ExplicitNull, ExplicitNullType, T
from .utils.dict_helper import DictWithLowerStore
Expand All @@ -14,7 +14,7 @@

# A cached mapping of dataclass to the list of fields, as returned by
# `dataclasses.fields()`.
_FIELDS: Dict[Type, Tuple[Field]] = {}
_FIELDS: Dict[Type, Tuple[Field, ...]] = {}

# Mapping of main dataclass to its `load` function.
_CLASS_TO_LOAD_FUNC: Dict[Type, Any] = {}
Expand Down Expand Up @@ -56,7 +56,7 @@

# Mapping of dataclass to its Meta inner class, which will only be set when
# the :class:`JSONSerializable.Meta` is sub-classed.
_META: Dict[Type, M] = {}
_META: Dict[Type, META] = {}


def dataclass_to_loader(cls):
Expand Down Expand Up @@ -111,7 +111,7 @@ def dataclass_field_to_json_field(cls):
def dataclass_field_to_load_parser(
cls_loader: Type[AbstractLoader],
cls: Type,
config: M,
config: META,
save: bool = True) -> 'DictWithLowerStore[str, AbstractParser]':
"""
Returns a mapping of each lower-cased field name to its annotated type.
Expand All @@ -124,7 +124,7 @@ def dataclass_field_to_load_parser(

def _setup_load_config_for_cls(cls_loader: Type[AbstractLoader],
cls: Type,
config: M,
config: META,
save: bool = True
) -> 'DictWithLowerStore[str, AbstractParser]':
"""
Expand Down Expand Up @@ -267,7 +267,7 @@ def call_meta_initializer_if_needed(cls: Type[W]):
_META_INITIALIZER[cls_name](cls)


def get_meta(cls: Type) -> M:
def get_meta(cls: Type) -> META:
"""
Retrieves the Meta config for the :class:`AbstractJSONWizard` subclass.
Expand All @@ -276,7 +276,7 @@ def get_meta(cls: Type) -> M:
return _META.get(cls, AbstractMeta)


def dataclass_fields(cls) -> Tuple[Field]:
def dataclass_fields(cls) -> Tuple[Field, ...]:
"""
Cache the `dataclasses.fields()` call for each class, as overall that
ends up around 5x faster than making a fresh call each time.
Expand All @@ -288,7 +288,7 @@ def dataclass_fields(cls) -> Tuple[Field]:
return _FIELDS[cls]


def dataclass_init_fields(cls) -> Tuple[Field]:
def dataclass_init_fields(cls) -> Tuple[Field, ...]:
"""Get only the dataclass fields that would be passed into the constructor."""
return tuple(f for f in dataclass_fields(cls) if f.init)

Expand Down
3 changes: 3 additions & 0 deletions dataclass_wizard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
# Check if currently running Python 3.10 or higher
PY310_OR_ABOVE = _PY_VERSION >= (3, 10)

# Check if currently running Python 3.11 or higher
PY311_OR_ABOVE = _PY_VERSION >= (3, 11)

# The name of the dictionary object that contains `load` hooks for each
# object type. Also used to check if a class is a :class:`BaseLoadHook`
_LOAD_HOOKS = '__LOAD_HOOKS__'
Expand Down
2 changes: 1 addition & 1 deletion dataclass_wizard/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class LetterCase(Enum):
# Converts strings (generally in camel case) to snake case.
# ex: `myFieldName` -> `my_field_name`
SNAKE = FuncWrapper(to_snake_case)
# Perfoms no conversion on strings.
# Performs no conversion on strings.
# ex: `MY_FIELD_NAME` -> `MY_FIELD_NAME`
NONE = FuncWrapper(lambda s: s)

Expand Down
2 changes: 1 addition & 1 deletion dataclass_wizard/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ParseError(JSONWizardError):

def __init__(self, base_err: Exception,
obj: Any,
ann_type: Union[Type, Iterable],
ann_type: Optional[Union[Type, Iterable]],
_default_class: Optional[type] = None,
_field_name: Optional[str] = None,
_json_object: Any = None,
Expand Down
14 changes: 14 additions & 0 deletions dataclass_wizard/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, time, date, timedelta
from decimal import Decimal
from enum import Enum
from pathlib import Path
from typing import (
Any, Type, Dict, List, Tuple, Iterable, Sequence, Union,
NamedTupleMeta, SupportsFloat, AnyStr, Text, Callable, Optional
Expand All @@ -25,6 +26,7 @@
from .parsers import *
from .type_def import (
ExplicitNull, FrozenKeys, DefFactory, NoneType, JSONObject,
PyRequired, PyNotRequired,
M, N, T, E, U, DD, LSQ, NT
)
from .utils.string_conv import to_snake_case
Expand Down Expand Up @@ -207,6 +209,11 @@ def load_to_decimal(o: N, base_type: Type[Decimal]) -> Decimal:

return base_type(str(o))

@staticmethod
def load_to_path(o: N, base_type: Type[Path]) -> Path:

return base_type(str(o))

@staticmethod
@_alias(as_datetime)
def load_to_datetime(
Expand Down Expand Up @@ -360,6 +367,12 @@ def get_parser_for_annotation(cls, ann_type: Type[T],
cls.get_parser_for_annotation
)

elif base_type in (PyRequired, PyNotRequired):
# Given `Required[T]` or `NotRequired[T]`, we only need `T`
ann_type = get_args(ann_type)[0]
return cls.get_parser_for_annotation(
ann_type, base_cls, extras)

elif issubclass(base_type, defaultdict):
load_hook = hooks[defaultdict]
return DefaultDictParser(
Expand Down Expand Up @@ -479,6 +492,7 @@ def setup_default_loader(cls=LoadMixin):
cls.register_load_hook(defaultdict, cls.load_to_defaultdict)
cls.register_load_hook(dict, cls.load_to_dict)
cls.register_load_hook(Decimal, cls.load_to_decimal)
cls.register_load_hook(Path, cls.load_to_path)
# Dates and times
cls.register_load_hook(datetime, cls.load_to_datetime)
cls.register_load_hook(time, cls.load_to_time)
Expand Down
11 changes: 4 additions & 7 deletions dataclass_wizard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dataclasses import MISSING, Field, _create_fn
from datetime import date, datetime, time
from typing import (cast, Collection, Callable,
Optional, List, Union, Type)
Optional, List, Union, Type, Generic)

from .bases import META
from .constants import PY310_OR_ABOVE
Expand All @@ -15,9 +15,6 @@
# Type for a string or a collection of strings.
_STR_COLLECTION = Union[str, Collection[str]]

# A date, time, datetime sub type, or None.
DT_OR_NONE = Optional[DT]


class Extras(PyTypedDict):
"""
Expand Down Expand Up @@ -205,7 +202,7 @@ class DateTimePattern(datetime, _PatternBase):
__slots__ = ()


class _PatternedDT:
class _PatternedDT(Generic[DT]):
"""
Base class for pattern matching using :meth:`datetime.strptime` when
loading (de-serializing) a string to a date / time / datetime object.
Expand All @@ -216,8 +213,8 @@ class _PatternedDT:
__slots__ = ('cls',
'pattern')

def __init__(self, pattern: str, cls: DT_OR_NONE = None):
self.cls = cls
def __init__(self, pattern: str, cls: Optional[Type[DT]] = None):
self.cls: Optional[Type[DT]] = cls
self.pattern = pattern

def get_transform_func(self) -> Callable[[str], DT]:
Expand Down
Loading

0 comments on commit 546eda2

Please sign in to comment.