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.34: support recursive types and end-to-end (E2E) support for bytes and bytearray #171

Merged
merged 37 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b2bfd99
`v1`: Support recursive types and bytes
rnag Dec 20, 2024
1719690
show deprecation warning for `debug_enabled`
rnag Dec 20, 2024
e92417a
add test case for `bytes` and `bytearray`
rnag Dec 20, 2024
904cdbd
Performance fixes and updates
rnag Dec 24, 2024
4f0c908
code cleanup and add tests for `StrEnum` and `IntEnum`
rnag Dec 24, 2024
820e388
minor code refactor
rnag Dec 24, 2024
541debc
don't need to register load hook for `Enum`
rnag Dec 24, 2024
6230844
code cleanup
rnag Dec 24, 2024
3fdedcf
fix return type
rnag Dec 26, 2024
bd254d6
optimize `load_to_named_tuple`
rnag Dec 26, 2024
ff507f9
change name to `recursive_guard`
rnag Dec 27, 2024
d925f88
refactor code
rnag Dec 27, 2024
0d73e02
refactor code to use decorators.py
rnag Dec 27, 2024
e6125a9
code cleanup
rnag Dec 27, 2024
bc0b326
update benchmarks and tests
rnag Dec 27, 2024
1eb9a56
add test for Literal recursive
rnag Dec 29, 2024
8cdfbb7
make "load to NamedTuple" recursive
rnag Dec 29, 2024
ec00259
make "load to TypedDict" recursive
rnag Dec 29, 2024
abd6362
move `dataclasses-json` to slower pakages
rnag Dec 29, 2024
6294c25
fixing typing issues
rnag Dec 29, 2024
35ac54a
Support `TypeAliasType` and recursive `Union` in `load_to_union`
rnag Dec 29, 2024
59f9888
Update `make check` to replace version in `README.rst`
rnag Dec 29, 2024
a67ed2b
move Python 3.12 specific tests to test_union_as_type_alias_recursive.py
rnag Dec 30, 2024
1f2c899
fix
rnag Dec 30, 2024
1765920
fix
rnag Dec 30, 2024
ec8ea2f
fix
rnag Dec 30, 2024
2299f85
fix
rnag Dec 30, 2024
6dc6683
fix
rnag Dec 30, 2024
7c78677
fix
rnag Dec 30, 2024
a6917f0
Add docs on "Recursive Types"
rnag Dec 30, 2024
972bfd1
Update docs on "Recursive Types"
rnag Dec 30, 2024
07e8452
Merge sections together
rnag Dec 30, 2024
d2b4064
Update docs on "Supported Types"
rnag Dec 30, 2024
d96a95f
Update docs on "Supported Types"
rnag Dec 30, 2024
4a378bb
Update docs on "Supported Types"
rnag Dec 30, 2024
a3ef015
update HISTORY.rst
rnag Dec 30, 2024
25e2d1b
update HISTORY.rst
rnag Dec 30, 2024
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
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ exclude_lines =
# Conditional code which is dependent on the OS, or `os.name`
if name == 'nt':

# This will exclude all lines starting with something like
# if PY311_OR_ABOVE: or if PY310_BETA:.
if PY\d+_\w+:

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
Expand Down
30 changes: 30 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@
History
=======

0.34.0 (2024-12-30)
-------------------

**Features and Improvements**

- **V1 Opt-in**
- Support for recursive types OOTB for the following Python types:
- ``NamedTuple``
- ``TypedDict``
- ``Union``
- ``Literal``
- Nested `dataclasses`
- `Type aliases`_ (introduced in Python 3.12+)
- Full support for ``bytes`` and ``bytearray`` in the de/serialization process (fixes :issue:`140`).
- Performance improvements: Optimized Load functions for ``bool``, ``NamedTuple``, ``datetime``, ``date``, and ``time``.
- Added support for `Type aliases`_ (via ``type`` statement in Python 3.12+).
- Improved logic in ``load_to_str`` to better check if it's within an ``Optional[...]`` type.
- Enhanced handling of sub-types in de/serialization (**TODO**: add test cases).
- Show deprecation warning for Meta setting ``debug_enabled`` (replaced by ``v1_debug``).

- Updated benchmarks for improved accuracy.

**Bugfixes**

- Fixed issue where code generation failed to correctly account for indexes, especially when nested collection types like ``dict`` were used within a ``NamedTuple``.
- ``make check`` now works out-of-the-box for validating ``README.rst`` and other RST files for PyPI deployment.
- :pr:`169`: Explicitly added ``utf-8`` encoding for ``setup.py`` to enable installation from source on Windows (shoutout to :user:`birkholz-cubert`!).

.. _Type aliases: https://docs.python.org/3/library/typing.html#type-aliases

0.33.0 (2024-12-17)
-------------------

Expand Down
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,33 @@ servedocs: docs ## compile the docs watching for changes
release: dist ## package and upload a release
twine upload dist/*

check: dist ## verify release before upload to PyPI
check: dist-local ## verify release before upload to PyPI
twine check dist/*

dist: clean ## builds source and wheel package
python setup.py sdist
python setup.py bdist_wheel
ls -l dist

dist-local: clean replace_version ## builds source and wheel package (for local testing)
python setup.py sdist
python setup.py bdist_wheel
ls -l dist
$(MAKE) revert_readme

replace_version: ## replace |version| in README.rst with the current version
cp README.rst README.rst.bak
python -c "import re; \
from pathlib import Path; \
version = re.search(r\"__version__\\s*=\\s*'(.+?)'\", Path('dataclass_wizard/__version__.py').read_text()).group(1); \
readme_path = Path('README.rst'); \
readme_content = readme_path.read_text(); \
readme_path.write_text(readme_content.replace('|version|', version)); \
print(f'Replaced version in {readme_path}: {version}')"

revert_readme: ## revert README.rst to its original state
mv README.rst.bak README.rst

install: clean ## install the package to the active Python's site-packages
python setup.py install

Expand Down
155 changes: 135 additions & 20 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ This library supports **Python 3.9+**. Support for Python 3.6 – 3.8 was
available in earlier releases but is no longer maintained, as those
versions no longer receive security updates.

For convenience, the table below outlines the last compatible version
For convenience, the table below outlines the last compatible release
of *Dataclass Wizard* for unsupported Python versions (3.6 – 3.8):

.. list-table::
Expand All @@ -153,15 +153,15 @@ of *Dataclass Wizard* for unsupported Python versions (3.6 – 3.8):
* - Python Version
- Last Version of ``dataclass-wizard``
- Python EOL
* - 3.6
* - 3.8
- 0.26.1_
- 2021-12-23
- 2024-10-07
* - 3.7
- 0.26.1_
- 2023-06-27
* - 3.8
* - 3.6
- 0.26.1_
- 2024-10-07
- 2021-12-23

.. _0.26.1: https://pypi.org/project/dataclass-wizard/0.26.1/
.. _PyPI: https://pypi.org/project/dataclass-wizard/
Expand Down Expand Up @@ -834,10 +834,10 @@ 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
----------------------------------------------
Recursive Types and Dataclasses with Cyclic References
------------------------------------------------------

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

Expand All @@ -851,28 +851,24 @@ limitation is shown in the following toy example:

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.
This was a `longstanding issue`_, but starting with ``v0.27.0``, Dataclass Wizard now supports
recursive dataclasses, including cyclic references.

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.
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
# Enable support for self-referential / recursive dataclasses
recursive_classes = True

b: 'B | None' = None
Expand All @@ -882,13 +878,132 @@ the `Cyclic or "Recursive" Dataclasses`_ section in the documentation.
class B:
a: A | None = None


# confirm that `from_dict` with a recursive, self-referential
# 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())))

Starting with version **0.34.0**, recursive types are supported *out of the box* (OOTB) with ``v1`` opt-in,
removing the need for any ``Meta`` settings like ``recursive_classes = True``.

This makes working with recursive dataclasses even easier and more streamlined. In addition, recursive types
are now supported for the following Python type constructs:

- NamedTuple_
- TypedDict_
- Union_
- Literal_
- Nested dataclasses_
- `Type aliases`_ (introduced in Python 3.12+)

.. _NamedTuple: https://docs.python.org/3/library/typing.html#typing.NamedTuple
.. _TypedDict: https://docs.python.org/3/library/typing.html#typing.TypedDict
.. _Union: https://docs.python.org/3/library/typing.html#typing.Union
.. _Literal: https://docs.python.org/3/library/typing.html#typing.Literal
.. _Type aliases: https://docs.python.org/3/library/typing.html#type-aliases

Example Usage
~~~~~~~~~~~~~

Recursive types allow handling complex nested data structures, such as deeply nested JSON objects or lists.
With ``v0.34.0`` of Dataclass Wizard, de/serializing these structures becomes seamless
and more intuitive.

Recursive ``Union``
###################

.. code-block:: python3

from dataclasses import dataclass
from dataclass_wizard import JSONWizard

# For Python 3.9, use this `Union` approach:
from typing_extensions import TypeAlias
JSON: TypeAlias = 'str | int | float | bool | dict[str, JSON] | list[JSON] | None'

# For Python 3.10 and above, use this simpler approach:
# JSON = str | int | float | bool | dict[str, 'JSON'] | list['JSON'] | None

# For Python 3.12+, you can use the `type` statement:
# type JSON = str | int | float | bool | dict[str, JSON] | list[JSON] | None

@dataclass
class MyTestClass(JSONWizard):

class _(JSONWizard.Meta):
v1 = True

name: str
meta: str
msg: JSON

x = MyTestClass.from_dict(
{
"name": "name",
"meta": "meta",
"msg": [{"x": {"x": [{"x": ["x", 1, 1.0, True, None]}]}}],
}
)
assert x == MyTestClass(
name="name",
meta="meta",
msg=[{"x": {"x": [{"x": ["x", 1, 1.0, True, None]}]}}],
)

.. note::
The ``type`` statement in Python 3.12+ simplifies type alias definitions by avoiding string annotations for recursive references.

Recursive ``Union`` with Nested ``dataclasses``
###############################################

.. code-block:: python3

from dataclasses import dataclass, field
from dataclass_wizard import JSONWizard

@dataclass
class A(JSONWizard):

class _(JSONWizard.Meta):
v1 = True

value: int
nested: 'B'
next: 'A | None' = None


@dataclass
class B:
items: list[A] = field(default_factory=list)


x = A.from_dict(
{
"value": 1,
"next": {"value": 2, "next": None, "nested": {}},
"nested": {"items": [{"value": 3, "nested": {}}]},
}
)
assert x == A(
value=1,
next=A(value=2, next=None, nested=B(items=[])),
nested=B(items=[A(value=3, nested=B())]),
)

.. note::
Nested ``dataclasses`` are particularly useful for representing hierarchical structures, such as trees or graphs, in a readable and maintainable way.

Official References
~~~~~~~~~~~~~~~~~~~

For more information, see:

- `Typing in Python <https://docs.python.org/3/library/typing.html>`_
- `PEP 695: Type Syntax <https://peps.python.org/pep-0695/>`_

These examples illustrate the power of recursive types in simplifying complex data structures while leveraging the functionality of ``dataclass-wizard``.

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

Expand Down
Binary file modified benchmarks/catch_all.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 12 additions & 11 deletions benchmarks/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Name(NamedTuple):
last: str
salutation: Optional[str] = 'Mr.'


@dataclass
class NameDataclass:
first: str
Expand Down Expand Up @@ -218,14 +219,14 @@ def test_load(request, data, data_2, data_dacite, n):
"""
[ RESULTS ON MAC OS X ]

benchmarks.complex.complex - [INFO] dataclass-wizard 0.373847
benchmarks.complex.complex - [INFO] dataclass-factory 0.777164
benchmarks.complex.complex - [INFO] dataclasses-json 28.177022
benchmarks.complex.complex - [INFO] dacite 6.619898
benchmarks.complex.complex - [INFO] mashumaro 0.351623
benchmarks.complex.complex - [INFO] pydantic 0.563395
benchmarks.complex.complex - [INFO] jsons 30.564242
benchmarks.complex.complex - [INFO] jsons (strict) 35.122489
benchmarks.complex.complex - [INFO] dataclass-wizard 0.325364
benchmarks.complex.complex - [INFO] dataclass-factory 0.773195
benchmarks.complex.complex - [INFO] dataclasses-json 28.435088
benchmarks.complex.complex - [INFO] dacite 6.287875
benchmarks.complex.complex - [INFO] mashumaro 0.344701
benchmarks.complex.complex - [INFO] pydantic 0.547749
benchmarks.complex.complex - [INFO] jsons 29.978993
benchmarks.complex.complex - [INFO] jsons (strict) 34.052532
"""
g = globals().copy()
g.update(locals())
Expand All @@ -236,9 +237,6 @@ def test_load(request, data, data_2, data_dacite, n):
log.info('dataclass-factory %f',
timeit('factory.load(data_2, MyClass)', globals=g, number=n))

log.info('dataclasses-json %f',
timeit('MyClassDJ.from_dict(data_2)', globals=g, number=n))

log.info('dacite %f',
timeit('dacite_from_dict(MyClassDacite, data_dacite, config=dacite_cfg)',
globals=g, number=n))
Expand All @@ -264,6 +262,9 @@ def test_load(request, data, data_2, data_dacite, n):
if not request.config.getoption("--all"):
pytest.skip("Skipping benchmarks for the rest by default, unless --all is specified.")

log.info('dataclasses-json %f',
timeit('MyClassDJ.from_dict(data_2)', globals=g, number=n))

log.info('jsons %f',
timeit('MyClassJsons.load(data)', globals=g, number=n))

Expand Down
14 changes: 7 additions & 7 deletions benchmarks/nested.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,13 @@ def test_load(request, data, n):
"""
[ RESULTS ON MAC OS X ]

benchmarks.nested.nested - [INFO] dataclass-wizard 0.135700
benchmarks.nested.nested - [INFO] dataclass-factory 0.412265
benchmarks.nested.nested - [INFO] dataclasses-json 11.448704
benchmarks.nested.nested - [INFO] mashumaro 0.150680
benchmarks.nested.nested - [INFO] pydantic 0.328947
benchmarks.nested.nested - [INFO] jsons 25.052287
benchmarks.nested.nested - [INFO] jsons (strict) 43.233567
benchmarks.nested.nested - [INFO] dataclass-wizard 0.130734
benchmarks.nested.nested - [INFO] dataclass-factory 0.404371
benchmarks.nested.nested - [INFO] dataclasses-json 11.315233
benchmarks.nested.nested - [INFO] mashumaro 0.158986
benchmarks.nested.nested - [INFO] pydantic 0.330295
benchmarks.nested.nested - [INFO] jsons 25.084872
benchmarks.nested.nested - [INFO] jsons (strict) 28.306646

"""
g = globals().copy()
Expand Down
5 changes: 3 additions & 2 deletions dataclass_wizard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
~~~~~~~~~~~~~~~~

Lightning-fast JSON wizardry for Python dataclasses — effortless
serialization with no external tools required!
serialization right out of the box!

Sample Usage:

Expand Down Expand Up @@ -119,6 +119,7 @@
import logging

from .bases_meta import LoadMeta, DumpMeta, EnvMeta
from .constants import PACKAGE_NAME
from .dumpers import DumpMixin, setup_default_dumper, asdict
from .loaders import LoadMixin, setup_default_loader
from .loader_selection import fromlist, fromdict
Expand All @@ -135,7 +136,7 @@

# Set up logging to ``/dev/null`` like a library is supposed to.
# http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library
logging.getLogger('dataclass_wizard').addHandler(logging.NullHandler())
logging.getLogger(PACKAGE_NAME).addHandler(logging.NullHandler())

# Setup the default type hooks to use when converting `str` (json) or a Python
# `dict` object to a `dataclass` instance.
Expand Down
Loading