-
-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -10,13 +10,20 @@ | |||||||||||||
from .signer import _make_keys_list | ||||||||||||||
from .signer import Signer | ||||||||||||||
|
||||||||||||||
_TAnyStr = t.TypeVar("_TAnyStr", str, bytes, covariant=True) | ||||||||||||||
|
||||||||||||||
def is_text_serializer(serializer: t.Any) -> bool: | ||||||||||||||
|
||||||||||||||
class _PDataSerializer(t.Protocol[_TAnyStr]): | ||||||||||||||
def loads(self, payload: str | bytes) -> t.Any: ... | ||||||||||||||
def dumps(self, obj: t.Any, **kwargs: t.Any) -> _TAnyStr: ... | ||||||||||||||
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
You are providing an upper bound here, so you don't want to make the argument list too strict. For As for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, after making these changes mypy does accept There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 is_text_serializer(serializer: _PDataSerializer[t.Any]) -> bool: | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could make this a |
||||||||||||||
"""Checks whether a serializer generates text or binary.""" | ||||||||||||||
return isinstance(serializer.dumps({}), str) | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class Serializer: | ||||||||||||||
class Serializer(t.Generic[_TAnyStr]): | ||||||||||||||
"""A serializer wraps a :class:`~itsdangerous.signer.Signer` to | ||||||||||||||
enable serializing and securely signing data other than bytes. It | ||||||||||||||
can unsign to verify that the data hasn't been changed. | ||||||||||||||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This seems more sane to me, if you're worried about subclasses providing a |
||||||||||||||
|
||||||||||||||
#: The default ``Signer`` class to instantiate when signing data. | ||||||||||||||
#: The default is :class:`itsdangerous.signer.Signer`. | ||||||||||||||
|
@@ -82,11 +89,43 @@ class Serializer: | |||||||||||||
dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] | ||||||||||||||
] = [] | ||||||||||||||
|
||||||||||||||
# Tell type checkers that the default type is Serializer[str] if no | ||||||||||||||
# data serializer is provided. | ||||||||||||||
@t.overload | ||||||||||||||
def __init__( | ||||||||||||||
self: Serializer[str], | ||||||||||||||
secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], | ||||||||||||||
salt: str | bytes | None = b"itsdangerous", | ||||||||||||||
serializer: None = None, | ||||||||||||||
serializer_kwargs: dict[str, t.Any] | None = None, | ||||||||||||||
signer: type[Signer] | None = None, | ||||||||||||||
signer_kwargs: dict[str, t.Any] | None = None, | ||||||||||||||
fallback_signers: list[ | ||||||||||||||
dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] | ||||||||||||||
] | ||||||||||||||
| None = None, | ||||||||||||||
): ... | ||||||||||||||
|
||||||||||||||
@t.overload | ||||||||||||||
def __init__( | ||||||||||||||
self: Serializer[_TAnyStr], | ||||||||||||||
secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], | ||||||||||||||
salt: str | bytes | None = b"itsdangerous", | ||||||||||||||
serializer: _PDataSerializer[_TAnyStr] = ..., | ||||||||||||||
serializer_kwargs: dict[str, t.Any] | None = None, | ||||||||||||||
signer: type[Signer] | None = None, | ||||||||||||||
signer_kwargs: dict[str, t.Any] | None = None, | ||||||||||||||
fallback_signers: list[ | ||||||||||||||
dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] | ||||||||||||||
] | ||||||||||||||
| None = None, | ||||||||||||||
): ... | ||||||||||||||
|
||||||||||||||
def __init__( | ||||||||||||||
self, | ||||||||||||||
secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], | ||||||||||||||
salt: str | bytes | None = b"itsdangerous", | ||||||||||||||
serializer: t.Any = None, | ||||||||||||||
serializer: _PDataSerializer[_TAnyStr] | None = None, | ||||||||||||||
serializer_kwargs: dict[str, t.Any] | None = None, | ||||||||||||||
signer: type[Signer] | None = None, | ||||||||||||||
signer_kwargs: dict[str, t.Any] | None = None, | ||||||||||||||
|
@@ -111,7 +150,7 @@ def __init__( | |||||||||||||
if serializer is None: | ||||||||||||||
serializer = self.default_serializer | ||||||||||||||
|
||||||||||||||
self.serializer: t.Any = serializer | ||||||||||||||
self.serializer: _PDataSerializer[_TAnyStr] = serializer | ||||||||||||||
self.is_text_serializer: bool = is_text_serializer(serializer) | ||||||||||||||
|
||||||||||||||
if signer is None: | ||||||||||||||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The type of the passed in serializer doesn't matter, since it is re-checked. |
||||||||||||||
) -> t.Any: | ||||||||||||||
"""Loads the encoded object. This function raises | ||||||||||||||
:class:`.BadPayload` if the payload is not valid. The | ||||||||||||||
``serializer`` parameter can be used to override the serializer | ||||||||||||||
|
@@ -199,7 +240,7 @@ def iter_unsigners(self, salt: str | bytes | None = None) -> cabc.Iterator[Signe | |||||||||||||
for secret_key in self.secret_keys: | ||||||||||||||
yield fallback(secret_key, salt=salt, **kwargs) | ||||||||||||||
|
||||||||||||||
def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> str | bytes: | ||||||||||||||
def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> _TAnyStr: | ||||||||||||||
"""Returns a signed string serialized with the internal | ||||||||||||||
serializer. The return value can be either a byte or unicode | ||||||||||||||
string depending on the format of the internal serializer. | ||||||||||||||
|
@@ -208,9 +249,9 @@ def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> str | bytes: | |||||||||||||
rv = self.make_signer(salt).sign(payload) | ||||||||||||||
|
||||||||||||||
if self.is_text_serializer: | ||||||||||||||
return rv.decode("utf-8") | ||||||||||||||
return rv.decode("utf-8") # type: ignore[return-value] | ||||||||||||||
|
||||||||||||||
return rv | ||||||||||||||
return rv # type: ignore[return-value] | ||||||||||||||
|
||||||||||||||
def dump(self, obj: t.Any, f: t.IO[t.Any], salt: str | bytes | None = None) -> None: | ||||||||||||||
"""Like :meth:`dumps` but dumps into a file. The file handle has | ||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -17,6 +17,8 @@ | |||||
from .serializer import Serializer | ||||||
from .signer import Signer | ||||||
|
||||||
_TAnyStr = t.TypeVar("_TAnyStr", str, bytes, covariant=True) | ||||||
|
||||||
Comment on lines
+20
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Same thing here |
||||||
|
||||||
class TimestampSigner(Signer): | ||||||
"""Works like the regular :class:`.Signer` but also records the time | ||||||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
"""Uses :class:`TimestampSigner` instead of the default | ||||||
:class:`.Signer`. | ||||||
""" | ||||||
|
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 would get rid of this, just use
t.AnyStr
, this actually needs to be invariant. Alternatively you could replace it with atyping_extensions.TypeVar
and setdefault=str
instead ofcovariant=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 sameTypeVar
the variance calculation changed to invariant.That's why it's sometimes dangerous to re-use the same
TypeVar
between aProtocol
and aGeneric
, since the variance may be wrong for theGeneric
, but mypy does not do a variance calculation forGeneric
. 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.