Skip to main content

Protocol intersection for mypy

Project description

typing-protocol-intersection

tests & static analysis PyPI - Python Version

A tiny Python 3 package providing exactly one class - ProtocolIntersection (for Protocols themselves see PEP 544). Along with a mypy plugin this class allows to say that a function takes a parameter which implements multiple protocols or returns an object implementing multiple protocols without explicitly creating a new protocol class that inherits them. See the examples section below.

Installation

pip install typing-protocol-intersection 

Configuration

> cat mypy.ini
[mypy]
plugins = typing_protocol_intersection.mypy_plugin

Examples

Make sure to check the Recommended usage section after you familiarize yourself with the examples.

Simple example

from typing import Protocol
from typing_protocol_intersection import ProtocolIntersection

class HasX(Protocol):
    x: str

class HasY(Protocol):
    y: str

def foo(xy: ProtocolIntersection[HasX, HasY]) -> None:
    print(xy.x, xy.y)

Valid program

Here's a more complex example showing what you can write with the help of this mypy plugin:

from types import SimpleNamespace
from typing import Protocol, Generic, TypeVar, Dict

from typing_protocol_intersection import ProtocolIntersection

class HasX(Protocol):
    x: str

class HasY(Protocol):
    y: str

T = TypeVar("T")
class Builder(Generic[T]):
    def __init__(self) -> None:
        super().__init__()
        self._d: Dict[str, str] = {}

    def with_x(self) -> "Builder[ProtocolIntersection[T, HasX]]":
        self._d["x"] = "X"
        return self  # type: ignore

    def with_y(self) -> "Builder[ProtocolIntersection[T, HasY]]":
        self._d["y"] = "Y"
        return self  # type: ignore

    def build(self) -> T:
        return SimpleNamespace(**self._d)  # type: ignore

class DesiredObject(HasX, HasY, Protocol):
    pass

def get_x_y_1(o: DesiredObject) -> None:
    print(f"{o.x=}; {o.y=}")

def get_x_y_2(o: ProtocolIntersection[HasX, HasY]) -> None:
    print(f"{o.x=}; {o.y=}")

def main() -> None:
    valid_o = Builder().with_x().with_y().build()
    get_x_y_1(valid_o)
    get_x_y_2(valid_o)

if __name__ == "__main__":
    main()
> # without plugin
> mypy example.py
example.py:18:25: error: "ProtocolIntersection" expects no type arguments, but 2 given  [type-arg]
example.py:22:25: error: "ProtocolIntersection" expects no type arguments, but 2 given  [type-arg]
example.py:35:18: error: "ProtocolIntersection" expects no type arguments, but 2 given  [type-arg]
example.py:36:11: error: "ProtocolIntersection" has no attribute "x"  [attr-defined]
example.py:36:11: error: "ProtocolIntersection" has no attribute "y"  [attr-defined]
example.py:40:15: error: Argument 1 to "get_x_y_1" has incompatible type "ProtocolIntersection"; expected "DesiredObject"  [arg-type]
Found 6 errors in 1 file (checked 1 source file)

> # with plugin
> mypy example.py
Success: no issues found in 1 source file

Invalid program

And here's how would the plugin help if you forgot to include one of the protocols:

from types import SimpleNamespace
from typing import Protocol, Generic, TypeVar, Dict

from typing_protocol_intersection import ProtocolIntersection

class HasX(Protocol):
    x: str

class HasY(Protocol):
    y: str

T = TypeVar("T")
class Builder(Generic[T]):
    def __init__(self) -> None:
        super().__init__()
        self._d: Dict[str, str] = {}

    def with_x(self) -> "Builder[ProtocolIntersection[T, HasX]]":
        self._d["x"] = "X"
        return self  # type: ignore

    def with_y(self) -> "Builder[ProtocolIntersection[T, HasY]]":
        self._d["y"] = "Y"
        return self  # type: ignore

    def build(self) -> T:
        return SimpleNamespace(**self._d)  # type: ignore

class DesiredObject(HasX, HasY, Protocol):
    pass

def get_x_y_1(o: DesiredObject) -> None:
    print(f"{o.x=}; {o.y=}")

def get_x_y_2(o: ProtocolIntersection[HasX, HasY]) -> None:
    print(f"{o.x=}; {o.y=}")

def main() -> None:
    valid_o = Builder().with_x().build()  # <-- note no .with_y()
    get_x_y_1(valid_o)
    get_x_y_2(valid_o)

if __name__ == "__main__":
    main()
> mypy example.py
example.py:40:15: error: Argument 1 to "get_x_y_1" has incompatible type "ProtocolIntersection[HasX]"; expected "DesiredObject"  [arg-type]
example.py:40:15: note: "ProtocolIntersection" is missing following "DesiredObject" protocol member:
example.py:40:15: note:     y
example.py:41:15: error: Argument 1 to "get_x_y_2" has incompatible type "typing_protocol_intersection.types.ProtocolIntersection[HasX]"; expected "typing_protocol_intersection.types.ProtocolIntersection[HasY, HasX]"  [arg-type]
example.py:41:15: note: "ProtocolIntersection" is missing following "ProtocolIntersection" protocol member:
example.py:41:15: note:     y
Found 2 errors in 1 file (checked 1 source file)

Recommended usage

The ProtocolIntersection class name might seem a bit lengthy, but it's explicit, which is good. For brevity and better readability, it's recommended to use an alias when importing.

from typing_protocol_intersection import ProtocolIntersection as Has

The simple example would translate to

from typing import Protocol
from typing_protocol_intersection import ProtocolIntersection as Has

class X(Protocol):
    x: str

class Y(Protocol):
    y: str

def foo(xy: Has[X, Y]) -> None:
    print(xy.x, xy.y)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typing-protocol-intersection-0.2.2.tar.gz (8.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typing_protocol_intersection-0.2.2-py3-none-any.whl (7.2 kB view details)

Uploaded Python 3

File details

Details for the file typing-protocol-intersection-0.2.2.tar.gz.

File metadata

File hashes

Hashes for typing-protocol-intersection-0.2.2.tar.gz
Algorithm Hash digest
SHA256 50f6b1b45ee5ef60de132e27cd188b964572ab4165ae22fc4a68e7da96b6a1c8
MD5 f2bcaa1bf27ec17ed75b289196f5b2a9
BLAKE2b-256 88a8024aa7c499736a7597a55b59eb15e4dc1871f5271f53d71bbe2bbd6bc5d6

See more details on using hashes here.

File details

Details for the file typing_protocol_intersection-0.2.2-py3-none-any.whl.

File metadata

File hashes

Hashes for typing_protocol_intersection-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9598d16aaa65678056830d2c63465da7f2b70199f87df6f733ee4802c0aa390a
MD5 c2d3ca8c40628e5d665d076b2e4fe92b
BLAKE2b-256 e8c1cdbafb84dc012d970510d7060b8e17aaadfbb8c210a15083f462301bfb21

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page