Skip to content

Commit 4b87740

Browse files
Allow covariants of __enter__, __aenter__, and @classmethod (#1336)
* Allow covariants of __enter__, __aenter__, and @classmethod The problem we currently have is the return type of classes such as Client does not allow covariants when subclassing or context manager. In other words: ```python class Base: def __enter__(self) -> Base: # XXX return self class Derived(Base): ... with Derived() as derived: # The type of derived is Base but not Derived. It is WRONG ... ``` There are three approaches to improve type annotations. 1. Just do not type-annotate and let the type checker infer `return self`. 2. Use a generic type with a covariant bound `_AsyncClient = TypeVar('_AsyncClient', bound=AsyncClient)` 3. Use a generic type `T = TypeVar('T')` or `Self = TypeVar('Self')` They have pros and cons. 1. It just works and is not friendly to developers as there is no type annotation at the first sight. A developer has to reveal its type via a type checker. Aslo, documentation tools that rely on type annotations lack the type. I haven't found any python docuementation tools that rely on type inference to infer `return self`. There are some tools simply check annotations. 2. This approach is correct and has a nice covariant bound that adds type safety. It is also nice to documentation tools and _somewhat_ friendly to developers. Type checkers, pyright that I use, always shows the the bounded type '_AsyncClient' rather than the subtype. Aslo, it requires more key strokes. Not good, not good. It is used by `BaseException.with_traceback` See https://github.com/python/typeshed/pull/4298/files 3. This approach always type checks, and I believe it _will_ be the official solution in the future. Fun fact, Rust has a Self type keyword. It is slightly unfriendly to documentation, but is simple to implement and easy to understand for developers. Most importantly, type checkers love it. See python/mypy#1212 But, we can have 2 and 3 combined: ```python _Base = typing.TypeVar('_Base', bound=Base) class Base: def __enter__(self: _Base) -> _Base: return self class Derive(Base): ... with Derived() as derived: ... # type of derived is Derived and it's a subtype of Base ``` * revert back type of of SteamContextManager to Response * Remove unused type definitions * Add comment and link to PEP484 for clarification * Switch to `T = TypeVar("T", covariant=True)` * fixup! Switch to `T = TypeVar("T", covariant=True)` * Add back bound=xxx in TypeVar Co-authored-by: Florimond Manca <[email protected]>
1 parent 65b69fa commit 4b87740

File tree

1 file changed

+8
-2
lines changed

1 file changed

+8
-2
lines changed

httpx/_client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
warn_deprecated,
5757
)
5858

59+
# The type annotation for @classmethod and context managers here follows PEP 484
60+
# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
61+
T = typing.TypeVar("T", bound="Client")
62+
U = typing.TypeVar("U", bound="AsyncClient")
63+
64+
5965
logger = get_logger(__name__)
6066

6167
KEEPALIVE_EXPIRY = 5.0
@@ -1106,7 +1112,7 @@ def close(self) -> None:
11061112
if proxy is not None:
11071113
proxy.close()
11081114

1109-
def __enter__(self) -> "Client":
1115+
def __enter__(self: T) -> T:
11101116
self._transport.__enter__()
11111117
for proxy in self._proxies.values():
11121118
if proxy is not None:
@@ -1752,7 +1758,7 @@ async def aclose(self) -> None:
17521758
if proxy is not None:
17531759
await proxy.aclose()
17541760

1755-
async def __aenter__(self) -> "AsyncClient":
1761+
async def __aenter__(self: U) -> U:
17561762
await self._transport.__aenter__()
17571763
for proxy in self._proxies.values():
17581764
if proxy is not None:

0 commit comments

Comments
 (0)