Skip to content

Commit

Permalink
v0.27.0: Drop support for Python Versions 3.6, 3.7, and 3.8 (#139)
Browse files Browse the repository at this point in the history
* bump

* minor fix

* minor fix

* Add test case to satisfy #89

* Support cyclic references (#138)

* support cyclic or self-referential dataclasses

* update to add custom recursion error

* Don't replace RecursionError with RecursiveClassError if recursion-safe loader is already enabled

If we get a `RecursionError` in this case, something else must have gone wrong

---------

Co-authored-by: Ritvik Nag <[email protected]>

* Update all requirements to latest versions

* Update requirements in requirements-dev.txt and requirements-test.txt to latest

* "When you drop py3.6/7/8, remove everything EXCEPT the py.typed file."

* See [comment](#136 (comment))

* Update tests to support Python 3.9+

* Run tests on CI for PY 3.9+
* `typing_extensions` should be required for Python 3.10 (as well as 3.9)

* Starting to purge PY 3.6, 3.7 and 3.8 support

* Fix `Sphinx` dependency issue

* I realized this was a bug!

* Continue with purge of PY 3.6, 3.7, and 3.8

* Continue with purge of PY 3.6, 3.7, and 3.8

* Continue with purge of PY 3.6, 3.7, and 3.8

* This concludes the purge of PY 3.6, 3.7, and 3.8

* Remove unnecessary code

* Remove unnecessary code

* Update docs to mention cyclic or "recursive" dataclasses

* Update docs to remove `typing.List` mentions

* Update docs to remove `typing.List` mentions

* Update HISTORY.rst

---------

Co-authored-by: Dan Lenski <[email protected]>
  • Loading branch information
rnag and dlenski authored Nov 10, 2024
1 parent b688833 commit 2b80792
Show file tree
Hide file tree
Showing 45 changed files with 554 additions and 556 deletions.
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

0 comments on commit 2b80792

Please sign in to comment.