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.35.0: Patterned Date and Time, Finalize Alias and AliasPath #174

Merged
merged 56 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8272fdc
Support Patterned Date and Time
rnag Jan 1, 2025
29569df
minor fix
rnag Jan 1, 2025
bee0797
V1: Enable `KeyCase.AUTO` to support multiple JSON keys
rnag Jan 2, 2025
f2e0ae8
minor refactor
rnag Jan 2, 2025
68c8fd9
minor refactor
rnag Jan 2, 2025
f43759c
minor refactor
rnag Jan 2, 2025
666d9ab
minor refactor
rnag Jan 3, 2025
48e2d71
Fix: `key_case='AUTO'` now supports multiple aliases
rnag Jan 13, 2025
3ca9895
add space
rnag Jan 13, 2025
1b8e5a3
update `Alias` to support multiple aliases
rnag Jan 13, 2025
b7f6ef3
update `AliasPath` to support multiple alias paths
rnag Jan 14, 2025
ae8c55f
fix benhmarks
rnag Jan 14, 2025
202075d
minor optimizations for the better
rnag Jan 15, 2025
8cfdec8
minor refactor
rnag Jan 15, 2025
0ae4ebc
update docstrings
rnag Jan 15, 2025
a2bf461
cleanup imports
rnag Jan 15, 2025
c2ef3f1
cleanup imports
rnag Jan 15, 2025
b6e25ef
cleanup imports
rnag Jan 15, 2025
648dffd
minor fix
rnag Jan 15, 2025
be171de
update docs
rnag Jan 15, 2025
28ea8fd
address code quality suggestions
rnag Jan 16, 2025
e292edf
Optimize `load_to_int` and make it reject floats with fractional parts
rnag Jan 16, 2025
3b0665e
finalize `load_to_int` logic
rnag Jan 16, 2025
ed895d1
fix
rnag Jan 16, 2025
25d8041
I think this is more readable
rnag Jan 16, 2025
2db3a45
add TODO
rnag Jan 16, 2025
592a1f7
cleanup TODOS and simplify how we check for `TypedDict`s
rnag Jan 17, 2025
ee07e73
move `is_builtin_class` to function_builder.py
rnag Jan 17, 2025
927a19e
code cleanup
rnag Jan 17, 2025
7e3b1b3
minor refactor
rnag Jan 17, 2025
0bb9ca4
update docs
rnag Jan 17, 2025
7a202bd
update docs
rnag Jan 17, 2025
7c92e8e
minor refactor
rnag Jan 18, 2025
0e91626
minor refactor
rnag Jan 18, 2025
832f10c
update docstrings
rnag Jan 18, 2025
739393d
Add Aware and UTC date/times
rnag Jan 18, 2025
6a2497b
add tests
rnag Jan 19, 2025
3945f5f
update docstrings
rnag Jan 19, 2025
ab0c059
fix CI tests
rnag Jan 19, 2025
74a7166
fix CI tests
rnag Jan 19, 2025
6db7a34
update docs
rnag Jan 19, 2025
559be67
update docs
rnag Jan 19, 2025
36d8565
update docs
rnag Jan 19, 2025
365d617
simplify docs
rnag Jan 19, 2025
cf07867
enhance docs
rnag Jan 19, 2025
91b50ea
enhance docs
rnag Jan 19, 2025
6bb5ff4
enhance docs
rnag Jan 19, 2025
73fecb8
enhance docs
rnag Jan 19, 2025
6403a1b
add docs on v1_alias.rst
rnag Jan 20, 2025
b471c9f
add links and notes to docs on V1 Opt-in
rnag Jan 20, 2025
604e011
Update HISTORY.rst and ensure `py.typed` marker is included
rnag Jan 20, 2025
626ae1f
Update HISTORY.rst
rnag Jan 20, 2025
3b77241
Update HISTORY.rst
rnag Jan 20, 2025
81fb34f
Update README.rst
rnag Jan 20, 2025
d43b267
Update README.rst
rnag Jan 20, 2025
6d01ca1
add notes to V1 Opt-in
rnag Jan 20, 2025
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
14 changes: 6 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,27 @@ jobs:
name: Create Release
runs-on: ubuntu-latest

strategy:
matrix:
python-versions: [3.11]

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Checkout code
uses: actions/checkout@v3

# Temporarily disable this - I want it to trigger on merge, but it doesn't
# work (at least not on a tagged commit too)
# - name: Exit if not on main branch
# if: endsWith(github.ref, 'main') == false
# run: exit -1

- uses: actions/setup-python@v2
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-versions }}
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox-gh-actions wheel
pip install setuptools wheel

- name: Replace version in README
run: |
Expand Down
31 changes: 31 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@
History
=======

0.35.0 (2025-01-19)
-------------------

**Features and Improvements**

* **V1 Opt-In:**
* Add support for Patterned Date and Time:
* Naive Date/Time/Datetime
* Timezone-aware Time/Datetime
* UTC Time/Datetime
* Update :func:`Alias` and :func:`AliasPath` to support multiple aliases and nested path(s)
* Update the ``KeyCase.AUTO`` setting (specified via ``v1_key_case='AUTO'``) to correctly
handle multiple possible keys for the field (e.g., it doesn't latch onto the first encountered key but now
tries all valid key case transformations at runtime). This now results in expected or desired behavior (fixes :issue:`175`)
* **Float to Int Conversion Change**: In V1 Opt-in (via ``Meta`` setting ``v1=True``), floats or float strings
with fractional parts (e.g., ``123.4`` or ``"123.4"``) are no longer silently converted to integers.
Instead, they now raise an error. However, floats without fractional parts (e.g., ``3.0`` or ``"3.0"``)
will continue to convert to integers as before.
* Add documentation:
* Patterned Date and Time
* Aliases
* Add tests for coverage
* Optimize logic for determining if an annotated type is a ``TypedDict``
* Update ``requirements-bench.txt`` to correctly capture all Benchmark-related dependencies

**Bugfixes**

* Ensure the ``py.typed`` marker is included in the source distribution (fixes :issue:`173`)
* Address a minor bug in object path parsing that did not correctly interpret quoted literal values
within blocks such as braces ``[]``

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

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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
6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,11 @@ 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
python setup.py sdist 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
python setup.py sdist bdist_wheel
ls -l dist
$(MAKE) revert_readme

Expand Down
169 changes: 125 additions & 44 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ for complex and *nested dataclass* models!
>>> from dataclass_wizard import JSONWizard
...
>>> @dataclass
... class MyClass(JSONWizard):
... class MyClass(JSONWizard, key_case='AUTO'):
... my_str: str | None
... is_active_tuple: tuple[bool, ...]
... list_of_int: list[int] = field(default_factory=list)
Expand Down Expand Up @@ -92,8 +92,6 @@ Early access to **V1** is available! To opt in, simply enable ``v1=True`` in the

For more information, see the `Field Guide to V1 Opt-in`_.

.. _`Field Guide to V1 Opt-in`: https://github.com/rnag/dataclass-wizard/wiki/Field-Guide-to-V1-Opt%E2%80%90in

Performance Improvements
~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -482,6 +480,13 @@ Here is an example to demonstrate the usage of these helper functions:
Custom Key Mappings
-------------------

.. note::
**Important:** The functionality for **custom key mappings** (such as JSON-to-dataclass field mappings) is being re-imagined with the introduction of **V1 Opt-in**. Enhanced support for these features is now available, improving the user experience for working with custom mappings.

For more details, see the `Field Guide to V1 Opt-in`_ and the `V1 Alias`_ documentation.

This change is part of the ongoing improvements in version ``v0.35.0+``, and the old functionality will no longer be maintained in future releases.

If you ever find the need to add a `custom mapping`_ of a JSON key to a dataclass
field (or vice versa), the helper function ``json_field`` -- which can be
considered an alias to ``dataclasses.field()`` -- is one approach that can
Expand Down Expand Up @@ -516,7 +521,14 @@ Example below:
Mapping Nested JSON Keys
------------------------

The ``dataclass-wizard`` library lets you map deeply nested JSON keys to dataclass fields using custom path notation. This is ideal for handling complex or non-standard JSON structures.
.. note::
**Important:** The current "nested path" functionality is being re-imagined.
Please refer to the new docs for **V1 Opt-in** features, which introduce enhanced support for these use
cases. For more details, see the `Field Guide to V1 Opt-in`_ and the `V1 Alias`_ documentation.

This change is part of the ongoing improvements in version ``v0.35.0+``, and the old functionality will no longer be maintained in future releases.

The ``dataclass-wizard`` library allows you to map deeply nested JSON keys to dataclass fields using custom path notation. This is ideal for handling complex or non-standard JSON structures.

You can specify paths to JSON keys with the ``KeyPath`` or ``path_field`` helpers. For example, the deeply nested key ``data.items.myJSONKey`` can be mapped to a dataclass field, such as ``my_str``:

Expand All @@ -536,7 +548,7 @@ You can specify paths to JSON keys with the ``KeyPath`` or ``path_field`` helper
Custom Paths for Complex JSON
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can use `custom paths to access nested keys`_ and map them to specific fields, even when keys contain special characters or follow non-standard conventions.
You can now use `custom paths to access nested keys`_ and map them to specific fields, even when keys contain special characters or follow non-standard conventions.

Example with nested and complex keys:

Expand Down Expand Up @@ -785,18 +797,30 @@ undefined keys, the field will default to an empty dictionary.
Date and Time with Custom Patterns
----------------------------------

As of *v0.20.0*, date and time strings in a `custom format`_ can be de-serialized
using the ``DatePattern``, ``TimePattern``, and ``DateTimePattern`` type annotations,
representing patterned `date`, `time`, and `datetime` objects respectively.
.. tip::
As of **v0.35.0** with V1 Opt-in, Dataclass Wizard now supports timezone-aware and UTC ``datetime``
and ``time`` patterns, as well as multiple pattern strings (i.e. multiple `custom formats`) for greater
flexibility in pattern matching. These features are **not** available in the current ``v0.*`` versions.

This will internally call ``datetime.strptime`` with the format specified in the annotation,
and also use the ``fromisoformat()`` method in case the date string is in ISO-8601 format.
All dates and times will continue to be serialized as ISO format strings by default. For more
info, check out the `Patterned Date and Time`_ section in the docs.
The new features include:

A brief example of the intended usage is shown below:
- Timezone-aware ``datetime`` and ``time`` patterns.
- UTC ``datetime`` and ``time`` patterns.
- Multiple `custom formats`_ for a single field, providing more control over pattern matching.

.. code:: python3
For more details and examples on how to use these new features, refer to the `V1 Opt-in documentation for Patterned Date and Time`_.

As of **v0.20.0**, date and time strings in `custom formats`_ can be de-serialized using the ``DatePattern``,
``TimePattern``, and ``DateTimePattern`` type annotations, which represent patterned ``date``, ``time``, and
``datetime`` objects, respectively.

Internally, these annotations use ``datetime.strptime`` with the specified format and the ``fromisoformat()``
method for ISO-8601 formatted strings. All date and time values are still serialized to ISO format strings by
default. For more information, refer to the `Patterned Date and Time`_ section in the documentation.

Here is an example demonstrating how to use these annotations:

.. code-block:: python3

from dataclasses import dataclass
from datetime import time, datetime
Expand All @@ -807,9 +831,13 @@ A brief example of the intended usage is shown below:

@dataclass
class MyClass:
# Custom format for date (Month-Year)
date_field: DatePattern['%m-%Y']
# Custom format for datetime (Month/Day/Year Hour.Minute.Second)
dt_field: Annotated[datetime, Pattern('%m/%d/%y %H.%M.%S')]
# Custom format for time (Hour:Minute)
time_field1: TimePattern['%H:%M']
# Custom format for a list of times (12-hour format with AM/PM)
time_field2: Annotated[list[time], Pattern('%I:%M %p')]


Expand All @@ -820,7 +848,7 @@ A brief example of the intended usage is shown below:

class_obj = fromdict(MyClass, data)

# All annotated fields de-serialize as just date, time, or datetime, as shown.
# All annotated fields de-serialize to date, time, or datetime objects, as shown.
print(class_obj)
# MyClass(date_field=datetime.date(2022, 12, 1), dt_field=datetime.datetime(2023, 1, 2, 2, 3, 52),
# time_field1=datetime.time(15, 20), time_field2=[datetime.time(13, 20), datetime.time(0, 30)])
Expand All @@ -830,8 +858,8 @@ A brief example of the intended usage is shown below:
# {'dateField': '2022-12-01', 'dtField': '2023-01-02T02:03:52',
# 'timeField1': '15:20:00', 'timeField2': ['13:20:00', '00:30:00']}

# But, the patterned date/times can still be de-serialized back after
# serialization. In fact, it'll be faster than parsing the custom patterns!
# The patterned date/times can be de-serialized back after serialization, which will be faster than
# re-parsing the custom patterns!
assert class_obj == fromdict(MyClass, asdict(class_obj))

Recursive Types and Dataclasses with Cyclic References
Expand Down Expand Up @@ -1456,48 +1484,98 @@ refer to the `Using Field Properties`_ section in the documentation.
What's New in v1.0
------------------

.. admonition:: v1 Opt-in Now Available
.. admonition:: Opt-in for v1 Now Available

Early opt-in for **v1** is now available with enhanced features, including intuitive ``Union`` parsing and optimized performance. To enable this,
The early opt-in for **v1** is now available with enhanced features, including intuitive ``Union`` parsing and optimized performance. To enable this,
set ``v1=True`` in your ``Meta`` settings.

For more details and migration guidance, see the `Field Guide to V1 Opt-in`_.

.. warning::
.. warning:: *Important Changes in v1.0*

- **Default Key Transformation Update**

- **Default Key Transformation Update**
Starting with **v1.0.0**, the default key transformation for JSON serialization
will change to keep keys *as-is* instead of converting them to ``camelCase``.

Starting with ``v1.0.0``, the default key transformation for JSON serialization
will change to keep keys *as-is* instead of converting them to `camelCase`.
**New Default Behavior**:
The default setting for key transformation will be ``key_transform='NONE'``.

**New Default Behavior**: ``key_transform='NONE'`` will be the standard setting.
**How to Prepare**:
You can enforce this behavior immediately by using the ``JSONPyWizard`` helper, as shown below:

**How to Prepare**: You can enforce this future behavior right now by using the ``JSONPyWizard`` helper:
.. code-block:: python3

.. code-block:: python3
from dataclasses import dataclass
from dataclass_wizard import JSONPyWizard

from dataclasses import dataclass
from dataclass_wizard import JSONPyWizard
@dataclass
class MyModel(JSONPyWizard):
my_field: str

@dataclass
class MyModel(JSONPyWizard):
my_field: str
print(MyModel(my_field="value").to_dict())
# Output: {'my_field': 'value'}

print(MyModel(my_field="value").to_dict())
# Output: {'my_field': 'value'}
- **Default __str__() Behavior Change**

- **Float to Int Conversion Change**
Starting with **v1.0.0**, we no longer pretty-print the serialized JSON value with keys in ``camelCase``.
Instead, we now use the ``pprint`` module to handle serialization formatting.

Starting in ``v1.0``, floats or float strings with fractional
parts (e.g., ``123.4`` or ``"123.4"``) will no longer be silently
converted to integers. Instead, they will raise an error.
However, floats with no fractional parts (e.g., ``3.0``
or ``"3.0"``) will still convert to integers as before.
**New Default Behavior**:
The ``__str__()`` method in the ``JSONWizard`` class will use ``pprint`` by default.

**How to Prepare**: To ensure compatibility with the new behavior:
- Use ``float`` annotations for fields that may include fractional values.
- Review your data and avoid passing fractional values (e.g., ``123.4``) to fields annotated as ``int``.
- Update tests or logic that rely on the current rounding behavior.
**How to Prepare**:
You can immediately test this new behavior using the ``JSONPyWizard`` helper, as demonstrated below:

.. code-block:: python3

from dataclasses import dataclass
from dataclass_wizard import JSONWizard, JSONPyWizard

@dataclass
class CurrentModel(JSONWizard):
my_field: str

@dataclass
class NewModel(JSONPyWizard):
my_field: str

print(CurrentModel(my_field="value"))
#> {
# "myField": "value"
# }

print(NewModel(my_field="value"))
#> NewModel(my_field='value')

- **Float to Int Conversion Change**
Starting with **v1.0**, floats or float strings with fractional parts (e.g., ``123.4`` or ``"123.4"``) will no longer be silently converted to integers. Instead, they will raise an error. However, floats without fractional parts (e.g., ``3.0`` or ``"3.0"``) will continue to convert to integers as before.

**How to Prepare**:
You can opt in to **v1** via ``v1=True`` to test this behavior right now. Additionally, to ensure compatibility with the new behavior:

- Use ``float`` annotations for fields that may include fractional values.
- Review your data to avoid passing fractional values (e.g., ``123.4``) to fields annotated as ``int``.
- Update tests or logic that depend on the current rounding behavior.

.. code-block:: python3

from dataclasses import dataclass
from dataclass_wizard import JSONPyWizard

@dataclass
class Test(JSONPyWizard):
class _(JSONPyWizard.Meta):
v1 = True

list_of_int: list[int]

input_dict = {'list_of_int': [1, '2.0', '3.', -4, '-5.00', '6', '-7']}
t = Test.from_dict(input_dict)
print(t) #> Test(list_of_int=[1, 2, 3, -4, -5, 6, -7])

# ERROR!
_ = Test.from_dict({'list_of_int': [123.4]})

Contributing
------------
Expand Down Expand Up @@ -1542,7 +1620,7 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage
.. _`wiz-cli`: https://dataclass-wizard.readthedocs.io/en/latest/wiz_cli.html
.. _`key limitations`: https://florimond.dev/en/posts/2018/10/reconciling-dataclasses-and-properties-in-python/
.. _`more complete example`: https://dataclass-wizard.readthedocs.io/en/latest/examples.html#a-more-complete-example
.. _custom format: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
.. _custom formats: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
.. _`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
Expand All @@ -1555,3 +1633,6 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage
.. _annotations: https://docs.python.org/3/library/typing.html#typing.Annotated
.. _typing: https://docs.python.org/3/library/typing.html
.. _dataclasses: https://docs.python.org/3/library/dataclasses.html
.. _V1 Opt-in documentation for Patterned Date and Time: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/v1_patterned_date_time.html
.. _`Field Guide to V1 Opt-in`: https://github.com/rnag/dataclass-wizard/wiki/Field-Guide-to-V1-Opt%E2%80%90in
.. _V1 Alias: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/v1_alias.html
10 changes: 8 additions & 2 deletions dataclass_wizard/bases_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,19 @@ def bind_to(cls, dataclass: type, create=True, is_default=True,
add_for_load = field_to_alias.pop('__load__', True)
add_for_dump = field_to_alias.pop('__dump__', True)

# Convert string values to single-element tuples
field_to_aliases = {k: (v, ) if isinstance(v, str) else v
for k, v in field_to_alias.items()}

if add_for_load:
DATACLASS_FIELD_TO_ALIAS_FOR_LOAD[dataclass].update(
field_to_alias)
field_to_aliases
)

if add_for_dump:
dataclass_field_to_json_field(dataclass).update(
field_to_alias)
{k: v[0] for k, v in field_to_aliases.items()}
)

if cls.key_transform_with_dump is not None:
cls_dumper.transform_dataclass_field = _as_enum_safe(
Expand Down
Loading