currently I am implementing a lot of subclasses that should implement a function, this is a minimal example of the current project.
In this case, I have a function that needs to call, to the login of a user, but depending of the implementation injected previously the application will use UserUP
or UserToken
Because in login_user
I have all data available for the two options, I set the params like follow.
from typing import Protocol, runtime_checkable
@runtime_checkable
class User(Protocol):
def login(self, **kwargs) -> None:
raise NotImplementedError
class UserUP(User):
def login(self, user: str, password:str, **kwargs) -> None:
print(user)
print(password)
class UserToken(User):
def login(self, token:str, **kwargs) -> None:
print(token)
def login_user(user: User) -> None:
user.login(user="user", password="password", token="Token")
login_user(UserUP())
login_user(UserToken())
The problem is that when I run mypy
I get errors like the following:
app.py:10: error: Signature of "login" incompatible with supertype "User" [override]
app.py:10: note: Superclass:
app.py:10: note: def login(self, **kwargs: Any) -> None
app.py:10: note: Subclass:
app.py:10: note: def login(self, user: str, password: str, **kwargs: Any) -> None
app.py:16: error: Signature of "login" incompatible with supertype "User" [override]
app.py:16: note: Superclass:
app.py:16: note: def login(self, **kwargs: Any) -> None
app.py:16: note: Subclass:
app.py:16: note: def login(self, token: str, **kwargs: Any) -> None
Of course, the signature is incompatible, but which alternatives do I have to implement things like this?
User.login
You need to expose which possible keywords can be used, and which type, in the main function.
from typing import Protocol, runtime_checkable
@runtime_checkable
class User(Protocol):
def login(self, *, user: str, password: str, token: str, **kwargs) -> None:
raise NotImplementedError
class UserUP(User):
def login(self, *, user: str, password: str, **kwargs) -> None:
print(user)
print(password)
class UserToken(User):
def login(self, *, token: str, **kwargs) -> None:
print(token)
def login_user(user: User) -> None:
user.login(user="user", password="password", token="Token")
login_user(UserUP())
login_user(UserToken())
Success: no issues found in 1 source file
SUTerliakov suggested another implementation in the comment section: https://mypy-play.net/?mypy=master&python=3.10&flags=strict&gist=358f3ceba80b1d8141e49d223e25495a
from typing import Any, Protocol, runtime_checkable, ParamSpec
_P = ParamSpec('_P')
@runtime_checkable
class User(Protocol[_P]):
def login(self, *args: _P.args, **kwargs: _P.kwargs) -> None:
raise NotImplementedError
class UserUP(User[str, str]):
def login(self, user: str, password: str, *args: Any, **kwargs: Any) -> None:
print(user)
print(password)
class UserToken(User[str]):
def login(self, token: str, *args: Any, **kwargs: Any) -> None:
print(token)
def login_user(user: User[Any]) -> None:
user.login(user="user", password="password")
login_user(UserUP())
login_user(UserToken())
This solution generate a valid mypy
report:
Success: no issues found in 1 source file
There is two caveats with this implementation:
mypy
, which means that you can create new implementation that require specific arguments without mypy
telling you that the protocol requires it:With the following code, you have a valid mypy
check and yet, a crash because token
is required by the UserToken.login
method.
def login_user(user: User) -> None:
user.login(user="user", password="password")
mypy result: Success: no issues found in 1 source file
Run result:
TypeError: UserToken.login() missing 1 required positional argument: 'token'
User
need specific arguments.The protocol protect further implementation. If the protocol doesn't state that a specific parameter need to be fulfilled to match any protocol implementation, you can't define an implementation requiring this parameter.
Let's remove the token
argument in our login_user
like previously:
def login_user(user: User) -> None:
user.login(user="user", password="password")
mypy result:
file.py:23: error: Missing named argument "token" for "login" of "User" [call-arg]
Found 1 error in 1 file (checked 1 source file)