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

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
fail-fast: false

steps:
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- uses: hynek/setup-cached-uv@757bedc3f972eb7227a1aa657651f15a8527c817 # v2.3.0
- name: Run zizmor 🌈
run: uvx zizmor --format sarif . > results.sarif
env:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Now you can make your changes locally.
```shell
$ just lint
$ just test
$ just --set python python3.9 test # Test on other versions
$ just --set python python3.10 test # Test on other versions
```

6. Write any necessary documentation, including updating the changelog (HISTORY.md). The docs can be built like so:
Expand Down
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python

- Fix structuring of nested generic classes with stringified annotations.
([#688](https://github.com/python-attrs/cattrs/pull/688))
- Python 3.9 is no longer supported, as it is end-of-life. Use previous versions on this Python version.
([#698](https://github.com/python-attrs/cattrs/pull/698))

## 25.3.0 (2025-10-07)

Expand Down
8 changes: 4 additions & 4 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ sync version="":
uv sync {{ if python != '' { '-p ' + python } else if version != '' { '-p ' + version } else { '' } }} --all-groups --all-extras

lint:
uv run -p python3.13 --group lint ruff check src/ tests bench
uv run -p python3.13 --group lint black --check src tests docs/conf.py
uv run -p python3.14 --group lint ruff check src/ tests bench
uv run -p python3.14 --group lint black --check src tests docs/conf.py

test *args="-x --ff -n auto tests":
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test --group lint pytest {{args}}

testall:
just python=python3.9 test
just python=python3.10 test
just python=pypy3.10 test
just python=python3.11 test
just python=python3.12 test
just python=python3.13 test
just python=python3.14 test

cov *args="-x --ff -n auto tests":
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test --group lint coverage run -m pytest {{args}}
Expand All @@ -27,12 +27,12 @@ cov *args="-x --ff -n auto tests":
{{ if covcleanup == "true" { "@rm .coverage*" } else { "" } }}

covall:
just python=python3.9 covcleanup=false cov
just python=python3.10 covcleanup=false cov
just python=pypy3.10 covcleanup=false cov
just python=python3.11 covcleanup=false cov
just python=python3.12 covcleanup=false cov
just python=python3.13 covcleanup=false cov
just python=python3.14 covcleanup=false cov
uv run coverage combine
uv run coverage report
@rm .coverage*
Expand Down
4 changes: 0 additions & 4 deletions docs/indepth.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ The new copy may be changed through the `copy` arguments, but will retain all ma

## Customizing Collection Unstructuring

```{important}
This feature is supported for Python 3.9 and later.
```

```{tip}
See [](customizing.md#customizing-collections) for a more modern and more powerful way of customizing collection handling.
```
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,14 @@ dependencies = [
"typing-extensions>=4.14.0",
"exceptiongroup>=1.1.1; python_version < '3.11'",
]
requires-python = ">=3.9"
requires-python = ">=3.10"
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.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -152,10 +151,14 @@ ignore = [
"SIM300", # Yoda rocks in asserts
"PGH003", # leave my type: ignores alone
"B006", # mutable argument defaults
"B905", # zip strictness, too noisy
"DTZ001", # datetimes in tests
"DTZ006", # datetimes in tests
"UP006", # We support old typing constructs at runtime
"UP007", # We support old typing constructs at runtime
"UP035", # We support old typing constructs at runtime
"UP038", # Dubious rule
"UP045", # We support old typing constructs at runtime
]

[tool.ruff.lint.pyupgrade]
Expand Down
28 changes: 2 additions & 26 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,7 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet

signature = _signature

if sys.version_info >= (3, 10):
signature = partial(_signature, eval_str=True)
signature = partial(_signature, eval_str=True)


try:
Expand Down Expand Up @@ -269,7 +266,7 @@ def get_newtype_base(typ: Any) -> Optional[type]:

from typing import NotRequired, Required

elif sys.version_info >= (3, 10):
else:
from typing import _UnionGenericAlias

def is_union_type(obj):
Expand All @@ -291,27 +288,6 @@ def get_newtype_base(typ: Any) -> Optional[type]:
else:
from typing_extensions import NotRequired, Required

else:
# 3.9
from typing import _UnionGenericAlias

from typing_extensions import NotRequired, Required

def is_union_type(obj):
return obj is Union or (
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
)

def get_newtype_base(typ: Any) -> Optional[type]:
supertype = getattr(typ, "__supertype__", None)
if (
supertype is not None
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
and typ.__module__ in ("typing", "typing_extensions")
):
return supertype
return None


def get_notrequired_base(type) -> Union[Any, NothingType]:
if is_annotated(type):
Expand Down
17 changes: 5 additions & 12 deletions src/cattrs/gen/typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,12 @@
import re
import sys
from collections.abc import Mapping
from inspect import get_annotations
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar

from attrs import NOTHING, Attribute
from typing_extensions import _TypedDictMeta

try:
from inspect import get_annotations

def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)

except ImportError:
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
def get_annots(cl) -> dict[str, Any]:
return cl.__dict__.get("__annotations__", {})


from .._compat import (
get_full_type_hints,
get_notrequired_base,
Expand Down Expand Up @@ -50,6 +39,10 @@ def get_annots(cl) -> dict[str, Any]:
T = TypeVar("T")


def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)


def make_dict_unstructure_fn(
cl: type[T],
converter: BaseConverter,
Expand Down
8 changes: 1 addition & 7 deletions src/cattrs/preconf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import sys
from datetime import datetime
from enum import Enum
from typing import Any, Callable, TypeVar, get_args
from typing import Any, Callable, ParamSpec, TypeVar, get_args

from .._compat import is_subclass
from ..converters import Converter, UnstructureHook
from ..fns import identity

if sys.version_info[:2] < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec


def validate_datetime(v, _):
if not isinstance(v, datetime):
Expand Down
2 changes: 0 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ def converter_cls(request):
settings.load_profile("fast" if environ.get("FAST") == "1" else "tests")

collect_ignore_glob = []
if sys.version_info < (3, 10):
collect_ignore_glob.append("*_604.py")
if sys.version_info < (3, 12):
collect_ignore_glob.append("*_695.py")
if platform.python_implementation() == "PyPy":
Expand Down
Loading
Loading