tetienne
tetienne

Reputation: 379

Cannot guess why Overloaded function implementation does not accept all possible arguments

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

Answers (2)

Maks
Maks

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

Michael0x2a
Michael0x2a

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

Related Questions