CarliJoy / intersection_examples

Python Typing Intersection examples
MIT License
33 stars 2 forks source link

sample use case: return type of `@contextmanager` decorated functions #15

Open ippeiukai opened 1 year ago

ippeiukai commented 1 year ago

I want to contribute a real life use case that we encountered which may justify the needs of Intersection types.

Can anyone suggest an alternative without using Intersection?

from typing import *
from contextlib import AbstractContextManager, ContextDecorator, contextmanager

@contextmanager
def _hoge():
    ...
    try:
        yield
    finally:
        ...

def foo() -> ContextManager[Never] & ContextDecorator:  # How should we annotate this without Intersection?
    ...
    return _hoge()

# ====
# foo is used both as ContextManager and ContextDecorator

with foo():
    ...

@foo()
def bar():
    ...

NOTE: the spec of @contextmanager is here: https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager

Essentially, what the decorated function returns is a ContextManager that uses ContextDecorator mixin.

ippeiukai commented 1 year ago

The best I came up with was using @contextmanager within if TYPE_CHECKING block and trusting type checkers to infer the type of foo. This isn't the same as explicitly annotating foo's return type.

if TYPE_CHECKING:
    @contextmanager
    def foo() -> Iterator[Never]:
        ...
else:
    def foo():
        ...
        return _hoge()
erictraut commented 1 year ago

The correct return type for foo is _GeneratorContextManager (imported from contextlib).

from contextlib import _GeneratorContextManager

def foo() -> _GeneratorContextManager:
    ...
    return _hoge()

_GeneratorContextManager is the actual class used at runtime, and this class subclasses from both AbstractContextManager and ContextDecorator.

The fact that it's named with an underscore means that it's private, so you might be reluctant to use this type in a type annotation. I'm not sure why the authors of contextlib decided not to expose this type publicly. If you're using pyright, you don't need to supply a return type for foo because it infers the proper return type. Mypy doesn't do this currently.

image
ippeiukai commented 1 year ago

The fact that it's named with an underscore means that it's private, so you might be reluctant to use this type in a type annotation.

Yap, exactly that. It is clearly not meant to be exported from contextlib. https://github.com/python/cpython/blob/a24e25d74bb5d30775a61853d34e0bb5a7e12c64/Lib/contextlib.py#L10-L14 https://github.com/python/typeshed/blob/a079b0de6e703fcb615a1169a32bc2186efee9bd/stdlib/contextlib.pyi#L10-L23

I'm not sure why the authors of contextlib decided not to expose this type publicly.

I think not exposing such implementation detail is good. That way, we can later swap the implementation _hoge with our own AbstractContextManager subclass with ContextDecorator mixed in.