From 6c89173db8c9fe451b52a61f04be6a3664dfa582 Mon Sep 17 00:00:00 2001 From: Filipe Brandenburger Date: Tue, 26 Nov 2024 21:18:13 -0800 Subject: [PATCH] Fix type annotations for converter types, considering the Converter class (#1373) * Fix type annotations for converter types, considering the Converter class Also consider that field() or attr.ib() takes a list or tuple of converters as an implicit pipe, add type annotations for that syntax * Fix expected mypy output with bad converters now that typing information has been expanded --- src/attr/__init__.pyi | 17 ++++++++++---- src/attr/converters.pyi | 2 +- src/attrs/__init__.pyi | 17 ++++++++++---- tests/test_mypy.yml | 8 +++---- tests/typing_example.py | 50 +++++++++++++++++++++++++---------------- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 2bbdc051e..47951eebc 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -130,7 +130,7 @@ class Attribute(Generic[_T]): order: _EqOrderType hash: bool | None init: bool - converter: _ConverterType | Converter[Any, _T] | None + converter: Converter | None metadata: dict[Any, Any] type: type[_T] | None kw_only: bool @@ -194,7 +194,10 @@ def attrib( init: bool = ..., metadata: Mapping[Any, Any] | None = ..., type: type[_T] | None = ..., - converter: _ConverterType | Converter[Any, _T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., factory: Callable[[], _T] | None = ..., kw_only: bool = ..., eq: _EqOrderType | None = ..., @@ -214,7 +217,10 @@ def attrib( init: bool = ..., metadata: Mapping[Any, Any] | None = ..., type: type[_T] | None = ..., - converter: _ConverterType | Converter[Any, _T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., factory: Callable[[], _T] | None = ..., kw_only: bool = ..., eq: _EqOrderType | None = ..., @@ -234,7 +240,10 @@ def attrib( init: bool = ..., metadata: Mapping[Any, Any] | None = ..., type: object = ..., - converter: _ConverterType | Converter[Any, _T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., factory: Callable[[], _T] | None = ..., kw_only: bool = ..., eq: _EqOrderType | None = ..., diff --git a/src/attr/converters.pyi b/src/attr/converters.pyi index 9ef478f21..f902e181d 100644 --- a/src/attr/converters.pyi +++ b/src/attr/converters.pyi @@ -10,4 +10,4 @@ def optional(converter: _ConverterType) -> _ConverterType: ... def default_if_none(default: _T) -> _ConverterType: ... @overload def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... -def to_bool(val: str) -> bool: ... +def to_bool(val: str | int | bool) -> bool: ... diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi index b2670de21..c6902bdbe 100644 --- a/src/attrs/__init__.pyi +++ b/src/attrs/__init__.pyi @@ -51,7 +51,7 @@ _C = TypeVar("_C", bound=type) _EqOrderType = bool | Callable[[Any], Any] _ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] -_ConverterType = Callable[[Any], Any] +_ConverterType = Callable[[Any], Any] | Converter[Any, _T] _ReprType = Callable[[Any], str] _ReprArgType = bool | _ReprType _OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] @@ -94,7 +94,10 @@ def field( hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., - converter: _ConverterType | Converter[Any, _T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., factory: Callable[[], _T] | None = ..., kw_only: bool = ..., eq: _EqOrderType | None = ..., @@ -114,7 +117,10 @@ def field( hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., - converter: _ConverterType | Converter[Any, _T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., factory: Callable[[], _T] | None = ..., kw_only: bool = ..., eq: _EqOrderType | None = ..., @@ -134,7 +140,10 @@ def field( hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., - converter: _ConverterType | Converter[Any, _T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., factory: Callable[[], _T] | None = ..., kw_only: bool = ..., eq: _EqOrderType | None = ..., diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 6e5bf046c..8042ebb11 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -788,9 +788,9 @@ reveal_type(A) out: | main:15: error: Cannot determine __init__ type from converter [misc] - main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any] | Converter[Any, Never] | None" [arg-type] + main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any] | Converter[Any, Any] | list[Callable[[Any], Any] | Converter[Any, Any]] | tuple[Callable[[Any], Any] | Converter[Any, Any]] | None" [arg-type] main:16: error: Cannot determine __init__ type from converter [misc] - main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any] | Converter[Any, Never] | None" [arg-type] + main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any] | Converter[Any, Any] | list[Callable[[Any], Any] | Converter[Any, Any]] | tuple[Callable[[Any], Any] | Converter[Any, Any]] | None" [arg-type] main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingBadConverterReprocess @@ -816,9 +816,9 @@ reveal_type(A) out: | main:16: error: Cannot determine __init__ type from converter [misc] - main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any] | Converter[Any, Never] | None" [arg-type] + main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any] | Converter[Any, Any] | list[Callable[[Any], Any] | Converter[Any, Any]] | tuple[Callable[[Any], Any] | Converter[Any, Any]] | None" [arg-type] main:17: error: Cannot determine __init__ type from converter [misc] - main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any] | Converter[Any, Never] | None" [arg-type] + main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any] | Converter[Any, Any] | list[Callable[[Any], Any] | Converter[Any, Any]] | tuple[Callable[[Any], Any] | Converter[Any, Any]] | None" [arg-type] main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" - case: testAttrsUsingUnsupportedConverter diff --git a/tests/typing_example.py b/tests/typing_example.py index 2124912c8..cfeb95188 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -133,40 +133,52 @@ class AliasExample: attr.fields(AliasExample).without_alias.alias attr.fields(AliasExample)._with_alias.alias + # Converters -# XXX: Currently converters can only be functions so none of this works -# although the stubs should be correct. -# @attr.s -# class ConvCOptional: -# x: Optional[int] = attr.ib(converter=attr.converters.optional(int)) +@attr.s +class ConvCOptional: + x: int | None = attr.ib(converter=attr.converters.optional(int)) -# ConvCOptional(1) -# ConvCOptional(None) +ConvCOptional(1) +ConvCOptional(None) + +# XXX: Fails with E: Unsupported converter, only named functions, types and lambdas are currently supported [misc] +# See https://github.com/python/mypy/issues/15736 +# +# @attr.s +# class ConvCPipe: +# x: str = attr.ib(converter=attr.converters.pipe(int, str)) +# +# +# ConvCPipe(3.4) +# ConvCPipe("09") +# +# # @attr.s # class ConvCDefaultIfNone: # x: int = attr.ib(converter=attr.converters.default_if_none(42)) - - +# +# # ConvCDefaultIfNone(1) # ConvCDefaultIfNone(None) -# @attr.s -# class ConvCToBool: -# x: int = attr.ib(converter=attr.converters.to_bool) +@attr.s +class ConvCToBool: + x: int = attr.ib(converter=attr.converters.to_bool) -# ConvCToBool(1) -# ConvCToBool(True) -# ConvCToBool("on") -# ConvCToBool("yes") -# ConvCToBool(0) -# ConvCToBool(False) -# ConvCToBool("n") +ConvCToBool(1) +ConvCToBool(True) +ConvCToBool("on") +ConvCToBool("yes") +ConvCToBool(0) +ConvCToBool(False) +ConvCToBool("n") # Validators