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

Improve NewTypes #310

Merged
merged 4 commits into from
Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ History
* cattrs now supports un/structuring ``kw_only`` fields on attrs classes into/from dictionaries.
(`#247 <https://github.com/python-attrs/cattrs/pull/247>`_)
* `NewTypes <https://docs.python.org/3/library/typing.html#newtype>`_ are now supported by the ``cattrs.Converter``.
(`#255 <https://github.com/python-attrs/cattrs/pull/255>`_, `#94 <https://github.com/python-attrs/cattrs/issues/94>`_)
(`#255 <https://github.com/python-attrs/cattrs/pull/255>`_, `#94 <https://github.com/python-attrs/cattrs/issues/94>`_, `#297 <https://github.com/python-attrs/cattrs/issues/297>`_)
* ``cattrs.Converter`` and ``cattrs.BaseConverter`` can now copy themselves using the ``copy`` method.
(`#284 <https://github.com/python-attrs/cattrs/pull/284>`_)
* PyPy support (and tests, using a minimal Hypothesis profile) restored.
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,4 @@
"from enum import Enum, unique"
)
autodoc_typehints = "description"
autosectionlabel_prefix_document = True
25 changes: 24 additions & 1 deletion docs/structuring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,33 @@ To support arbitrary unions, register a custom structuring hook for the union
`PEP 593`_ annotations (``typing.Annotated[type, ...]``) are supported and are
matched using the first type present in the annotated type.

.. _structuring_newtypes:

``typing.NewType``
~~~~~~~~~~~~~~~~~~

`NewTypes`_ are supported and are structured according to the rules for their underlying type.
Their hooks can also be overriden using :py:attr:`cattrs.Converter.register_structure_hook`.

.. doctest::

>>> from typing import NewType
>>> from datetime import datetime

>>> IsoDate = NewType("IsoDate", datetime)

>>> converter = cattrs.Converter()
>>> converter.register_structure_hook(IsoDate, lambda v, _: datetime.fromisoformat(v))

>>> converter.structure("2022-01-01", IsoDate)
datetime.datetime(2022, 1, 1, 0, 0)

.. versionadded:: 22.2.0

.. seealso:: :ref:`Unstructuring NewTypes. <unstructuring_newtypes>`

.. note::
NewTypes are not supported by the legacy BaseConverter.

``attrs`` classes and dataclasses
---------------------------------
Expand Down Expand Up @@ -481,7 +504,7 @@ Here's a small example showing how to use factory hooks to apply the `forbid_ext
cattrs.errors.ForbiddenExtraKeysError: Extra fields in constructor for E: else


A complex use case for hook factories is described over at :ref:`Using factory hooks`.
A complex use case for hook factories is described over at :ref:`usage:Using factory hooks`.

.. _`PEP 593` : https://www.python.org/dev/peps/pep-0593/
.. _`NewTypes`: https://docs.python.org/3/library/typing.html#newtype
13 changes: 11 additions & 2 deletions docs/unstructuring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,26 @@ from ``typing`` on older Python versions.
Fields marked as ``typing.Annotated[type, ...]`` are supported and are matched
using the first type present in the annotated type.

.. _unstructuring_newtypes:

``typing.NewType``
------------------

`NewTypes`_ are supported and are unstructured according to the rules for their underlying type.
Their hooks can also be overriden using :py:attr:`cattrs.Converter.register_unstructure_hook`.

.. versionadded:: 22.2.0

.. seealso:: :ref:`Structuring NewTypes. <structuring_newtypes>`

.. note::
NewTypes are not supported by the legacy BaseConverter.

``attrs`` classes and dataclasses
---------------------------------

``attrs`` classes and dataclasses are supported out of the box.
:class:`.Converter` s support two unstructuring strategies:
:class:`cattrs.Converter` s support two unstructuring strategies:

* ``UnstructureStrategy.AS_DICT`` - similar to ``attr.asdict``, unstructures ``attrs`` and dataclass instances into dictionaries. This is the default.
* ``UnstructureStrategy.AS_TUPLE`` - similar to ``attr.astuple``, unstructures ``attrs`` and dataclass instances into tuples.
Expand Down Expand Up @@ -205,6 +214,6 @@ Here's a small example showing how to use factory hooks to skip unstructuring
{'an_int': 1}


A complex use case for hook factories is described over at :ref:`Using factory hooks`.
A complex use case for hook factories is described over at :ref:`usage:Using factory hooks`.

.. _`NewTypes`: https://docs.python.org/3/library/typing.html#newtype
2 changes: 1 addition & 1 deletion docs/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Validation
==========

`cattrs` has a detailed validation mode since version 2022.1.0, and this mode is enabled by default.
`cattrs` has a detailed validation mode since version 22.1.0, and this mode is enabled by default.
When running under detailed validation, the un/structuring hooks are slightly slower but produce more precise and exhaustive error messages.

Detailed validation
Expand Down
9 changes: 8 additions & 1 deletion src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ def register_unstructure_hook(self, cls: Any, func: Callable[[Any], Any]) -> Non
resolve_types(cls)
if is_union_type(cls):
self._unstructure_func.register_func_list([(lambda t: t == cls, func)])
elif get_newtype_base(cls) is not None:
# This is a newtype, so we handle it specially.
self._unstructure_func.register_func_list([(lambda t: t is cls, func)])
else:
self._unstructure_func.register_cls_list([(cls, func)])

Expand Down Expand Up @@ -270,6 +273,9 @@ def register_structure_hook(
if is_union_type(cl):
self._union_struct_registry[cl] = func
self._structure_func.clear_cache()
elif get_newtype_base(cl) is not None:
# This is a newtype, so we handle it specially.
self._structure_func.register_func_list([(lambda t: t is cl, func)])
else:
self._structure_func.register_cls_list([(cl, func)])

Expand Down Expand Up @@ -831,7 +837,8 @@ def __init__(

def get_structure_newtype(self, type: Type[T]) -> Callable[[Any, Any], T]:
base = get_newtype_base(type)
return self._structure_func.dispatch(base)
handler = self._structure_func.dispatch(base)
return lambda v, _: handler(v, base)

def gen_unstructure_annotated(self, type):
origin = type.__origin__
Expand Down
9 changes: 5 additions & 4 deletions src/cattrs/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,18 @@ def register_cls_list(self, cls_and_handler, direct: bool = False) -> None:

def register_func_list(
self,
func_and_handler: List[
pred_and_handler: List[
Union[
Tuple[Callable[[Any], bool], Any],
Tuple[Callable[[Any], bool], Any, bool],
]
],
):
"""register a function to determine if the handle
should be used for the type
"""
for tup in func_and_handler:
Register a predicate function to determine if the handle
should be used for the type.
"""
for tup in pred_and_handler:
if len(tup) == 2:
func, handler = tup
self._function_dispatch.register(func, handler)
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from cattrs import BaseConverter, Converter


@pytest.fixture(params=(True, False))
def genconverter(request):
return Converter(detailed_validation=request.param)


@pytest.fixture(params=(True, False))
def converter(request, converter_cls):
return converter_cls(detailed_validation=request.param)
Expand Down
57 changes: 57 additions & 0 deletions tests/test_newtypes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Tests for NewTypes."""
from typing import NewType

import pytest

from cattrs import BaseConverter

PositiveIntNewType = NewType("PositiveIntNewType", int)
BigPositiveIntNewType = NewType("BigPositiveIntNewType", PositiveIntNewType)


def test_newtype_structure_hooks(genconverter: BaseConverter):
"""NewTypes should work with `register_structure_hook`."""

assert genconverter.structure("0", int) == 0
assert genconverter.structure("0", PositiveIntNewType) == 0
assert genconverter.structure("0", BigPositiveIntNewType) == 0

genconverter.register_structure_hook(
PositiveIntNewType, lambda v, _: int(v) if int(v) > 0 else 1 / 0
)

with pytest.raises(ZeroDivisionError):
genconverter.structure("0", PositiveIntNewType)

assert genconverter.structure("1", PositiveIntNewType) == 1

with pytest.raises(ZeroDivisionError):
genconverter.structure("0", BigPositiveIntNewType)

genconverter.register_structure_hook(
BigPositiveIntNewType, lambda v, _: int(v) if int(v) > 50 else 1 / 0
)

with pytest.raises(ZeroDivisionError):
genconverter.structure("1", BigPositiveIntNewType)

assert genconverter.structure("1", PositiveIntNewType) == 1
assert genconverter.structure("51", BigPositiveIntNewType) == 51


def test_newtype_unstructure_hooks(genconverter: BaseConverter):
"""NewTypes should work with `register_unstructure_hook`."""

assert genconverter.unstructure(0, int) == 0
assert genconverter.unstructure(0, PositiveIntNewType) == 0
assert genconverter.unstructure(0, BigPositiveIntNewType) == 0

genconverter.register_unstructure_hook(PositiveIntNewType, oct)

assert genconverter.unstructure(0, PositiveIntNewType) == "0o0"
assert genconverter.unstructure(0, BigPositiveIntNewType) == "0o0"

genconverter.register_unstructure_hook(BigPositiveIntNewType, hex)

assert genconverter.unstructure(0, PositiveIntNewType) == "0o0"
assert genconverter.unstructure(0, BigPositiveIntNewType) == "0x0"