Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.27.0: Drop support for Python Versions 3.6, 3.7, and 3.8 #139

Merged
merged 24 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
# The type of runner that the job will run on
strategy:
matrix:
python-versions: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
python-versions: [3.9, '3.10', '3.11', '3.12', '3.13']
os: [ubuntu-20.04]
# Uncomment if I need to run it on other environments too (currently
# there's not a huge need)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

strategy:
matrix:
python-versions: [3.8]
python-versions: [3.11]

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work for Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13, and for PyPy. Check
3. The pull request should work for Python 3.9, 3.10, 3.11, 3.12 and 3.13, and for PyPy. Check
https://github.com/rnag/dataclass-wizard/actions/workflows/dev.yml
and make sure that the tests pass for all supported Python versions.

Expand Down
24 changes: 24 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@
History
=======

0.27.0 (2024-11-10)
-------------------

**Features and Improvements**

* This minor release drops support for Python 3.6, 3.7, and 3.8, all of which have reached End of Life (EOL). Check out the Python End of Life Cycle here_. Key changes resulting from this update include:
* Resolved pyup errors, previously flagged as "insecure" due to outdated package versions that lacked support for Python 3.8 or earlier.
* Update all requirements to latest versions.
* Cleaned up various TODO comments scattered throughout the codebase, as many were specific to older Python versions.
* Simplified and improved codebase for easier maintenance.
* Remove everything except the ``py.typed`` file (see comment_).
* Added `test case`_ to satisfy :issue:`89`.
* Added support for cyclic or "recursive" dataclasses, as first mentioned in :issue:`62` (special thanks to :user:`dlenski` for finalizing this in :pr:`138`!).

**Bugfixes**

* :issue:`62`: Cyclic or "recursive" dataclasses no longer raises a :class:`RecursionError`.
* Typing locals should now correctly key off the correct Python version, see the commit_ that addressed this.

.. _here: https://devguide.python.org/versions/#status-of-python-versions
.. _test case: https://github.com/rnag/dataclass-wizard/pull/139/commits/cf2e98cb75c75dc3e566ed0205637dbd4632e159
.. _comment: https://github.com/rnag/dataclass-wizard/pull/136#issuecomment-2466463153
.. _commit: https://github.com/rnag/dataclass-wizard/pull/139/commits/310a0c28690fdfdf15a386a427d1ea9aaf8898a1

0.26.1 (2024-11-09)
-------------------

Expand Down
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ include CONTRIBUTING.rst
include HISTORY.rst
include LICENSE
include README.rst
include dataclass_wizard/py.typed

recursive-include tests *.py
recursive-exclude tests/integration *
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ clean-build: ## remove build artifacts
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -f {} +
find . -name '*.egg' -type f -exec rm -f {} +

clean-pyc: ## remove Python file artifacts
find . -name '*.pyc' -exec rm -f {} +
Expand Down
72 changes: 63 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Alternatively, this library is available `on conda`_ under the `conda-forge`_ ch

$ conda install dataclass-wizard -c conda-forge

The ``dataclass-wizard`` library officially supports **Python 3.6** or higher.
The ``dataclass-wizard`` library officially supports **Python 3.9** or higher.

.. _on conda: https://anaconda.org/conda-forge/dataclass-wizard
.. _conda-forge: https://conda-forge.org/
Expand Down Expand Up @@ -133,7 +133,7 @@ Usage and Examples

Using the built-in JSON marshalling support for dataclasses:

Note: The following example should work in **Python 3.7+** with the included ``__future__``
Note: The following example should work in **Python 3.9+** with the included ``__future__``
import.

.. code:: python3
Expand Down Expand Up @@ -633,9 +633,7 @@ A brief example of the intended usage is shown below:

from dataclasses import dataclass
from datetime import time, datetime
from typing import List
# Note: in Python 3.9+, you can import this from `typing` instead
from typing_extensions import Annotated
from typing import Annotated

from dataclass_wizard import fromdict, asdict, DatePattern, TimePattern, Pattern

Expand All @@ -645,7 +643,7 @@ A brief example of the intended usage is shown below:
date_field: DatePattern['%m-%Y']
dt_field: Annotated[datetime, Pattern('%m/%d/%y %H.%M.%S')]
time_field1: TimePattern['%H:%M']
time_field2: Annotated[List[time], Pattern('%I:%M %p')]
time_field2: Annotated[list[time], Pattern('%I:%M %p')]


data = {'date_field': '12-2022',
Expand All @@ -669,6 +667,61 @@ A brief example of the intended usage is shown below:
# serialization. In fact, it'll be faster than parsing the custom patterns!
assert class_obj == fromdict(MyClass, asdict(class_obj))

"Recursive" Dataclasses with Cyclic References
----------------------------------------------

Prior to version `v0.27.0`, dataclasses with cyclic references
or self-referential structures were not supported. This
limitation is shown in the following toy example:

.. code:: python3

from dataclasses import dataclass

@dataclass
class A:
a: 'A | None' = None

a = A(a=A(a=A(a=A())))

This was a `longstanding issue`_.

New in ``v0.27.0``: The Dataclass Wizard now extends its support
to cyclic and self-referential dataclass models.

The example below demonstrates recursive dataclasses with cyclic
dependencies, following the pattern ``A -> B -> A -> B``. For more details, see
the `Cyclic or "Recursive" Dataclasses`_ section in the documentation.

.. code:: python3

from __future__ import annotations # This can be removed in Python 3.10+

from dataclasses import dataclass

from dataclass_wizard import JSONWizard


@dataclass
class A(JSONWizard):
class _(JSONWizard.Meta):
# enable support for self-referential / recursive dataclasses
recursive_classes = True

b: 'B | None' = None


@dataclass
class B:
a: A | None = None


# confirm that `from_dict` with a recursive, self-referential
# input `dict` works as expected.
a = A.from_dict({'b': {'a': {'b': {'a': None}}}})

assert a == A(b=B(a=A(b=B())))

Dataclasses in ``Union`` Types
------------------------------

Expand Down Expand Up @@ -778,7 +831,6 @@ result. An example of both these approaches is shown below.

from collections import defaultdict
from dataclasses import field, dataclass
from typing import DefaultDict, List

from dataclass_wizard import JSONWizard

Expand All @@ -792,8 +844,8 @@ result. An example of both these approaches is shown below.
my_str: str
other_str: str = 'any value'
optional_str: str = None
my_list: List[str] = field(default_factory=list)
my_dict: DefaultDict[str, List[float]] = field(
my_list: list[str] = field(default_factory=list)
my_dict: defaultdict[str, list[float]] = field(
default_factory=lambda: defaultdict(list))


Expand Down Expand Up @@ -925,4 +977,6 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage
.. _`Patterned Date and Time`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/patterned_date_time.html
.. _Union: https://docs.python.org/3/library/typing.html#typing.Union
.. _`Dataclasses in Union Types`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/dataclasses_in_union_types.html
.. _`Cyclic or "Recursive" Dataclasses`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/cyclic_or_recursive_dataclasses.html
.. _as milestones: https://github.com/rnag/dataclass-wizard/milestones
.. _longstanding issue: https://github.com/rnag/dataclass-wizard/issues/62
12 changes: 2 additions & 10 deletions dataclass_wizard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

>>> from dataclasses import dataclass, field
>>> from datetime import datetime
>>> from typing import Optional, List
>>> from typing import Optional
>>>
>>> from dataclass_wizard import JSONSerializable, property_wizard
>>>
Expand All @@ -18,7 +18,7 @@
>>> class MyClass(JSONSerializable, metaclass=property_wizard):
>>>
>>> my_str: Optional[str]
>>> list_of_int: List[int] = field(default_factory=list)
>>> list_of_int: list[int] = field(default_factory=list)
>>> # You can also define this as `my_dt`, however only the annotation
>>> # will carry over in that case, since the value is re-declared by
>>> # the property below.
Expand Down Expand Up @@ -98,7 +98,6 @@
import logging

from .bases_meta import LoadMeta, DumpMeta
from .constants import PY36
from .dumpers import DumpMixin, setup_default_dumper, asdict
from .loaders import LoadMixin, setup_default_loader, fromlist, fromdict
from .models import (json_field, json_key, Container,
Expand All @@ -122,10 +121,3 @@
# Setup the default type hooks to use when converting `dataclass` instances to
# a JSON `string` or a Python `dict` object.
setup_default_dumper()

if PY36: # pragma: no cover
# Python 3.6 requires a backport for `datetime.fromisoformat()`
# noinspection PyPackageRequirements
# noinspection PyUnresolvedReferences
from backports.datetime_fromisoformat import MonkeyPatch
MonkeyPatch.patch_fromisoformat()
6 changes: 6 additions & 0 deletions dataclass_wizard/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ class AbstractMeta(metaclass=ABCOrAndMeta):
# apply in a recursive manner.
recursive: ClassVar[bool] = True

# True to support cyclic or self-referential dataclasses. For example,
# the type of a dataclass field in class `A` refers to `A` itself.
#
# See https://github.com/rnag/dataclass-wizard/issues/62 for more details.
recursive_classes: ClassVar[bool] = False

# True to raise an class:`UnknownJSONKey` when an unmapped JSON key is
# encountered when `from_dict` or `from_json` is called; an unknown key is
# one that does not have a known mapping to a dataclass field.
Expand Down
9 changes: 8 additions & 1 deletion dataclass_wizard/bases_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
get_outer_class_name, get_class_name, create_new_class,
json_field_to_dataclass_field, dataclass_field_to_json_field
)
from .constants import TAG
from .decorators import try_with_load
from .dumpers import get_dumper
from .enums import LetterCase, DateTimeTo
Expand Down Expand Up @@ -173,10 +174,13 @@ def _as_enum_safe(cls, name: str, base_type: Type[E]) -> Optional[E]:
# noinspection PyPep8Naming
def LoadMeta(*, debug_enabled: bool = False,
recursive: bool = True,
recursive_classes: 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) -> META:
tag: str = None,
tag_key: str = TAG,
auto_assign_tags: bool = False) -> META:
"""
Helper function to setup the ``Meta`` Config for the JSON load
(de-serialization) process, which is intended for use alongside the
Expand All @@ -198,11 +202,14 @@ def LoadMeta(*, debug_enabled: bool = False,
base_dict = {
'__slots__': (),
'raise_on_unknown_json_key': raise_on_unknown_json_key,
'recursive_classes': recursive_classes,
'key_transform_with_load': key_transform,
'json_key_to_field': json_key_to_field,
'debug_enabled': debug_enabled,
'recursive': recursive,
'tag': tag,
'tag_key': tag_key,
'auto_assign_tags': auto_assign_tags,
}

# Create a new subclass of :class:`AbstractMeta`
Expand Down
4 changes: 1 addition & 3 deletions dataclass_wizard/class_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@

# A cached mapping of a dataclass to each of its case-insensitive field names
# and load hook.
#
# Note: need to create a `ForwardRef` here, because Python 3.6 complains.
_FIELD_NAME_TO_LOAD_PARSER: Dict[
Type, 'DictWithLowerStore[str, AbstractParser]'] = {}
Type, DictWithLowerStore[str, AbstractParser]] = {}

# Since the dump process doesn't use Parsers currently, we use a sentinel
# mapping to confirm if we need to setup the dump config for a dataclass
Expand Down
12 changes: 0 additions & 12 deletions dataclass_wizard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,6 @@
# Current system Python version
_PY_VERSION = sys.version_info[:2]

# Check if currently running Python 3.6
PY36 = _PY_VERSION == (3, 6)

# Check if currently running Python 3.8
PY38 = _PY_VERSION == (3, 8)

# Check if currently running Python 3.8 or higher
PY38_OR_ABOVE = _PY_VERSION >= (3, 8)

# Check if currently running Python 3.9
PY39 = _PY_VERSION == (3, 9)

# Check if currently running Python 3.10 or higher
PY310_OR_ABOVE = _PY_VERSION >= (3, 10)

Expand Down
9 changes: 0 additions & 9 deletions dataclass_wizard/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,6 @@ def new_func(o: Any):
return new_func


def discard_kwargs(f):

@wraps(f)
def new_func(*args, **_kwargs):
return f(*args)

return new_func


def _alias(default: Callable) -> Callable[[T], T]:
"""
Decorator which re-assigns a function `_f` to point to `default` instead.
Expand Down
Loading
Loading