Repository: aio-libs/aiohttp

Issue:
Refactor cleanup_ctx implementation
When I was working on type-annotating `web.Application` I found the inconsitency in `cleanup_ctx` implementation introduced by #3194

1. `cleanup_ctx.append()` acccepts a generator instance but iteration over `cleanup_ctx` content returns internal objects.
2. Removing an item from `cleanup_ctx` doesn't unregister `on_startup` item.
3. `cleanup_ctx.insert(0, gen)` doesn't register a new `on_startup` item.

I afraid that mixing `on_startup` and `cleanup_ctx` is impossible to implement in a non-controversial way.
Maybe we need to revert the change back.

/cc @Maillol 


Discussion:
[GitMate.io](https://gitmate.io) thinks possibly related issues are https://github.com/aio-libs/aiohttp/issues/985 (Refactor ProxyConnector), https://github.com/aio-libs/aiohttp/issues/402 (Refactor reify), https://github.com/aio-libs/aiohttp/issues/20 (Refactor files handling.), https://github.com/aio-libs/aiohttp/issues/979 (DRAFT, New TestClientApp implementation), and https://github.com/aio-libs/aiohttp/issues/314 (Flow control implementation problems).
CleanupContext can have a `FrozenList` to store `cleanup_ctx_item._exit` instead of use itself. 
CleanupContext store in itself the original  generator accepted during the append.

We can add both `dict` inside  CleanupContext
- generator ==> `cleanup_ctx_item._exit`
- generator ==> `cleanup_ctx_item._enter`

To remove `on_startup` item during the `CleanupContext.__delitem__`.

I can proposal a new implementation this weekend.
Remove can be done by `__setitem__` too: `app.cleanup_ctx[1:3] = [a, b, c, d]`.
Please note: the number for removed items is not equal to the number of inserted.
Also `app.cleanup_ctx.insert(1, a)` exists.
I can easily fix the issue with `__delitem__` / `remove`  and type returned by the iteration.

But with `insert`  I don't know which  behavior to choose:
```
app.cleanup_ctx.append(c1)
app.on_startup.append(s1)
app.cleanup_ctx..append(c2)
app.cleanup_ctx.insert(1, c3) 
# When c3 should be executed ? before or after s1 ? 
```

I don't see how we can keep order with insert `__setitem__` and slice.`

Resolving patch:
diff --git a/aiohttp/web_app.py b/aiohttp/web_app.py
--- a/aiohttp/web_app.py
+++ b/aiohttp/web_app.py
@@ -104,7 +104,8 @@ def __init__(self, *,
         self._on_startup = Signal(self)  # type: _AppSignal
         self._on_shutdown = Signal(self)  # type: _AppSignal
         self._on_cleanup = Signal(self)  # type: _AppSignal
-        self._cleanup_ctx = CleanupContext(self._on_startup)
+        self._cleanup_ctx = CleanupContext()
+        self._on_startup.append(self._cleanup_ctx._on_startup)
         self._on_cleanup.append(self._cleanup_ctx._on_cleanup)
         self._client_max_size = client_max_size
 
@@ -446,60 +447,37 @@ def exceptions(self) -> List[BaseException]:
         return self.args[1]
 
 
-class _CleanupContextItem:
-    """
-    CleanupContext uses this class to wrap an asynchronous generator
-    before adding to itself.
-    """
-
-    def __init__(self,
-                 cb: Callable[[Application], AsyncIterator[None]]) -> None:
-        self._cb = cb
-        self._iterator_at_exit = None  # type: Optional[AsyncIterator[None]]
-
-    async def _enter(self, app: Application) -> None:
-        it = self._cb(app).__aiter__()
-        await it.__anext__()
-        self._iterator_at_exit = it
-
-    async def _exit(self, app: Application) -> None:
-        if self._iterator_at_exit is None:
-            return
-        try:
-            await self._iterator_at_exit.__anext__()
-        except StopAsyncIteration:
-            pass
-        else:
-            raise RuntimeError("{!r} has more than one 'yield'"
-                               .format(self._iterator_at_exit))
-
-
-if TYPE_CHECKING:  # pragma: no branch
-    _CleanupContextBase = FrozenList[_CleanupContextItem]
+if TYPE_CHECKING:  # pragma: no cover
+    _CleanupContextBase = FrozenList[Callable[[Application],
+                                              AsyncIterator[None]]]
 else:
     _CleanupContextBase = FrozenList
 
 
 class CleanupContext(_CleanupContextBase):
 
-    def __init__(self, on_startup: _AppSignal):
+    def __init__(self) -> None:
         super().__init__()
-        self._on_startup = on_startup
+        self._exits = []  # type: List[AsyncIterator[None]]
 
-    def append(self,  # type: ignore
-               item: Callable[[Application], AsyncIterator[None]]) -> None:
-        cleanup_ctx_item = _CleanupContextItem(item)
-        super().append(cleanup_ctx_item._exit)  # type: ignore
-        self._on_startup.append(cleanup_ctx_item._enter)
+    async def _on_startup(self, app: Application) -> None:
+        for cb in self:
+            it = cb(app).__aiter__()
+            await it.__anext__()
+            self._exits.append(it)
 
-    async def _on_cleanup(self, app: Application):
+    async def _on_cleanup(self, app: Application) -> None:
         errors = []
-        for cb in reversed(self):
+        for it in reversed(self._exits):
             try:
-                await cb(app)  # type: ignore
+                await it.__anext__()
+            except StopAsyncIteration:
+                pass
             except Exception as exc:
                 errors.append(exc)
-
+            else:
+                errors.append(RuntimeError("{!r} has more than one 'yield'"
+                                           .format(it)))
         if errors:
             if len(errors) == 1:
                 raise errors[0]


Test patch:
diff --git a/tests/test_web_app.py b/tests/test_web_app.py
--- a/tests/test_web_app.py
+++ b/tests/test_web_app.py
@@ -415,43 +415,6 @@ async def inner(app):
     assert out == ['pre_1', 'post_1']
 
 
-async def test_mixe_cleanup_ctx_on_startup_and_on_cleanup() -> None:
-    app = web.Application()
-    out = []
-
-    def startup(num):
-        async def inner(app):
-            out.append('pre_' + str(num))
-        return inner
-
-    def cleanup(num):
-        async def inner(app):
-            out.append('post_' + str(num))
-        return inner
-
-    def cleanup_ctx(num):
-        @async_generator
-        async def inner(app):
-            out.append('pre_' + str(num))
-            await yield_(None)
-            out.append('post_' + str(num))
-        return inner
-
-    app.on_startup.append(startup(1))
-    app.cleanup_ctx.append(cleanup_ctx(2))
-    app.on_startup.append(startup(3))
-    app.cleanup_ctx.append(cleanup_ctx(4))
-    app.on_startup.append(startup(5))
-
-    app.freeze()
-    await app.startup()
-    assert out == ['pre_1', 'pre_2', 'pre_3', 'pre_4', 'pre_5']
-
-    del out[:]
-    await app.cleanup()
-    assert out == ['post_4', 'post_2']
-
-
 async def test_subapp_chained_config_dict_visibility(aiohttp_client) -> None:
 
     async def main_handler(request):