8
from typing import Any, Callable, TypeVar, cast
13
FuncType = Callable[..., Any]
14
F = TypeVar('F', bound=FuncType)
17
def _wrap_generator(ctx_factory, func):
19
Wrap each generator invocation with the context manager factory.
21
The input should be a function that returns a context manager,
22
not a context manager itself, to handle one-shot context managers.
24
@functools.wraps(func)
25
def generator_context(*args, **kwargs):
26
gen = func(*args, **kwargs)
35
response = gen.send(None)
40
request = yield response
51
response = gen.throw(*sys.exc_info())
56
response = gen.send(request)
60
except StopIteration as e:
66
return generator_context
69
def context_decorator(ctx, func):
71
Like contextlib.ContextDecorator.
73
But with the following differences:
74
1. Is done by wrapping, rather than inheritance, so it works with context
75
managers that are implemented from C and thus cannot easily inherit from
77
2. Wraps generators in the intuitive way (c.f. https://bugs.python.org/issue37743)
78
3. Errors out if you try to wrap a class, because it is ambiguous whether
79
or not you intended to wrap only the constructor
81
The input argument can either be a context manager (in which case it must
82
be a multi-shot context manager that can be directly invoked multiple times)
83
or a callable that produces a context manager.
85
assert not (callable(ctx) and hasattr(ctx, '__enter__')), (
86
f"Passed in {ctx} is both callable and also a valid context manager "
87
"(has __enter__), making it ambiguous which interface to use. If you "
88
"intended to pass a context manager factory, rewrite your call as "
89
"context_decorator(lambda: ctx()); if you intended to pass a context "
90
"manager directly, rewrite your call as context_decorator(lambda: ctx)"
99
if inspect.isclass(func):
101
"Cannot decorate classes; it is ambiguous whether or not only the "
102
"constructor or all methods should have the context manager applied; "
103
"additionally, decorating a class at definition-site will prevent "
104
"use of the identifier as a conventional type. "
105
"To specify which methods to decorate, decorate each of them "
109
if inspect.isgeneratorfunction(func):
110
return _wrap_generator(ctx_factory, func)
112
@functools.wraps(func)
113
def decorate_context(*args, **kwargs):
115
return func(*args, **kwargs)
117
return decorate_context
120
class _DecoratorContextManager:
121
"""Allow a context manager to be used as a decorator."""
123
def __call__(self, orig_func: F) -> F:
124
if inspect.isclass(orig_func):
125
warnings.warn("Decorating classes is deprecated and will be disabled in "
126
"future versions. You should only decorate functions or methods. "
127
"To preserve the current behavior of class decoration, you can "
128
"directly decorate the `__init__` method and nothing else.")
129
func = cast(F, lambda *args, **kwargs: orig_func(*args, **kwargs))
133
return cast(F, context_decorator(self.clone, func))
135
def __enter__(self) -> None:
136
raise NotImplementedError
138
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
139
raise NotImplementedError
143
return self.__class__()
146
class _NoParamDecoratorContextManager(_DecoratorContextManager):
147
"""Allow a context manager to be used as a decorator without parentheses."""
149
def __new__(cls, orig_func=None):
150
if orig_func is None:
151
return super().__new__(cls)
152
return cls()(orig_func)