From ebd71c5583c3127223b3cdef17ef0ae491e11a4d Mon Sep 17 00:00:00 2001 From: John Marsden Date: Fri, 12 Sep 2025 13:00:43 -0400 Subject: [PATCH 1/7] Improve type hinting of instance_of and or_ --- src/attr/validators.pyi | 53 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 3b38e5980..e6c1057a5 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -20,6 +20,9 @@ _T = TypeVar("_T") _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") _T3 = TypeVar("_T3") +_T4 = TypeVar("_T4") +_T5 = TypeVar("_T5") +_T6 = TypeVar("_T6") _I = TypeVar("_I", bound=Iterable) _K = TypeVar("_K") _V = TypeVar("_V") @@ -45,8 +48,10 @@ def instance_of( ) -> _ValidatorType[_T1 | _T2 | _T3]: ... @overload def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... + +_U = TypeVar("_U", bound=UnionType) @overload -def instance_of(type: UnionType) -> _ValidatorType[Any]: ... +def instance_of(type: _U) -> _ValidatorType[_U]: ... def optional( validator: ( _ValidatorType[_T] @@ -90,4 +95,48 @@ def not_( msg: str | None = None, exc_types: type[Exception] | Iterable[type[Exception]] = ..., ) -> _ValidatorType[_T]: ... -def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], + __v6: _ValidatorType[_T6], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ... +@overload +def or_( + __v1: _ValidatorType[Any], + __v2: _ValidatorType[Any], + __v3: _ValidatorType[Any], + __v4: _ValidatorType[Any], + __v5: _ValidatorType[Any], + __v6: _ValidatorType[Any], + *validators: _ValidatorType[Any], +) -> _ValidatorType[Any]: ... From 73e86c5454b73be22985bf8330ca7e781ca0c5c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:07:06 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/attr/validators.pyi | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index e6c1057a5..e3f0e9584 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -50,6 +50,7 @@ def instance_of( def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... _U = TypeVar("_U", bound=UnionType) + @overload def instance_of(type: _U) -> _ValidatorType[_U]: ... def optional( @@ -97,46 +98,46 @@ def not_( ) -> _ValidatorType[_T]: ... @overload def or_( - __v1: _ValidatorType[_T1], + __v1: _ValidatorType[_T1], __v2: _ValidatorType[_T2], ) -> _ValidatorType[_T1 | _T2]: ... @overload def or_( - __v1: _ValidatorType[_T1], - __v2: _ValidatorType[_T2], + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], __v3: _ValidatorType[_T3], ) -> _ValidatorType[_T1 | _T2 | _T3]: ... @overload def or_( - __v1: _ValidatorType[_T1], - __v2: _ValidatorType[_T2], - __v3: _ValidatorType[_T3], + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], __v4: _ValidatorType[_T4], ) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ... @overload def or_( - __v1: _ValidatorType[_T1], - __v2: _ValidatorType[_T2], - __v3: _ValidatorType[_T3], - __v4: _ValidatorType[_T4], + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], __v5: _ValidatorType[_T5], ) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ... @overload def or_( - __v1: _ValidatorType[_T1], - __v2: _ValidatorType[_T2], - __v3: _ValidatorType[_T3], - __v4: _ValidatorType[_T4], - __v5: _ValidatorType[_T5], + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], __v6: _ValidatorType[_T6], ) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ... @overload def or_( - __v1: _ValidatorType[Any], - __v2: _ValidatorType[Any], - __v3: _ValidatorType[Any], - __v4: _ValidatorType[Any], - __v5: _ValidatorType[Any], + __v1: _ValidatorType[Any], + __v2: _ValidatorType[Any], + __v3: _ValidatorType[Any], + __v4: _ValidatorType[Any], + __v5: _ValidatorType[Any], __v6: _ValidatorType[Any], *validators: _ValidatorType[Any], ) -> _ValidatorType[Any]: ... From 9f31b17e782e039acdda5ce1bb97142da9f14637 Mon Sep 17 00:00:00 2001 From: John Marsden Date: Fri, 12 Sep 2025 13:10:36 -0400 Subject: [PATCH 3/7] Moved _U to same location as all other TypeVar definitions Added a new TypeVar '_U' bound to UnionType for type validation. --- src/attr/validators.pyi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index e3f0e9584..63d705963 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -27,6 +27,7 @@ _I = TypeVar("_I", bound=Iterable) _K = TypeVar("_K") _V = TypeVar("_V") _M = TypeVar("_M", bound=Mapping) +_U = TypeVar("_U", bound=UnionType) def set_disabled(run: bool) -> None: ... def get_disabled() -> bool: ... @@ -48,9 +49,6 @@ def instance_of( ) -> _ValidatorType[_T1 | _T2 | _T3]: ... @overload def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... - -_U = TypeVar("_U", bound=UnionType) - @overload def instance_of(type: _U) -> _ValidatorType[_U]: ... def optional( From 81f8343eca04c1cc1368526520ae905f8d0a7e9e Mon Sep 17 00:00:00 2001 From: John Marsden Date: Fri, 12 Sep 2025 13:21:49 -0400 Subject: [PATCH 4/7] Revert to UnionType --- src/attr/validators.pyi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 63d705963..36a7e800c 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -27,7 +27,6 @@ _I = TypeVar("_I", bound=Iterable) _K = TypeVar("_K") _V = TypeVar("_V") _M = TypeVar("_M", bound=Mapping) -_U = TypeVar("_U", bound=UnionType) def set_disabled(run: bool) -> None: ... def get_disabled() -> bool: ... @@ -50,7 +49,7 @@ def instance_of( @overload def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... @overload -def instance_of(type: _U) -> _ValidatorType[_U]: ... +def instance_of(type: UnionType) -> _ValidatorType[Any]: ... def optional( validator: ( _ValidatorType[_T] From c452f1cafdfd900794347691a97fb882acecd23e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 28 Sep 2025 17:39:28 +0200 Subject: [PATCH 5/7] Add (failing) typing example --- typing-examples/baseline_examples.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/typing-examples/baseline_examples.py b/typing-examples/baseline_examples.py index 6372a15df..6f855b468 100644 --- a/typing-examples/baseline_examples.py +++ b/typing-examples/baseline_examples.py @@ -93,6 +93,17 @@ class Validated: num: int = attrs.field(validator=attrs.validators.ge(0)) +@attrs.define +class ValidatedInconsistentOr: + num: int = attrs.field( + validator=attrs.validators.or_( + # Various types of validators. + attrs.validators.ge(0), + attrs.validators.instance_of(object), + ) + ) + + attrs.validators.set_disabled(True) attrs.validators.set_disabled(False) From 7dcda39de3c60c3fced5dc853afc5266387bb4e8 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 28 Sep 2025 17:47:13 +0200 Subject: [PATCH 6/7] Oops, the type checkers were just smarter --- typing-examples/baseline_examples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typing-examples/baseline_examples.py b/typing-examples/baseline_examples.py index 6f855b468..0b20faf32 100644 --- a/typing-examples/baseline_examples.py +++ b/typing-examples/baseline_examples.py @@ -95,11 +95,11 @@ class Validated: @attrs.define class ValidatedInconsistentOr: - num: int = attrs.field( + num: int | str = attrs.field( validator=attrs.validators.or_( # Various types of validators. attrs.validators.ge(0), - attrs.validators.instance_of(object), + attrs.validators.instance_of(str), ) ) From 8f78ef5914e97c37846d0daa4e4f52d6af9c106a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 28 Sep 2025 18:05:13 +0200 Subject: [PATCH 7/7] Add news fragment --- changelog.d/1474.change.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1474.change.md diff --git a/changelog.d/1474.change.md b/changelog.d/1474.change.md new file mode 100644 index 000000000..351257b2d --- /dev/null +++ b/changelog.d/1474.change.md @@ -0,0 +1,3 @@ +The type annotation for `attrs.validators.or_()` now allows for different types of validators. + +This was only an issue on Pyright.