-
-
Notifications
You must be signed in to change notification settings - Fork 224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
type Serializer
as generic
#374
Conversation
Hmm, mypy somehow knows that from itsdangerous import Serializer
s = Serializer("secret")
data = s.dumps("test")
reveal_type(data) Mypy reveals |
dba89a7
to
01001c6
Compare
OK, figured out how to get mypy and pyright to understand that the default is |
This doesn't quite make things nice for typing users, as now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already looks pretty good, it's mostly some subtle variance issues, that weren't quite right.
@@ -10,13 +10,20 @@ | |||
from .signer import _make_keys_list | |||
from .signer import Signer | |||
|
|||
_TAnyStr = t.TypeVar("_TAnyStr", str, bytes, covariant=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_TAnyStr = t.TypeVar("_TAnyStr", str, bytes, covariant=True) |
I would get rid of this, just use t.AnyStr
, this actually needs to be invariant. Alternatively you could replace it with a typing_extensions.TypeVar
and set default=str
instead of covariant=True
, that may lead to more robust results compared to relying on just the overloads on __init__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got errors from mypy that it had to be covariant for class Serializer(t.Generic[t.AnyStr])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was because you weren't using it for the payload
argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After testing again I'm not getting a covariance error from mypy anymore when using AnyStr
, not sure what was going on before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Protocol
mypy does a variance calculation and gives an error if the variance doesn't match. Since you previously only used _TAnyStr
in return type annotations it was calculated as covariant, as soon as you added an argument that uses the same TypeVar
the variance calculation changed to invariant.
That's why it's sometimes dangerous to re-use the same TypeVar
between a Protocol
and a Generic
, since the variance may be wrong for the Generic
, but mypy does not do a variance calculation for Generic
. PEP-695 should make this less of a problem in the future, since type vars will generally be scoped to the generic itself, rather than in global scope and all they use auto-variance by default, so you don't have to think about it.
class _PDataSerializer(t.Protocol[_TAnyStr]): | ||
def loads(self, payload: str | bytes) -> t.Any: ... | ||
def dumps(self, obj: t.Any, **kwargs: t.Any) -> _TAnyStr: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class _PDataSerializer(t.Protocol[_TAnyStr]): | |
def loads(self, payload: str | bytes) -> t.Any: ... | |
def dumps(self, obj: t.Any, **kwargs: t.Any) -> _TAnyStr: ... | |
class _PDataSerializer(t.Protocol[t.AnyStr]): | |
def loads(self, payload: t.AnyStr, /) -> t.Any: ... | |
def dumps(self, obj: object, /) -> t.AnyStr: ... |
You are providing an upper bound here, so you don't want to make the argument list too strict.
For loads
you can't use str | bytes
because parameters are contravariant, so that would require each serializer to accept both str
and bytes
. Exactly because parameters are contravariant you can satisfy a function that expects str
with a supertype of str
, such as a union with bytes
.
As for dumps
, IIRC unless you specifically use *args: Any, **kwargs: Any
neither mypy nor pyright will treat this as a gradual signature, that allows serializers with arbitrary extra arguments, it will instead require each serializer function to take arbitrary arguments. This means you will have to ignore a type error when you pass in serializer_kwargs
, but that's a hit you will have to take until we have something like a TypeVarMapping
to pass just kwargs
along, ParamSpec
doesn't really work here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, after making these changes mypy does accept json
and pickle
as valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pyright is still not happy though. It still insists that modules do not satisfy the protocol.
def dumps(self, obj: t.Any, **kwargs: t.Any) -> _TAnyStr: ... | ||
|
||
|
||
def is_text_serializer(serializer: _PDataSerializer[t.Any]) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could make this a TypeGuard
, although it's probably better to wait for TypeIs
support to arrive, since that will yield better results. Although you could make it TypeGuard[_PDataSerializer[str]]
for now and add a comment to change it to TypeIs
later.
@@ -71,7 +78,7 @@ class Serializer: | |||
#: The default serialization module to use to serialize data to a | |||
#: string internally. The default is :mod:`json`, but can be changed | |||
#: to any object that provides ``dumps`` and ``loads`` methods. | |||
default_serializer: t.Any = json | |||
default_serializer: _PDataSerializer[_TAnyStr] = json # type: ignore[assignment] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default_serializer: _PDataSerializer[_TAnyStr] = json # type: ignore[assignment] | |
default_serializer: _PDataSerializer[str] = json |
This seems more sane to me, if you're worried about subclasses providing a bytes
serializer as default, you could change it to _PDataSerializer[t.Any]
or a very long-winded t.Union[_PDataSerializer[str], _PDataSerializer[bytes]]
. As a class attribute it shouldn't really be generic, that's prone to weird errors.
@@ -135,7 +174,9 @@ def secret_key(self) -> bytes: | |||
""" | |||
return self.secret_keys[-1] | |||
|
|||
def load_payload(self, payload: bytes, serializer: t.Any | None = None) -> t.Any: | |||
def load_payload( | |||
self, payload: bytes, serializer: _PDataSerializer[_TAnyStr] | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self, payload: bytes, serializer: _PDataSerializer[_TAnyStr] | None = None | |
self, payload: bytes, serializer: _PDataSerializer[t.Any] | None = None |
The type of the passed in serializer doesn't matter, since it is re-checked.
_TAnyStr = t.TypeVar("_TAnyStr", str, bytes, covariant=True) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_TAnyStr = t.TypeVar("_TAnyStr", str, bytes, covariant=True) |
Same thing here
@@ -166,7 +168,7 @@ def validate(self, signed_value: str | bytes, max_age: int | None = None) -> boo | |||
return False | |||
|
|||
|
|||
class TimedSerializer(Serializer): | |||
class TimedSerializer(Serializer[_TAnyStr]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class TimedSerializer(Serializer[_TAnyStr]): | |
class TimedSerializer(Serializer[t.AnyStr]): |
These variance issues are why you're getting complaints for |
Continued in #376. |
Serializer
now takes one generic parameter, eitherstr
orbytes
, to define whatdumps
returns rather than astr | bytes
union.The
default_serializer
attribute andserializer
argument are a generic protocol with the same parameter. The protocol requires an object that has aloads
method and adumps
method that returns the appropriate type.URLSafeSerizlizer
is typed to always returnstr
. This makes it more convenient as the user code no longer needs to do extra work to satisfy the type checker.fixes #347