Repository: pallets/click

Issue:
Run subcommand in context manager
If would like to run a subcommand inside of a context manager that would be setup in a higher command. How is it possible to do that with click? My pseudo-code looks something like:


```python

import click

from contextlib import contextmanager

@contextmanager
def database_context(db_url):
    try:
        print(f'setup db connection: {db_url}')
        yield
    finally:
        print('teardown db connection')


@click.group
@click.option('--db',default='local')
def main(db):
    print(f'running command against {db} database')
    db_url = get_db_url(db)
    connection_manager = database_context(db_url)
    # here come the mysterious part that makes all subcommands
    # run inside the connection manager

@main.command
def do_this_thing()
    print('doing this thing')

@main.command
def do_that_thing()
    print('doing that thing')

```

And this would be called like:

```
> that_cli do_that_thing
running command against local database
setup db connection: db://user:pass@localdb:db_name
doing that thing
teardown db connection

> that_cli --db staging do_this_thing
running command against staging database
setup db connection: db://user:pass@123.456.123.789:db_name
doing this thing
teardown db connection
```



Discussion:

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -3,6 +3,7 @@
 import os
 import sys
 from contextlib import contextmanager
+from contextlib import ExitStack
 from functools import update_wrapper
 from itertools import repeat
 
@@ -401,6 +402,7 @@ def __init__(
         self._close_callbacks = []
         self._depth = 0
         self._source_by_paramname = {}
+        self._exit_stack = ExitStack()
 
     def __enter__(self):
         self._depth += 1
@@ -493,23 +495,55 @@ def make_formatter(self):
             width=self.terminal_width, max_width=self.max_content_width
         )
 
+    def with_resource(self, context_manager):
+        """Register a resource as if it were used in a ``with``
+        statement. The resource will be cleaned up when the context is
+        popped.
+
+        Uses :meth:`contextlib.ExitStack.enter_context`. It calls the
+        resource's ``__enter__()`` method and returns the result. When
+        the context is popped, it closes the stack, which calls the
+        resource's ``__exit__()`` method.
+
+        To register a cleanup function for something that isn't a
+        context manager, use :meth:`call_on_close`. Or use something
+        from :mod:`contextlib` to turn it into a context manager first.
+
+        .. code-block:: python
+
+            @click.group()
+            @click.option("--name")
+            @click.pass_context
+            def cli(ctx):
+                ctx.obj = ctx.with_resource(connect_db(name))
+
+        :param context_manager: The context manager to enter.
+        :return: Whatever ``context_manager.__enter__()`` returns.
+
+        .. versionadded:: 8.0
+        """
+        return self._exit_stack.enter_context(context_manager)
+
     def call_on_close(self, f):
-        """This decorator remembers a function as callback that should be
-        executed when the context tears down.  This is most useful to bind
-        resource handling to the script execution.  For instance, file objects
-        opened by the :class:`File` type will register their close callbacks
-        here.
+        """Register a function to be called when the context tears down.
 
-        :param f: the function to execute on teardown.
+        This can be used to close resources opened during the script
+        execution. Resources that support Python's context manager
+        protocol which would be used in a ``with`` statement should be
+        registered with :meth:`with_resource` instead.
+
+        :param f: The function to execute on teardown.
         """
-        self._close_callbacks.append(f)
-        return f
+        return self._exit_stack.callback(f)
 
     def close(self):
-        """Invokes all close callbacks."""
-        for cb in self._close_callbacks:
-            cb()
-        self._close_callbacks = []
+        """Invoke all close callbacks registered with
+        :meth:`call_on_close`, and exit all context managers entered
+        with :meth:`with_resource`.
+        """
+        self._exit_stack.close()
+        # In case the context is reused, create a new exit stack.
+        self._exit_stack = ExitStack()
 
     @property
     def command_path(self):


Test patch:
diff --git a/tests/test_context.py b/tests/test_context.py
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -1,3 +1,5 @@
+from contextlib import contextmanager
+
 import pytest
 
 import click
@@ -212,6 +214,22 @@ def foo():
     assert called == [True]
 
 
+def test_with_resource():
+    @contextmanager
+    def manager():
+        val = [1]
+        yield val
+        val[0] = 0
+
+    ctx = click.Context(click.Command("test"))
+
+    with ctx.scope():
+        rv = ctx.with_resource(manager())
+        assert rv[0] == 1
+
+    assert rv == [0]
+
+
 def test_make_pass_decorator_args(runner):
     """
     Test to check that make_pass_decorator doesn't consume arguments based on