-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Make multiprocessing pipes generic #11137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b4bb20a
9f57174
8ba4657
460dc52
71306ae
ee59a69
c5dc823
d02c80b
b01faf3
1b8fbfe
211f4f6
2dbfadd
8a9068f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the variance of the TypeVars correct here? It looks like maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤷 variance with Python's TypeVars is still something I get highly confused about and has not "clicked" at all for me. Like I understand the general concept of directionality (sometimes any base type can be used, sometimes any subtype can be used), and that you'd want a "wider" type for parameters, "narrower" type for return types. But I'm never sure which to use (if any) when it comes to TypeVars There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think anybody who claims to be able to think about generics in Python without their head hurting sometimes is lying to you :) In general, a good heuristic for a given TypeVar in a given class is that if the TypeVar only appears in return annotations in that class, it should be covariant; if it only appears in parameter annotations, it should be contravariant; if it appears in both, it has to be invariant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted and bookmarked. It may not always apply, but heuristics I can follow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have explained my view of variance on typeshed a few times, and I'm too lazy to find a previous explanation, so here goes again :) Most of the time you only need invariant (default) and covariant TypeVars. To distinguish them, think about foo: list[int] = [1, 2, 3]
bar: list[float] = foo
bar.append(12.34) We communicate this to type checkers by using an invariant TypeVar in On the other hand, a Contravariance comes up less often, but it's basically the opposite of covariant: making And yes, variance is hard :) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,21 @@ | ||
import socket | ||
import sys | ||
import types | ||
from _typeshed import ReadableBuffer | ||
from _typeshed import Incomplete, ReadableBuffer | ||
from collections.abc import Iterable | ||
from typing import Any, SupportsIndex | ||
from types import TracebackType | ||
from typing import Any, Generic, SupportsIndex, TypeVar | ||
from typing_extensions import Self, TypeAlias | ||
|
||
__all__ = ["Client", "Listener", "Pipe", "wait"] | ||
|
||
# https://docs.python.org/3/library/multiprocessing.html#address-formats | ||
_Address: TypeAlias = str | tuple[str, int] | ||
|
||
class _ConnectionBase: | ||
# Defaulting to Any to avoid forcing generics on a lot of pre-existing code | ||
_SendT = TypeVar("_SendT", contravariant=True, default=Any) | ||
_RecvT = TypeVar("_RecvT", covariant=True, default=Any) | ||
|
||
class _ConnectionBase(Generic[_SendT, _RecvT]): | ||
def __init__(self, handle: SupportsIndex, readable: bool = True, writable: bool = True) -> None: ... | ||
@property | ||
def closed(self) -> bool: ... # undocumented | ||
|
@@ -22,54 +26,58 @@ class _ConnectionBase: | |
def fileno(self) -> int: ... | ||
def close(self) -> None: ... | ||
def send_bytes(self, buf: ReadableBuffer, offset: int = 0, size: int | None = None) -> None: ... | ||
def send(self, obj: Any) -> None: ... | ||
def send(self, obj: _SendT) -> None: ... | ||
def recv_bytes(self, maxlength: int | None = None) -> bytes: ... | ||
def recv_bytes_into(self, buf: Any, offset: int = 0) -> int: ... | ||
def recv(self) -> Any: ... | ||
def recv(self) -> _RecvT: ... | ||
def poll(self, timeout: float | None = 0.0) -> bool: ... | ||
def __enter__(self) -> Self: ... | ||
def __exit__( | ||
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_tb: types.TracebackType | None | ||
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_tb: TracebackType | None | ||
) -> None: ... | ||
def __del__(self) -> None: ... | ||
Avasam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
class Connection(_ConnectionBase): ... | ||
class Connection(_ConnectionBase[_SendT, _RecvT]): ... | ||
|
||
if sys.platform == "win32": | ||
class PipeConnection(_ConnectionBase): ... | ||
class PipeConnection(_ConnectionBase[_SendT, _RecvT]): ... | ||
|
||
class Listener: | ||
def __init__( | ||
self, address: _Address | None = None, family: str | None = None, backlog: int = 1, authkey: bytes | None = None | ||
) -> None: ... | ||
def accept(self) -> Connection: ... | ||
def accept(self) -> Connection[Incomplete, Incomplete]: ... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this could be improved further, rather than "too complex for the type system" or "you can truly send anything through the connection". Just not something I care to do in the scope of making |
||
def close(self) -> None: ... | ||
@property | ||
def address(self) -> _Address: ... | ||
@property | ||
def last_accepted(self) -> _Address | None: ... | ||
def __enter__(self) -> Self: ... | ||
def __exit__( | ||
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_tb: types.TracebackType | None | ||
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_tb: TracebackType | None | ||
) -> None: ... | ||
|
||
# Any: send and recv methods unused | ||
if sys.version_info >= (3, 12): | ||
def deliver_challenge(connection: Connection, authkey: bytes, digest_name: str = "sha256") -> None: ... | ||
def deliver_challenge(connection: Connection[Any, Any], authkey: bytes, digest_name: str = "sha256") -> None: ... | ||
|
||
else: | ||
def deliver_challenge(connection: Connection, authkey: bytes) -> None: ... | ||
def deliver_challenge(connection: Connection[Any, Any], authkey: bytes) -> None: ... | ||
|
||
def answer_challenge(connection: Connection, authkey: bytes) -> None: ... | ||
def answer_challenge(connection: Connection[Any, Any], authkey: bytes) -> None: ... | ||
def wait( | ||
object_list: Iterable[Connection | socket.socket | int], timeout: float | None = None | ||
) -> list[Connection | socket.socket | int]: ... | ||
def Client(address: _Address, family: str | None = None, authkey: bytes | None = None) -> Connection: ... | ||
object_list: Iterable[Connection[_SendT, _RecvT] | socket.socket | int], timeout: float | None = None | ||
) -> list[Connection[_SendT, _RecvT] | socket.socket | int]: ... | ||
def Client(address: _Address, family: str | None = None, authkey: bytes | None = None) -> Connection[Any, Any]: ... | ||
|
||
# N.B. Keep this in sync with multiprocessing.context.BaseContext.Pipe. | ||
# _ConnectionBase is the common base class of Connection and PipeConnection | ||
# and can be used in cross-platform code. | ||
# | ||
# The two connections should have the same generic types but inverted (Connection[_T1, _T2], Connection[_T2, _T1]). | ||
# However, TypeVars scoped entirely within a return annotation is unspecified in the spec. | ||
if sys.platform != "win32": | ||
def Pipe(duplex: bool = True) -> tuple[Connection, Connection]: ... | ||
def Pipe(duplex: bool = True) -> tuple[Connection[Any, Any], Connection[Any, Any]]: ... | ||
|
||
else: | ||
def Pipe(duplex: bool = True) -> tuple[PipeConnection, PipeConnection]: ... | ||
def Pipe(duplex: bool = True) -> tuple[PipeConnection[Any, Any], PipeConnection[Any, Any]]: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from __future__ import annotations | ||
|
||
import sys | ||
from multiprocessing.connection import Pipe | ||
|
||
if sys.platform != "win32": | ||
from multiprocessing.connection import Connection | ||
else: | ||
from multiprocessing.connection import PipeConnection as Connection | ||
|
||
|
||
# Unfortunately, we cannot validate that both connections have the same, but inverted generic types, | ||
# since TypeVars scoped entirely within a return annotation is unspecified in the spec. | ||
# Pipe[str, int]() -> tuple[Connection[str, int], Connection[int, str]] | ||
|
||
a: Connection[str, int] | ||
b: Connection[int, str] | ||
a, b = Pipe() | ||
|
||
connections: tuple[Connection[str, int], Connection[int, str]] = Pipe() | ||
a, b = connections | ||
|
||
a.send("test") | ||
a.send(0) # type: ignore | ||
test1: str = b.recv() | ||
test2: int = b.recv() # type: ignore | ||
|
||
b.send("test") # type: ignore | ||
b.send(0) | ||
test3: str = a.recv() # type: ignore | ||
test4: int = a.recv() |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe these should be
Connection[object, object]
orConnection[Unused, Unused]
? Otherwise we might get complaints about false positives from the (very small) group of people who use mypy's--disallow-any-expr
optional lint: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-disallow-any-exprUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure for the first,
_SendT
, contravariant, used for params argument.Idk about the second,
_RecvT
, covariant, used for return type.Arguably this is also a "private" name and I fail to come up with a real-world example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One option is to use the stricter non-Any types for now, and see if anybody complains about false positives... But I'm also fine with the PR being merged as-is; I don't have a strong opinion here :-)