Skip to content

Commit

Permalink
Add 3.13 compatibility (#129)
Browse files Browse the repository at this point in the history
* Pull dataclasses remove functions into module

* explicitly pass no type params to _eval_type() to suppress warning

* Only add eval type param on 3.13 or greater

* Address comments

---------

Co-authored-by: Ritvik Nag <[email protected]>
  • Loading branch information
benjjs and rnag authored Nov 5, 2024
1 parent 32c5cd0 commit e5f37cb
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 15 deletions.
3 changes: 3 additions & 0 deletions dataclass_wizard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__'
Expand Down
5 changes: 3 additions & 2 deletions dataclass_wizard/models.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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


Expand Down
4 changes: 2 additions & 2 deletions dataclass_wizard/serial_json.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down
59 changes: 59 additions & 0 deletions dataclass_wizard/utils/dataclass_compat.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 7 additions & 11 deletions dataclass_wizard/utils/typing_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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}:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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__`
Expand All @@ -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, ()}
Expand Down Expand Up @@ -248,7 +242,6 @@ def _get_origin(cls, raise_=False):
raise
return cls


def _get_args(cls):
if is_literal(cls):
return cls.__values__
Expand All @@ -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.
Expand Down Expand Up @@ -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],
Expand Down

0 comments on commit e5f37cb

Please sign in to comment.