diff --git a/dataclass_wizard/constants.py b/dataclass_wizard/constants.py index 3e0623d3..a4c2df2d 100644 --- a/dataclass_wizard/constants.py +++ b/dataclass_wizard/constants.py @@ -26,6 +26,9 @@ # Check if currently running Python 3.11 or higher PY311_OR_ABOVE = _PY_VERSION >= (3, 11) +# Check if currently running Python 3.13 or higher +PY313_OR_ABOVE = _PY_VERSION >= (3, 13) + # 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__' diff --git a/dataclass_wizard/models.py b/dataclass_wizard/models.py index 05b2a96c..e24738f9 100644 --- a/dataclass_wizard/models.py +++ b/dataclass_wizard/models.py @@ -1,6 +1,5 @@ import json -# noinspection PyProtectedMember -from dataclasses import MISSING, Field, _create_fn +from dataclasses import MISSING, Field from datetime import date, datetime, time from typing import (cast, Collection, Callable, Optional, List, Union, Type, Generic) @@ -9,6 +8,8 @@ from .constants import PY310_OR_ABOVE from .decorators import cached_property from .type_def import T, DT, Encoder, PyTypedDict, FileEncoder +# noinspection PyProtectedMember +from .utils.dataclass_compat import _create_fn from .utils.type_conv import as_datetime, as_time, as_date diff --git a/dataclass_wizard/serial_json.py b/dataclass_wizard/serial_json.py index e376684e..f309bf5b 100644 --- a/dataclass_wizard/serial_json.py +++ b/dataclass_wizard/serial_json.py @@ -1,6 +1,4 @@ import json -# noinspection PyProtectedMember -from dataclasses import _create_fn, _set_new_attribute from typing import Type, List, Union, AnyStr from .abstractions import AbstractJSONWizard, W @@ -10,6 +8,8 @@ from .dumpers import asdict from .loaders import fromdict, fromlist from .type_def import Decoder, Encoder, JSONObject, ListOfJSONObject +# noinspection PyProtectedMember +from .utils.dataclass_compat import _create_fn, _set_new_attribute class JSONSerializable(AbstractJSONWizard): diff --git a/dataclass_wizard/utils/dataclass_compat.py b/dataclass_wizard/utils/dataclass_compat.py new file mode 100644 index 00000000..1079fcd2 --- /dev/null +++ b/dataclass_wizard/utils/dataclass_compat.py @@ -0,0 +1,59 @@ +""" +Pulling some functions removed in recent versions of Python into the module for continued compatibility. +All function names and bodies are left exactly as they were prior to being removed. +""" + +from dataclasses import MISSING +from types import FunctionType + + +def _set_qualname(cls, value): + # Removed in Python 3.13 + # Original: `dataclasses._set_qualname` + # Ensure that the functions returned from _create_fn uses the proper + # __qualname__ (the class they belong to). + if isinstance(value, FunctionType): + value.__qualname__ = f"{cls.__qualname__}.{value.__name__}" + return value + + +def _set_new_attribute(cls, name, value): + # Removed in Python 3.13 + # Original: `dataclasses._set_new_attribute` + # Never overwrites an existing attribute. Returns True if the + # attribute already exists. + if name in cls.__dict__: + return True + _set_qualname(cls, value) + setattr(cls, name, value) + return False + + +def _create_fn(name, args, body, *, globals=None, locals=None, + return_type=MISSING): + # Removed in Python 3.13 + # Original: `dataclasses._create_fn` + # Note that we may mutate locals. Callers beware! + # The only callers are internal to this module, so no + # worries about external callers. + if locals is None: + locals = {} + return_annotation = '' + if return_type is not MISSING: + locals['__dataclass_return_type__'] = return_type + return_annotation = '->__dataclass_return_type__' + args = ','.join(args) + body = '\n'.join(f' {b}' for b in body) + + # Compute the text of the entire function. + txt = f' def {name}({args}){return_annotation}:\n{body}' + + # Free variables in exec are resolved in the global namespace. + # The global namespace we have is user-provided, so we can't modify it for + # our purposes. So we put the things we need into locals and introduce a + # scope to allow the function we're creating to close over them. + local_vars = ', '.join(locals.keys()) + txt = f"def __create_fn__({local_vars}):\n{txt}\n return {name}" + ns = {} + exec(txt, globals, ns) + return ns['__create_fn__'](**locals) diff --git a/dataclass_wizard/utils/typing_compat.py b/dataclass_wizard/utils/typing_compat.py index f2f0f281..10a9bb6a 100644 --- a/dataclass_wizard/utils/typing_compat.py +++ b/dataclass_wizard/utils/typing_compat.py @@ -22,7 +22,7 @@ from collections.abc import Callable from .string_conv import repl_or_with_union -from ..constants import PY36, PY38, PY310_OR_ABOVE, PY39 +from ..constants import PY36, PY38, PY310_OR_ABOVE, PY313_OR_ABOVE, PY39 from ..type_def import FREF, PyLiteral, PyTypedDicts, PyForwardRef @@ -85,11 +85,9 @@ def get_keys_for_typed_dict(cls): except ImportError: from typing import _AnnotatedAlias - def _is_annotated(cls): return isinstance(cls, _AnnotatedAlias) - def _is_base_generic(cls): if isinstance(cls, typing._GenericAlias): if cls.__origin__ in {typing.Generic, typing._Protocol}: @@ -142,7 +140,6 @@ def is_literal(cls) -> bool: def _process_forward_annotation(base_type): return PyForwardRef(base_type, is_argument=False) - def _get_origin(cls, raise_=False): if isinstance(cls, types.UnionType): return typing.Union @@ -180,7 +177,6 @@ def _get_origin(cls, raise_=False): raise return cls - def _get_named_tuple_field_types(cls, raise_=True): """ Note: The latest Python versions only support the `__annotations__` @@ -204,12 +200,10 @@ def _get_named_tuple_field_types(cls, raise_=True): from typing_extensions import AnnotatedMeta - def _process_forward_annotation(base_type): return PyForwardRef( repl_or_with_union(base_type), is_argument=False) - def _is_base_generic(cls): if isinstance(cls, (typing.GenericMeta, typing._Union)): return cls.__args__ in {None, ()} @@ -248,7 +242,6 @@ def _get_origin(cls, raise_=False): raise return cls - def _get_args(cls): if is_literal(cls): return cls.__values__ @@ -266,7 +259,6 @@ def _get_args(cls): # my_union: Union return () - def _get_named_tuple_field_types(cls, raise_=True): """ Note: Prior to PEP 526, only `_field_types` attribute was assigned. @@ -372,8 +364,12 @@ def eval_forward_ref(base_type: FREF, # Evaluate the ForwardRef here base_globals = sys.modules[cls.__module__].__dict__ - # noinspection PyProtectedMember - return typing._eval_type(base_type, base_globals, _TYPING_LOCALS) + if PY313_OR_ABOVE: + # noinspection PyProtectedMember + return typing._eval_type(base_type, base_globals, _TYPING_LOCALS, type_params=()) + else: + # noinspection PyProtectedMember + return typing._eval_type(base_type, base_globals, _TYPING_LOCALS) def eval_forward_ref_if_needed(base_type: typing.Union[typing.Type, FREF],