Skip to content
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

Add options to nest context managers in the reverse order #4

Merged
merged 6 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,16 @@ defined above.
... @hookimpl
... @contextmanager
... def context(self, arg1, arg2):
... print('inside Plugin_1.context()')
... print('inside Plugin_1.context(): before')
... yield arg1 + arg2
... print('inside Plugin_1.context(): after')
...
... @hookimpl
... @asynccontextmanager
... async def acontext(self, arg1, arg2):
... print('inside Plugin_1.acontext()')
... print('inside Plugin_1.acontext(): before')
... yield arg1 + arg2
... print('inside Plugin_1.acontext(): after')

>>> class Plugin_2:
... """A 2nd hook implementation namespace."""
Expand All @@ -153,14 +155,16 @@ defined above.
... @hookimpl
... @contextmanager
... def context(self, arg1, arg2):
... print('inside Plugin_2.context()')
... print('inside Plugin_2.context(): before')
... yield arg1 - arg2
... print('inside Plugin_2.context(): after')
...
... @hookimpl
... @asynccontextmanager
... async def acontext(self, arg1, arg2):
... print('inside Plugin_2.acontext()')
... print('inside Plugin_2.acontext(): before')
... yield arg1 - arg2
... print('inside Plugin_2.acontext(): after')

```

Expand Down Expand Up @@ -213,9 +217,24 @@ inside Plugin_1.afunc()
```python
>>> with pm.with_.context(arg1=1, arg2=2) as y: # with_ instead of hook
... print(y)
inside Plugin_2.context()
inside Plugin_1.context()
inside Plugin_2.context(): before
inside Plugin_1.context(): before
[-1, 3]
inside Plugin_1.context(): after
inside Plugin_2.context(): after

```

In the reverse order:

```python
>>> with pm.with_reverse.context(arg1=1, arg2=2) as y: # with_reverse instead of hook
... print(y)
inside Plugin_1.context(): before
inside Plugin_2.context(): before
[3, -1]
inside Plugin_2.context(): after
inside Plugin_1.context(): after

```

Expand All @@ -227,9 +246,27 @@ inside Plugin_1.context()
... print(y)

>>> asyncio.run(call_acontext())
inside Plugin_2.acontext()
inside Plugin_1.acontext()
inside Plugin_2.acontext(): before
inside Plugin_1.acontext(): before
[-1, 3]
inside Plugin_1.acontext(): after
inside Plugin_2.acontext(): after

```

In the reverse order:

```python
>>> async def call_acontext():
... async with pm.awith_reverse.acontext(arg1=1, arg2=2) as y: # awith_reverse instead of hook
... print(y)

>>> asyncio.run(call_acontext())
inside Plugin_1.acontext(): before
inside Plugin_2.acontext(): before
[3, -1]
inside Plugin_2.acontext(): after
inside Plugin_1.acontext(): after

```

Expand Down
32 changes: 13 additions & 19 deletions src/apluggy/_wrap/awith.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,26 @@ def __init__(self, pm: PluginManager_) -> None:

def __getattr__(self, name: str) -> Callable[..., AsyncContextManager]:
hook: HookCaller = getattr(self.pm.hook, name)
call = _Call(hook)
return call
return _Call(hook)


class AWithReverse:
def __init__(self, pm: PluginManager_) -> None:
self.pm = pm

def __getattr__(self, name: str) -> Callable[..., AsyncContextManager]:
hook: HookCaller = getattr(self.pm.hook, name)
return _Call(hook, reverse=True)


def _Call(
hook: Callable[..., list[AsyncContextManager]]
hook: Callable[..., list[AsyncContextManager]], reverse: bool = False
) -> Callable[..., AsyncContextManager]:
@contextlib.asynccontextmanager
async def call(*args: Any, **kwargs: Any) -> AsyncIterator[list]:
ctxs = hook(*args, **kwargs)
if reverse:
ctxs = list(reversed(ctxs))
async with contextlib.AsyncExitStack() as stack:
yields = [await stack.enter_async_context(ctx) for ctx in ctxs]

Expand All @@ -34,20 +44,4 @@ async def call(*args: Any, **kwargs: Any) -> AsyncIterator[list]:

yield yields

# TODO: The following commented out code is an attempt to support
# `asend()` through the `gen` attribute. It only works for
# simple cases. It doesn't work with starlette.lifespan().
# When starlette is shutting down, an exception is raised
# `RuntimeError: generator didn't stop after athrow()`.

# stop = False
# while not stop:
# sent = yield yields
# try:
# yields = await asyncio.gather(
# *[ctx.gen.asend(sent) for ctx in ctxs]
# )
# except StopAsyncIteration:
# stop = True

return call
6 changes: 4 additions & 2 deletions src/apluggy/_wrap/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from pluggy._hooks import _Plugin

from .ahook import AHook
from .awith import AWith
from .with_ import With
from .awith import AWith, AWithReverse
from .with_ import With, WithReverse


class PluginManager(PluginManager_):
Expand Down Expand Up @@ -116,7 +116,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.ahook = AHook(self)
self.with_ = With(self)
self.with_reverse = WithReverse(self)
self.awith = AWith(self)
self.awith_reverse = AWithReverse(self)

def register(
self, plugin: _Plugin | Callable[[], _Plugin], name: Optional[str] = None
Expand Down
30 changes: 26 additions & 4 deletions src/apluggy/_wrap/with_.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,26 @@ def __init__(self, pm: PluginManager_) -> None:

def __getattr__(self, name: str) -> Callable[..., GenCtxManager]:
hook: HookCaller = getattr(self.pm.hook, name)
call = _Call(hook)
return call
return _Call(hook)


def _Call(hook: Callable[..., list[GenCtxManager]]) -> Callable[..., GenCtxManager]:
class WithReverse:
def __init__(self, pm: PluginManager_) -> None:
self.pm = pm

def __getattr__(self, name: str) -> Callable[..., GenCtxManager]:
hook: HookCaller = getattr(self.pm.hook, name)
return _Call(hook, reverse=True)


def _Call(
hook: Callable[..., list[GenCtxManager]], reverse: bool = False
) -> Callable[..., GenCtxManager]:
@contextlib.contextmanager
def call(*args: Any, **kwargs: Any) -> Generator[list, Any, list]:
ctxs = hook(*args, **kwargs)
if reverse:
ctxs = list(reversed(ctxs))
with contextlib.ExitStack() as stack:
yields = [stack.enter_context(ctx) for ctx in ctxs]

Expand All @@ -36,6 +48,16 @@ def call(*args: Any, **kwargs: Any) -> Generator[list, Any, list]:
# `send()` and `throw()` and returns the return values of the
# hook implementations.

# TODO: Stop yielding from _support_gen() and simply uncomment
# above `yield yields` as Nextline no longer uses `send()` or
# `throw()`. ExitStack correctly executes the code after the yield
# statement in the reverse order of entering the contexts and
# propagates exceptions from inner contexts to outer contexts.
# _support_gen() also executes the code after the first yield in
# the reverse order. However, it might not be the most sensible
# order if `send()` is used. _support_gen() doesn't propagate the
# exceptions in the same way as ExitStack.

returns = yield from _support_gen(yields, ctxs)
return returns

Expand Down Expand Up @@ -81,7 +103,7 @@ class _Context:
raise

yields = []
for c in contexts:
for c in reversed(contexts): # close in the reversed order after yielding
y = None
if not c.stop_iteration:
try:
Expand Down
10 changes: 10 additions & 0 deletions tests/plugins/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,21 @@ def test_with(pm: PluginManager):
assert r == [-1, -1, 3]


def test_with_reverse(pm: PluginManager):
with pm.with_reverse.context(arg1=1, arg2=2) as r:
assert r == [3, -1, -1]


async def test_awith(pm: PluginManager):
async with pm.awith.acontext(arg1=1, arg2=2) as r:
assert r == [-1, -1, 3]


async def test_awith_reverse(pm: PluginManager):
async with pm.awith_reverse.acontext(arg1=1, arg2=2) as r:
assert r == [3, -1, -1]


def test_name(pm: PluginManager):
id_ = id(pm.list_name_plugin()[1][1]) # the object id of the 2nd plugin

Expand Down
4 changes: 3 additions & 1 deletion tests/test_with_send_throw.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ def test_plugins(data: st.DataObject):

for s, expected in zip(sends, yields_tr[1:]):
yielded = c.gen.send(s)
assert expected == yielded
# assert expected == yielded
assert list(reversed(expected)) == yielded
# Reversed because the context managers are executed in the reverse order after yielding.

if throw:
thrown = Thrown()
Expand Down
Loading