Skip to content

Commit

Permalink
add support for CatchAll
Browse files Browse the repository at this point in the history
  • Loading branch information
rnag committed Dec 11, 2024
1 parent 2c6868d commit dde2d83
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 155 deletions.
11 changes: 8 additions & 3 deletions dataclass_wizard/class_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,16 +465,21 @@ def dataclass_fields(cls):
return FIELDS[cls]


def dataclass_init_fields(cls):

return tuple(f for f in dataclass_fields(cls) if f.init)
def dataclass_init_fields(cls, as_list=False):
init_fields = [f for f in dataclass_fields(cls) if f.init]
return init_fields if as_list else tuple(init_fields)


def dataclass_field_names(cls):

return tuple(f.name for f in dataclass_fields(cls))


def dataclass_init_field_names(cls):

return tuple(f.name for f in dataclass_init_fields(cls))


def dataclass_field_to_default(cls):

if cls not in FIELD_TO_DEFAULT:
Expand Down
23 changes: 21 additions & 2 deletions dataclass_wizard/class_helper.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections import defaultdict
from dataclasses import Field
from typing import Any, Callable
from typing import Any, Callable, Literal, overload

from .abstractions import W, AbstractLoader, AbstractDumper, AbstractParser, E, AbstractLoaderGenerator
from .bases import META, AbstractMeta
Expand Down Expand Up @@ -226,22 +226,41 @@ def get_meta(cls: type, base_cls: T = AbstractMeta) -> T | META:
"""


def create_meta(cls: type, cls_name: str | None = None, **kwargs) -> None:
"""
Sets the Meta config for the :class:`AbstractJSONWizard` subclass.
WARNING: Only use if the Meta config is undefined,
e.g. `get_meta` for the `cls` returns `base_cls`.
"""


def dataclass_fields(cls: type) -> 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.
"""

@overload
def dataclass_init_fields(cls: type, as_list: Literal[True] = False) -> list[Field]:
"""Get only the dataclass fields that would be passed into the constructor."""


def dataclass_init_fields(cls: type) -> tuple[Field, ...]:
@overload
def dataclass_init_fields(cls: type, as_list: Literal[False] = False) -> tuple[Field]:
"""Get only the dataclass fields that would be passed into the constructor."""


def dataclass_field_names(cls: type) -> tuple[str, ...]:
"""Get the names of all dataclass fields"""


def dataclass_init_field_names(cls: type) -> tuple[str, ...]:
"""Get the names of all __init__() dataclass fields"""


def dataclass_field_to_default(cls: type) -> dict[str, Any]:
"""Get default values for the (optional) dataclass fields."""

Expand Down
18 changes: 14 additions & 4 deletions dataclass_wizard/dumpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .constants import _DUMP_HOOKS, TAG, CATCH_ALL
from .decorators import _alias
from .errors import show_deprecation_warning
from .loader_selection import _get_load_fn_for_dataclass
from .log import LOG
from .models import get_skip_if_condition, finalize_skip_if
from .type_def import (
Expand Down Expand Up @@ -312,10 +313,19 @@ def dump_func_for_dataclass(cls: Type[T],
# the load parser for each field (if needed), but don't cache the
# result, as it's conceivable we might yet call `LoadMeta` later.
from .loader_selection import get_loader
cls_loader = get_loader(cls, v1=meta.v1)
# Use the cached result if it exists, but don't cache it ourselves.
_ = dataclass_field_to_load_parser(
cls_loader, cls, config, save=False)

if meta.v1:
# TODO there must be a better way to do this,
# this is just a temporary workaround.
try:
_ = _get_load_fn_for_dataclass(cls, v1=True)
except Exception:
pass
else:
cls_loader = get_loader(cls, v1=meta.v1)
# Use the cached result if it exists, but don't cache it ourselves.
_ = dataclass_field_to_load_parser(
cls_loader, cls, config, save=False)

# Tag key to populate when a dataclass is in a `Union` with other types.
tag_key = meta.tag_key or TAG
Expand Down
23 changes: 14 additions & 9 deletions dataclass_wizard/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from dataclasses import Field, MISSING
from typing import (Any, Type, Dict, Tuple, ClassVar,
Optional, Union, Iterable, Callable, Collection, Sequence)
from warnings import deprecated

from .utils.string_conv import normalize

Expand Down Expand Up @@ -315,37 +316,41 @@ def message(self) -> str:

class UnknownKeysError(JSONWizardError):
"""
Error raised when an unknown JSON key is encountered in the JSON load
process.
Error raised when unknown JSON key(s) are
encountered in the JSON load process.
Note that this error class is only raised when the
`raise_on_unknown_json_key` flag is enabled in the :class:`Meta` class.
`raise_on_unknown_json_key` flag is enabled in
the :class:`Meta` class.
"""

_TEMPLATE = ('One or more JSON keys are not mapped to the dataclass schema for class `{cls}`.\n'
' Unknown key{s}: {json_key!r}\n'
' Unknown key{s}: {unknown_keys!r}\n'
' Dataclass fields: {fields!r}\n'
' Input JSON object: {json_string}')

def __init__(self,
json_key: str,
unknown_keys: 'list[str] | str',
obj: JSONObject,
cls: Type,
cls_fields: Tuple[Field, ...], **kwargs):
super().__init__()

self.json_key = json_key
self.unknown_keys = unknown_keys
self.obj = obj
self.fields = [f.name for f in cls_fields]
self.kwargs = kwargs
self.class_name: str = self.name(cls)

# self.class_name: str = type_name(cls)
@property
@deprecated('use `unknown_keys` instead')
def json_key(self):
return self.unknown_keys

@property
def message(self) -> str:
from .utils.json_util import safe_dumps
if not isinstance(self.json_key, str) and len(self.json_key) > 1:
if not isinstance(self.unknown_keys, str) and len(self.unknown_keys) > 1:
s = 's'
else:
s = ''
Expand All @@ -355,7 +360,7 @@ def message(self) -> str:
s=s,
json_string=safe_dumps(self.obj),
fields=self.fields,
json_key=self.json_key)
unknown_keys=self.unknown_keys)

if self.kwargs:
sep = '\n '
Expand Down
7 changes: 4 additions & 3 deletions dataclass_wizard/loader_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ def fromlist(cls: type[T], list_of_dict: list[JSONObject]) -> list[T]:
return [load(d) for d in list_of_dict]


def _get_load_fn_for_dataclass(cls: type[T]) -> Callable[[JSONObject], T]:
meta = get_meta(cls)
def _get_load_fn_for_dataclass(cls: type[T], v1=None) -> Callable[[JSONObject], T]:
if v1 is None:
v1 = getattr(get_meta(cls), 'v1', False)

if meta.v1:
if v1:
from .v1.loaders import load_func_for_dataclass as V1_load_func_for_dataclass
# noinspection PyTypeChecker
load = V1_load_func_for_dataclass(cls, {})
Expand Down
Loading

0 comments on commit dde2d83

Please sign in to comment.