itsdangerous icon indicating copy to clipboard operation
itsdangerous copied to clipboard

Consider making `Serializer` generic in `t.AnyStr` for type checking to avoid overly ambiguous return types

Open Daverball opened this issue 2 years ago • 0 comments

Currently Serializer.dumps has an ambiguous return type (str | bytes) due to how the Serializer determines its own return type in __init__ however for some of the derived classes such as URLSafeSerializer we know statically that the return type will be str and not bytes.

If Serializer were generic we could perform this type narrowing when we inherit from Serializer by instead inheriting from Serializer[str]. That way we don't need to type:ignore or assert isinstance(a, str) everywhere we use a URLSafeSerializer and correctly expect to get a str back from dumps.

If you wanted to be a bit more fancy you could even define a generic Protocol for the custom serializer that's passed in __init__ (rather than use Any) and create overloads to return the correct Serializer type, i.e. if dumps on the Serializer that's passed in returns bytes, then it will return Serializer[bytes], the default with None can then return Serializer[str].

i.e. something like this: (it would require a bit more care due to the order of arguments, to properly match every possible case, but the basic idea should come across)

class Serializer(_t.Generic[_t.AnyStr]):

   @_t.overload
    def __init__(
        self: Serializer[str],
        secret_key: _t_secret_key,
        salt: _t_opt_str_bytes = ...,
        serializer: _t.Optional[_SerializerProtocol[str]] = ...,
        serializer_kwargs: _t_opt_kwargs = ...,
        signer: _t.Optional[_t_signer] = ...,
        signer_kwargs: _t_opt_kwargs = ...,
        fallback_signers: _t.Optional[_t_fallbacks] = ...,
    ): ...

   @_t.overload
    def __init__(
        self: Serializer[bytes],
        secret_key: _t_secret_key,
        salt: _t_opt_str_bytes,
        serializer: _SerializerProtocol[bytes],
        serializer_kwargs: _t_opt_kwargs = ...,
        signer: _t.Optional[_t_signer] = ...,
        signer_kwargs: _t_opt_kwargs = ...,
        fallback_signers: _t.Optional[_t_fallbacks] = ...,
    ): ...

   # fallback for when the passed in serializer can't be pattern matched
   @_t.overload
    def __init__(
        self,
        secret_key: _t_secret_key,
        salt: _t_opt_str_bytes,
        serializer: Any,
        serializer_kwargs: _t_opt_kwargs = ...,
        signer: _t.Optional[_t_signer] = ...,
        signer_kwargs: _t_opt_kwargs = ...,
        fallback_signers: _t.Optional[_t_fallbacks] = ...,
    ): ...

Environment:

  • Python version: 3.10
  • ItsDangerous version: 2.1.2

Daverball avatar Jun 13 '23 12:06 Daverball