Skip to content
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
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.8"]
fail-fast: false

steps:
Expand All @@ -24,6 +24,7 @@ jobs:
- uses: "actions/setup-python@v4"
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true

- name: "Run Tox"
run: |
Expand Down
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
- **Potentially breaking**: {py:func}`cattrs.gen.make_dict_structure_fn` and {py:func}`cattrs.gen.typeddicts.make_dict_structure_fn` will use the values for the `detailed_validation` and `forbid_extra_keys` parameters from the given converter by default now.
If you're using these functions directly, the old behavior can be restored by passing in the desired values directly.
([#410](https://github.com/python-attrs/cattrs/issues/410) [#411](https://github.com/python-attrs/cattrs/pull/411))
- Python 3.12 is now supported. Python 3.7 is no longer supported; use older releases there.
([#424](https://github.com/python-attrs/cattrs/pull/424))
- Introduce the `use_class_methods` strategy. Learn more [here](https://catt.rs/en/latest/strategies.html#using-class-specific-structure-and-unstructure-methods).
([#405](https://github.com/python-attrs/cattrs/pull/405))
- Implement the `union passthrough` strategy, enabling much richer union handling for preconfigured converters. [Learn more here](https://catt.rs/en/stable/strategies.html#union-passthrough).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ destructure them.

- Free software: MIT license
- Documentation: https://catt.rs
- Python versions supported: 3.7 and up. (Older Python versions, like 2.7, 3.5 and 3.6 are supported by older versions; see the changelog.)
- Python versions supported: 3.8 and up. (Older Python versions are supported by older versions; see the changelog.)

## Features

Expand Down
1 change: 0 additions & 1 deletion docs/structuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,6 @@ Generic TypedDicts work on Python 3.11 and later, since that is the first Python

[`typing.Required` and `typing.NotRequired`](https://peps.python.org/pep-0655/) are supported.

On Python 3.7, using `typing_extensions.TypedDict` is required since `typing.TypedDict` doesn't exist there.
On Python 3.8, using `typing_extensions.TypedDict` is recommended since `typing.TypedDict` doesn't support all necessary features, so certain combinations of subclassing, totality and `typing.Required` won't work.

[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), structuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_structure_fn`.
Expand Down
1 change: 0 additions & 1 deletion docs/unstructuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ False

Generic TypedDicts work on Python 3.11 and later, since that is the first Python version that supports them in general.

On Python 3.7, using `typing_extensions.TypedDict` is required since `typing.TypedDict` doesn't exist there.
On Python 3.8, using `typing_extensions.TypedDict` is recommended since `typing.TypedDict` doesn't support all necessary features, so certain combinations of subclassing, totality and `typing.Required` won't work.

[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), unstructuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_unstructure_fn`.
Expand Down
997 changes: 424 additions & 573 deletions pdm.lock

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test = [
"hypothesis>=6.79.4",
"pytest>=7.4.0",
"pytest-benchmark>=4.0.0",
"immutables>=0.19",
"immutables>=0.20",
"typing-extensions>=4.7.1",
"coverage>=7.2.7",
]
Expand Down Expand Up @@ -53,10 +53,23 @@ dependencies = [
"typing-extensions>=4.1.0, !=4.6.3; python_version < '3.11'",
"exceptiongroup>=1.1.1; python_version < '3.11'",
]
requires-python = ">=3.7"
requires-python = ">=3.8"
readme = "README.md"
license = {text = "MIT"}
keywords = ["attrs", "serialization", "dataclasses"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Typing :: Typed",
]

[project.urls]
Homepage = "https://catt.rs"
Expand Down
43 changes: 10 additions & 33 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
from dataclasses import fields as dataclass_fields
from dataclasses import is_dataclass
from typing import AbstractSet as TypingAbstractSet
from typing import Any, Deque, Dict, FrozenSet, List
from typing import Any, Deque, Dict, Final, FrozenSet, List
from typing import Mapping as TypingMapping
from typing import MutableMapping as TypingMutableMapping
from typing import MutableSequence as TypingMutableSequence
from typing import MutableSet as TypingMutableSet
from typing import NewType, Optional
from typing import NewType, Optional, Protocol
from typing import Sequence as TypingSequence
from typing import Set as TypingSet
from typing import Tuple, get_type_hints
from typing import Tuple, get_args, get_origin, get_type_hints

from attr import NOTHING, Attribute, Factory
from attr import fields as attrs_fields
from attr import resolve_types
from attrs import NOTHING, Attribute, Factory
from attrs import fields as attrs_fields
from attrs import resolve_types

__all__ = ["ExceptionGroup", "ExtensionsTypedDict", "TypedDict", "is_typeddict"]

Expand All @@ -27,18 +27,6 @@
except ImportError:
ExtensionsTypedDict = None

if sys.version_info >= (3, 8):
from typing import Final, Protocol, get_args, get_origin

else:

def get_args(cl):
return cl.__args__

def get_origin(cl):
return getattr(cl, "__origin__", None)

from typing_extensions import Final, Protocol

if sys.version_info >= (3, 11):
from builtins import ExceptionGroup
Expand Down Expand Up @@ -355,16 +343,11 @@ def get_full_type_hints(obj, globalns=None, localns=None):
TupleSubscriptable = Tuple

from collections import Counter as ColCounter
from typing import Counter, Union, _GenericAlias
from typing import Counter, TypedDict, Union, _GenericAlias

from typing_extensions import Annotated, NotRequired, Required
from typing_extensions import get_origin as te_get_origin

if sys.version_info >= (3, 8):
from typing import TypedDict
else:
TypedDict = ExtensionsTypedDict

def is_annotated(type) -> bool:
return te_get_origin(type) is Annotated

Expand Down Expand Up @@ -440,16 +423,10 @@ def is_counter(type):
or getattr(type, "__origin__", None) is ColCounter
)

if sys.version_info >= (3, 8):
from typing import Literal

def is_literal(type) -> bool:
return type.__class__ is _GenericAlias and type.__origin__ is Literal
from typing import Literal

else:
# No literals in 3.7.
def is_literal(_) -> bool:
return False
def is_literal(type) -> bool:
return type.__class__ is _GenericAlias and type.__origin__ is Literal

def is_generic(obj):
return isinstance(obj, _GenericAlias)
Expand Down
4 changes: 2 additions & 2 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,7 @@ def __init__(
if OriginAbstractSet in co:
if OriginMutableSet not in co:
co[OriginMutableSet] = co[OriginAbstractSet]
co[AbcMutableSet] = co[OriginAbstractSet] # For 3.7/3.8 compatibility.
co[AbcMutableSet] = co[OriginAbstractSet] # For 3.8 compatibility.
if FrozenSetSubscriptable not in co:
co[FrozenSetSubscriptable] = co[OriginAbstractSet]

Expand All @@ -830,7 +830,7 @@ def __init__(
co[set] = co[OriginMutableSet]

if FrozenSetSubscriptable in co:
co[frozenset] = co[FrozenSetSubscriptable] # For 3.7/3.8 compatibility.
co[frozenset] = co[FrozenSetSubscriptable] # For 3.8 compatibility.

# abc.Sequence overrides, if defined, can apply to MutableSequences, lists and
# tuples
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/v.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def format_exception(exc: BaseException, type: Union[type, None]) -> str:
res = f"invalid value for type, expected {tn}"
elif isinstance(exc, ForbiddenExtraKeysError):
res = f"extra fields found ({', '.join(exc.extra_fields)})"
elif isinstance(exc, AttributeError) and exc.args[0].endswith( # noqa: SIM114
elif isinstance(exc, AttributeError) and exc.args[0].endswith(
"object has no attribute 'items'"
):
# This was supposed to be a mapping (and have .items()) but it something else.
Expand Down
3 changes: 1 addition & 2 deletions tests/_compat.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import sys

is_py37 = sys.version_info[:2] == (3, 7)
is_py38 = sys.version_info[:2] == (3, 8)
is_py39_plus = sys.version_info >= (3, 9)
is_py310_plus = sys.version_info >= (3, 10)
is_py311_plus = sys.version_info >= (3, 11)

if is_py37 or is_py38:
if is_py38:
from typing import Dict, List

List_origin = List
Expand Down
3 changes: 0 additions & 3 deletions tests/strategies/test_native_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from cattrs import BaseConverter
from cattrs.strategies import configure_union_passthrough

from .._compat import is_py37


def test_only_primitives(converter: BaseConverter) -> None:
"""A native union with only primitives works."""
Expand All @@ -30,7 +28,6 @@ def test_only_primitives(converter: BaseConverter) -> None:
converter.structure((), union)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
def test_literals(converter: BaseConverter) -> None:
"""A union with primitives and literals works."""
from typing import Literal
Expand Down
2 changes: 1 addition & 1 deletion tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_forbid_extra_keys(cls_and_vals):
cl, vals, kwargs = cls_and_vals
inst = cl(*vals, **kwargs)
unstructured = converter.unstructure(inst)
bad_key = list(unstructured)[0] + "A" if unstructured else "Hyp"
bad_key = next(iter(unstructured)) + "A" if unstructured else "Hyp"
while bad_key in unstructured:
bad_key += "A"
unstructured[bad_key] = 1
Expand Down
6 changes: 0 additions & 6 deletions tests/test_structure_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from cattrs.converters import BaseConverter, Converter

from ._compat import is_py37
from .untyped import simple_classes


Expand Down Expand Up @@ -137,7 +136,6 @@ def dis(obj, _):
assert inst == converter.structure(converter.unstructure(inst), Union[cl_a, cl_b])


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_literal(converter_cls):
"""Structuring a class with a literal field works."""
Expand All @@ -154,7 +152,6 @@ class ClassWithLiteral:
) == ClassWithLiteral(4)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_literal_enum(converter_cls):
"""Structuring a class with a literal field works."""
Expand All @@ -175,7 +172,6 @@ class ClassWithLiteral:
) == ClassWithLiteral(Foo.FOO)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_literal_multiple(converter_cls):
"""Structuring a class with a literal field works."""
Expand Down Expand Up @@ -211,7 +207,6 @@ class ClassWithLiteral:
assert isinstance(cwl.literal_field, Bar)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_literal_error(converter_cls):
"""Structuring a class with a literal field can raise an error."""
Expand All @@ -227,7 +222,6 @@ class ClassWithLiteral:
converter.structure({"literal_field": 3}, ClassWithLiteral)


@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
def test_structure_literal_multiple_error(converter_cls):
"""Structuring a class with a literal field can raise an error."""
Expand Down
2 changes: 1 addition & 1 deletion tests/test_unstructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,4 @@ def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type: Type):
inputs = seq_type(cl(*vals, **kwargs) for cl, vals, kwargs in cls_and_vals)
outputs = converter.unstructure(inputs)
assert type(outputs) == seq_type
assert all(type(e) is dict for e in outputs)
assert all(type(e) is dict for e in outputs) # noqa: E721
2 changes: 1 addition & 1 deletion tests/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import (
Any,
Dict,
Final,
FrozenSet,
List,
MutableSequence,
Expand Down Expand Up @@ -46,7 +47,6 @@
text,
tuples,
)
from typing_extensions import Final

from .untyped import gen_attr_names, make_class

Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Keep docs in sync with docs env and .readthedocs.yml.
[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.11: py311, lint
3.12: py312
pypy-3: pypy3

[tox]
envlist = pypy3, py37, py38, py39, py310, py311, lint
envlist = pypy3, py38, py39, py310, py311, py312, lint
isolated_build = true
skipsdist = true

Expand Down