Skip to content

Commit c447dfe

Browse files
authored
Provisionally enable 3.12 (#424)
* Provisionally enable 3.12 * Maybe fix? * Remove 3.7 * Update history
1 parent 7305bb7 commit c447dfe

17 files changed

+463
-631
lines changed

.github/workflows/main.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
strategy:
1717
matrix:
18-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
18+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.8"]
1919
fail-fast: false
2020

2121
steps:
@@ -24,6 +24,7 @@ jobs:
2424
- uses: "actions/setup-python@v4"
2525
with:
2626
python-version: "${{ matrix.python-version }}"
27+
allow-prereleases: true
2728

2829
- name: "Run Tox"
2930
run: |

HISTORY.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- **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.
99
If you're using these functions directly, the old behavior can be restored by passing in the desired values directly.
1010
([#410](https://github.com/python-attrs/cattrs/issues/410) [#411](https://github.com/python-attrs/cattrs/pull/411))
11+
- Python 3.12 is now supported. Python 3.7 is no longer supported; use older releases there.
12+
([#424](https://github.com/python-attrs/cattrs/pull/424))
1113
- Introduce the `use_class_methods` strategy. Learn more [here](https://catt.rs/en/latest/strategies.html#using-class-specific-structure-and-unstructure-methods).
1214
([#405](https://github.com/python-attrs/cattrs/pull/405))
1315
- 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).

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ destructure them.
108108

109109
- Free software: MIT license
110110
- Documentation: https://catt.rs
111-
- 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.)
111+
- Python versions supported: 3.8 and up. (Older Python versions are supported by older versions; see the changelog.)
112112

113113
## Features
114114

docs/structuring.md

-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ Generic TypedDicts work on Python 3.11 and later, since that is the first Python
260260

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

263-
On Python 3.7, using `typing_extensions.TypedDict` is required since `typing.TypedDict` doesn't exist there.
264263
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.
265264

266265
[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), structuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_structure_fn`.

docs/unstructuring.md

-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ False
5252

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

55-
On Python 3.7, using `typing_extensions.TypedDict` is required since `typing.TypedDict` doesn't exist there.
5655
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.
5756

5857
[Similar to _attrs_ classes](customizing.md#using-cattrsgen-generators), unstructuring can be customized using {meth}`cattrs.gen.typeddicts.make_dict_unstructure_fn`.

pdm.lock

+424-573
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+15-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ test = [
1919
"hypothesis>=6.79.4",
2020
"pytest>=7.4.0",
2121
"pytest-benchmark>=4.0.0",
22-
"immutables>=0.19",
22+
"immutables>=0.20",
2323
"typing-extensions>=4.7.1",
2424
"coverage>=7.2.7",
2525
]
@@ -53,10 +53,23 @@ dependencies = [
5353
"typing-extensions>=4.1.0, !=4.6.3; python_version < '3.11'",
5454
"exceptiongroup>=1.1.1; python_version < '3.11'",
5555
]
56-
requires-python = ">=3.7"
56+
requires-python = ">=3.8"
5757
readme = "README.md"
5858
license = {text = "MIT"}
5959
keywords = ["attrs", "serialization", "dataclasses"]
60+
classifiers = [
61+
"Development Status :: 5 - Production/Stable",
62+
"Intended Audience :: Developers",
63+
"License :: OSI Approved :: MIT License",
64+
"Programming Language :: Python :: 3.8",
65+
"Programming Language :: Python :: 3.9",
66+
"Programming Language :: Python :: 3.10",
67+
"Programming Language :: Python :: 3.11",
68+
"Programming Language :: Python :: 3.12",
69+
"Programming Language :: Python :: Implementation :: CPython",
70+
"Programming Language :: Python :: Implementation :: PyPy",
71+
"Typing :: Typed",
72+
]
6073

6174
[project.urls]
6275
Homepage = "https://catt.rs"

src/cattrs/_compat.py

+10-33
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
from dataclasses import fields as dataclass_fields
77
from dataclasses import is_dataclass
88
from typing import AbstractSet as TypingAbstractSet
9-
from typing import Any, Deque, Dict, FrozenSet, List
9+
from typing import Any, Deque, Dict, Final, FrozenSet, List
1010
from typing import Mapping as TypingMapping
1111
from typing import MutableMapping as TypingMutableMapping
1212
from typing import MutableSequence as TypingMutableSequence
1313
from typing import MutableSet as TypingMutableSet
14-
from typing import NewType, Optional
14+
from typing import NewType, Optional, Protocol
1515
from typing import Sequence as TypingSequence
1616
from typing import Set as TypingSet
17-
from typing import Tuple, get_type_hints
17+
from typing import Tuple, get_args, get_origin, get_type_hints
1818

19-
from attr import NOTHING, Attribute, Factory
20-
from attr import fields as attrs_fields
21-
from attr import resolve_types
19+
from attrs import NOTHING, Attribute, Factory
20+
from attrs import fields as attrs_fields
21+
from attrs import resolve_types
2222

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

@@ -27,18 +27,6 @@
2727
except ImportError:
2828
ExtensionsTypedDict = None
2929

30-
if sys.version_info >= (3, 8):
31-
from typing import Final, Protocol, get_args, get_origin
32-
33-
else:
34-
35-
def get_args(cl):
36-
return cl.__args__
37-
38-
def get_origin(cl):
39-
return getattr(cl, "__origin__", None)
40-
41-
from typing_extensions import Final, Protocol
4230

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

357345
from collections import Counter as ColCounter
358-
from typing import Counter, Union, _GenericAlias
346+
from typing import Counter, TypedDict, Union, _GenericAlias
359347

360348
from typing_extensions import Annotated, NotRequired, Required
361349
from typing_extensions import get_origin as te_get_origin
362350

363-
if sys.version_info >= (3, 8):
364-
from typing import TypedDict
365-
else:
366-
TypedDict = ExtensionsTypedDict
367-
368351
def is_annotated(type) -> bool:
369352
return te_get_origin(type) is Annotated
370353

@@ -440,16 +423,10 @@ def is_counter(type):
440423
or getattr(type, "__origin__", None) is ColCounter
441424
)
442425

443-
if sys.version_info >= (3, 8):
444-
from typing import Literal
445-
446-
def is_literal(type) -> bool:
447-
return type.__class__ is _GenericAlias and type.__origin__ is Literal
426+
from typing import Literal
448427

449-
else:
450-
# No literals in 3.7.
451-
def is_literal(_) -> bool:
452-
return False
428+
def is_literal(type) -> bool:
429+
return type.__class__ is _GenericAlias and type.__origin__ is Literal
453430

454431
def is_generic(obj):
455432
return isinstance(obj, _GenericAlias)

src/cattrs/converters.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ def __init__(
819819
if OriginAbstractSet in co:
820820
if OriginMutableSet not in co:
821821
co[OriginMutableSet] = co[OriginAbstractSet]
822-
co[AbcMutableSet] = co[OriginAbstractSet] # For 3.7/3.8 compatibility.
822+
co[AbcMutableSet] = co[OriginAbstractSet] # For 3.8 compatibility.
823823
if FrozenSetSubscriptable not in co:
824824
co[FrozenSetSubscriptable] = co[OriginAbstractSet]
825825

@@ -828,7 +828,7 @@ def __init__(
828828
co[set] = co[OriginMutableSet]
829829

830830
if FrozenSetSubscriptable in co:
831-
co[frozenset] = co[FrozenSetSubscriptable] # For 3.7/3.8 compatibility.
831+
co[frozenset] = co[FrozenSetSubscriptable] # For 3.8 compatibility.
832832

833833
# abc.Sequence overrides, if defined, can apply to MutableSequences, lists and
834834
# tuples

src/cattrs/v.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def format_exception(exc: BaseException, type: Union[type, None]) -> str:
4141
res = f"invalid value for type, expected {tn}"
4242
elif isinstance(exc, ForbiddenExtraKeysError):
4343
res = f"extra fields found ({', '.join(exc.extra_fields)})"
44-
elif isinstance(exc, AttributeError) and exc.args[0].endswith( # noqa: SIM114
44+
elif isinstance(exc, AttributeError) and exc.args[0].endswith(
4545
"object has no attribute 'items'"
4646
):
4747
# This was supposed to be a mapping (and have .items()) but it something else.

tests/_compat.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import sys
22

3-
is_py37 = sys.version_info[:2] == (3, 7)
43
is_py38 = sys.version_info[:2] == (3, 8)
54
is_py39_plus = sys.version_info >= (3, 9)
65
is_py310_plus = sys.version_info >= (3, 10)
76
is_py311_plus = sys.version_info >= (3, 11)
87

9-
if is_py37 or is_py38:
8+
if is_py38:
109
from typing import Dict, List
1110

1211
List_origin = List

tests/strategies/test_native_unions.py

-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
from cattrs import BaseConverter
1212
from cattrs.strategies import configure_union_passthrough
1313

14-
from .._compat import is_py37
15-
1614

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

3230

33-
@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
3431
def test_literals(converter: BaseConverter) -> None:
3532
"""A union with primitives and literals works."""
3633
from typing import Literal

tests/test_converter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def test_forbid_extra_keys(cls_and_vals):
120120
cl, vals, kwargs = cls_and_vals
121121
inst = cl(*vals, **kwargs)
122122
unstructured = converter.unstructure(inst)
123-
bad_key = list(unstructured)[0] + "A" if unstructured else "Hyp"
123+
bad_key = next(iter(unstructured)) + "A" if unstructured else "Hyp"
124124
while bad_key in unstructured:
125125
bad_key += "A"
126126
unstructured[bad_key] = 1

tests/test_structure_attrs.py

-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from cattrs.converters import BaseConverter, Converter
1313

14-
from ._compat import is_py37
1514
from .untyped import simple_classes
1615

1716

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

139138

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

156154

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

177174

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

213209

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

229224

230-
@pytest.mark.skipif(is_py37, reason="Not supported on 3.7")
231225
@pytest.mark.parametrize("converter_cls", [BaseConverter, Converter])
232226
def test_structure_literal_multiple_error(converter_cls):
233227
"""Structuring a class with a literal field can raise an error."""

tests/test_unstructure.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,4 @@ def test_seq_of_simple_classes_unstructure(cls_and_vals, seq_type: Type):
132132
inputs = seq_type(cl(*vals, **kwargs) for cl, vals, kwargs in cls_and_vals)
133133
outputs = converter.unstructure(inputs)
134134
assert type(outputs) == seq_type
135-
assert all(type(e) is dict for e in outputs)
135+
assert all(type(e) is dict for e in outputs) # noqa: E721

tests/typed.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import (
1313
Any,
1414
Dict,
15+
Final,
1516
FrozenSet,
1617
List,
1718
MutableSequence,
@@ -46,7 +47,6 @@
4647
text,
4748
tuples,
4849
)
49-
from typing_extensions import Final
5050

5151
from .untyped import gen_attr_names, make_class
5252

tox.ini

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Keep docs in sync with docs env and .readthedocs.yml.
22
[gh-actions]
33
python =
4-
3.7: py37
54
3.8: py38
65
3.9: py39
76
3.10: py310
87
3.11: py311, lint
8+
3.12: py312
99
pypy-3: pypy3
1010

1111
[tox]
12-
envlist = pypy3, py37, py38, py39, py310, py311, lint
12+
envlist = pypy3, py38, py39, py310, py311, py312, lint
1313
isolated_build = true
1414
skipsdist = true
1515

0 commit comments

Comments
 (0)