Reputation: 83
I am having trouble annotating attrs class attribute.
I am using NewType for defining new UserId type and attrs frozen classes.
This is code where mypy doesn't complain and everything is alright:
from typing import NewType
from attr import frozen, field
UserId = NewType("UserId", str)
@frozen
class Order:
id: UserId = field()
mypy does not have any issues when checking this code. The problem appears after using validators from attrs.
from typing import NewType
from attr import frozen, field, validators
UserId = NewType("UserId", str)
@frozen
class Order:
id: UserId = field(validator=validators.matches_re("^\d+$"))
mypy now complains about incorrect types:
project/test_annotation.py:10: error: Incompatible types in assignment (expression has type "str", variable has type "UserId") [assignment]
Found 1 error in 1 file (checked 1 source file)
I don't understand how field() returns string type right now.
Can somebody explain that? Also, how can we work around this problem?
env:
Python 3.10.6
attrs==22.1.0
cattrs==22.2.0
Upvotes: 0
Views: 1037
Reputation: 7933
To make it happy, cast
. field
is considerably complicated, your field
returns a str
thanks to this overload:
... # other overloads
# This form catches an explicit None or no default and infers the type from the
# other arguments.
@overload
def field(
*,
default: None = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: _ReprArgType = ...,
hash: Optional[bool] = ...,
init: bool = ...,
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> _T: ...
It basically says "when validators
is a [sequence of or one of] validators working on some type T
, then field
returns T
".
So, you pass a validator that works on str
, and thus field
type is str
as well. NewType("UserID", str)
is not a subtype of str
, so this assignment fails. You have two major options:
cast to desired type:
from typing import cast
...
@frozen
class Order:
id: UserId = cast(str, field(validator=validators.matches_re("^\d+$")))
create your own validator. You don't need to replicate the logic, only change the signature and call original implementation with type: ignore
or casts.
from typing import TYPE_CHECKING, Callable, Match, Optional, Pattern, Union, cast
if TYPE_CHECKING:
from attr import _ValidatorType
def userid_matches_re(
regex: Union[Pattern[str], str],
flags: int = ...,
func: Optional[
Callable[[str, str, int], Optional[Match[str]]]
] = ...,
) -> '_ValidatorType[UserID]':
return cast('_ValidatorType[UserID]', validators.matches_re(regex, flags, func))
... and use it in your class instead of validators.matches_re
. The signature above is stolen from stubs with AnyStr
replaced with str
, because you don't allow bytes
anyway.
I'd recommend the first variant, because another solution is just more boilerplate with the same cast
as a result, it gives you no more safety. However, it may be viable if you have many fields with this validator.
Upvotes: 1