Reputation: 379
I want to fully type my Python project. But I'm stuck with a constructor that can be called with different parameters.
I've tried to remove the type from the final constructor, I've tried to remove some constructor... but still, get the same issue.
class PageObject(ABC):
logger = logging.getLogger(__name__)
@overload
def __init__(self, driver: Driver) -> None:
...
@overload
def __init__(self, by: Tuple[By, str], driver: Driver) -> None:
...
@overload
def __init__(self, context: WebElement, driver: Driver) -> None:
...
@overload
def __init__(self, by: Tuple[By, str], parent: "PageObject") -> None:
...
@overload
def __init__(self, parent: "PageObject") -> None:
...
def __init__(
self,
by: Optional[Tuple[By, str]] = None,
context: Optional[WebElement] = None,
parent: Optional["PageObject"] = None,
driver: Optional[Driver] = None,
) -> None:
if by and context:
raise ValueError("You cannot provide a locator AND a context.")
# ...
When I run mypy I got the following errors:
base/page_object.py:36: error: Overloaded function implementation does not accept all possible arguments of signature 1
base/page_object.py:36: error: Overloaded function implementation does not accept all possible arguments of signature 2
base/page_object.py:36: error: Overloaded function implementation does not accept all possible arguments of signature 3
base/page_object.py:36: error: Overloaded function implementation does not accept all possible arguments of signature 4
base/page_object.py:36: error: Overloaded function implementation does not accept all possible arguments of signature 5
Upvotes: 8
Views: 4774
Reputation: 1639
Might be relevant to someone:
This works as expected see my example:
from typing import Any, Optional, overload, Union
@overload
def a(b: str, c: None) -> int:
...
@overload
def a(b: int, c: int) -> str:
...
def a(b: Any, c: Any) -> Any:
if isinstance(b, str):
return int(b)
if isinstance(b, int):
return str(b * c)
lalala = a('test', None) # ok
lala = a(2, 1) # ok
la = a('test', 'cooltest') # an error
l = a(True, False) # not an error ? I guess mypy treats booleans as ints here
m = a(bytes(123), bytes(123)) # an error
and Guido's answer to msg379769 here https://bugs.python.org/issue42169
Upvotes: -1
Reputation: 64188
Here is the problem. Suppose somebody tries running PageObject(Driver())
-- that is, we pass in a Driver
object as the first argument.
This matches your first overload and so would be type-checked by mypy. But what actually happens at runtime? The first runtime parameter is by
, so your Driver
object gets assigned to by
, not driver
. So now there's a mismatch between your types, since by
is supposed to be of type Optional[Tuple[By, str]]
.
Probably the easiest workaround is to just forbid your users from using positional arguments altogether and mandate that they use only keyword arguments. You can do this like so:
class PageObject:
@overload
def __init__(self, *, driver: Driver) -> None:
...
@overload
def __init__(self, *, by: Tuple[By, str], driver: Driver) -> None:
...
@overload
def __init__(self, *, context: WebElement, driver: Driver) -> None:
...
@overload
def __init__(self, *, by: Tuple[By, str], parent: "PageObject") -> None:
...
@overload
def __init__(self, *, parent: "PageObject") -> None:
...
def __init__(
self,
*,
by: Optional[Tuple[By, str]] = None,
context: Optional[WebElement] = None,
parent: Optional["PageObject"] = None,
driver: Optional[Driver] = None,
) -> None:
...
Now, mypy typechecks this without an error, and doing PageObject(Driver())
is treated as an error both by mypy and by Python. Instead, you now need to do PageObject(driver=Driver())
.
If you do want to allow positional arguments, I'm afraid you'll need to redesign your code. Perhaps you can look into using staticmethods or classmethods or such so you can have different "flavors" of constructors -- basically, the factory pattern as suggested in the comments.
Upvotes: 14