7

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

tetienne
  • 369
  • 3
  • 13
  • 1
    The documentation for `typing.overload` implies that the undecorated function shouldn't be typed; the type checker isn't supposed to consider it anyway. – chepner Jul 26 '19 at 14:50
  • but why do you want to use this? what are you trying to achieve – basilisk Jul 26 '19 at 14:50
  • @chepner Removing the types from the undecorated function does not sastify mypy: base/page_object.py:36: error: Function is missing a type annotation – tetienne Jul 26 '19 at 14:55
  • @basilisk PageObject can be build from different way, it depends where I am in the code. For instance, sometime I will have a by object, but no context. – tetienne Jul 26 '19 at 14:56
  • You might consider using distinct class methods for the different cases, rather than overloading `__init__`. – chepner Jul 26 '19 at 15:03
  • using overload like this looks ugly and not pythonic. I recommand you try another way and not stick to this. maybe using kwargs ? or Factory Pattern – basilisk Jul 26 '19 at 15:07
  • @basilisk you are probably right. It's more Java like. I've still had some trouble to think pythonic. – tetienne Jul 26 '19 at 15:13
  • 1
    Another observation from the documentation: the same *number* of arguments is used in each; only the *types* differ from one overloaded method to the next. – chepner Jul 26 '19 at 15:16
  • take a look at the Factory Pattern. if you are familiar with java it's same concept. it will be cleaner ti implement it, or you can give kwargs (keyword arguments) as arguments and make different objects based on that. take a look here https://medium.com/@mrfksiv/python-design-patterns-03-the-factory-86cb351c68b0 and here : https://stackoverflow.com/questions/21660834/why-cant-pass-args-and-kwargs-in-init-of-a-child-class/21661243 . hope it helps ;) – basilisk Jul 26 '19 at 15:24
  • @MichaelKolber The `typing` module also has an `overload` decorator, which is the one I assumed we were talking about. `overloading.py` might not be compatible with `mypy`. – chepner Jul 26 '19 at 15:32
  • (Specifically, `typing.overload` simply returns a function that raises at runtime, ensuring that an undecorated implementation must be supplied. `overloading.py` appears to merge multiple overloaded functions into one "actual" function instead.) – chepner Jul 26 '19 at 15:35
  • @chepner Good point – Michael Kolber Jul 26 '19 at 16:04

2 Answers2

12

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.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • Works perfectly! This way I still have a clear definition of my constructors and keep the main one. – tetienne Jul 29 '19 at 07:35
0

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

Maks
  • 1,527
  • 1
  • 13
  • 16
  • This doesn't address the question: the question is asking about functions which can take a different number of arguments, and/or parameters that are named differently. In your example (which is also discussed at greater length in PEP 484), the number of arguments and their names are the same in all cases. – orthocresol Sep 17 '21 at 15:19
  • While this answer does not address exactly the question asked, it's a useful answer to very similar questions. – joanis Sep 17 '21 at 19:35