Repository: pallets/click (python)

Issue:
Click doesn't close file options during shell completion
Click doesn't close file options during shell completion, which causes a resource warning if a program uses a file option.

For example, I have group like this:

```python
@click.group()
@click.option('--config_file',
              default=CONFIG,
              type=click.File(mode='r'),
              help='help')
@click.pass_context
def cli(ctx, config_file: typing.TextIO):
```

and I get this warning:

```
/Users/grzesiek/Library/Caches/pypoetry/virtualenvs/findata-fetcher-3hK6JJJX-py3.12/lib/python3.12/site-packages/click/shell_completion.py:293: ResourceWarning: unclosed file <_io.TextIOWrapper name='/Users/grzesiek/.config/findata/fetcher.json' mode='r' encoding='UTF-8'>
  completions = self.get_completions(args, incomplete)
```

## Details

I don't come with reproduction steps, but I can give something equally valuable, I can explain how this bug comes to be.

The issue stems from allocating a context in `core.py` outside of a `with` statement during shell completion. Here's a stack-trace of how that happens:

```
  File "/Users/grzesiek/.local/bin/findata-fetcher", line 8, in <module>
    sys.exit(main())
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/fetcher/tool.py", line 576, in main
    cli(obj={})
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/core.py", line 1171, in __call__
    return self.main(*args, **kwargs)
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/core.py", line 1084, in main
    self._main_shell_completion(extra, prog_name, complete_var)
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/core.py", line 1165, in _main_shell_completion
    rv = shell_complete(self, ctx_args, prog_name, complete_var,
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/shell_completion.py", line 49, in shell_complete
    echo(comp.complete())
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/shell_completion.py", line 296, in complete
    completions = self.get_completions(args, incomplete)
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/shell_completion.py", line 273, in get_completions
    ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/shell_completion.py", line 513, in _resolve_context
    ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
  File "/Users/grzesiek/.local/pipx/venvs/findata-fetcher/lib/python3.12/site-packages/click/core.py", line 952, in make_context
    with ctx.scope(cleanup=False):
```

This context gets [returned](https://github.com/pallets/click/blob/ca5e1c3d75e95cbc70fa6ed51ef263592e9ac0d0/src/click/core.py#L1147C19-L1147C19) to `get_completions`, but `get_completions` or its caller never call the context's `__exit__` function. 

Calling the `__exit__` function is essential, because `types.File` depends on it for cleanup: https://github.com/pallets/click/blob/ca5e1c3d75e95cbc70fa6ed51ef263592e9ac0d0/src/click/types.py#L740.

## Environment

- Python version: 3.12
- Click version: 8.1.7


Pull request:
Close contexts created during shell completion #2644.
Includes a test reproducing #2644. Then continues from the work done by @akhil-vempali in #2767 to fix the issue. There was some confusion in the mess of context objects in the implementation that has been fixed in this version.

Resolves #2644.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 540bbe2..01e8d8b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -59,6 +59,9 @@ Unreleased
 -   If help is shown because ``no_args_is_help`` is enabled (defaults to ``True``
     for groups, ``False`` for commands), the exit code is 2 instead of 0.
     :issue:`1489` :pr:`1489`
+-   Contexts created during shell completion are closed properly, fixing
+    ``ResourceWarning``s when using ``click.File``. :issue:`2644` :pr:`2800`
+    :pr:`2767`
 
 
 Version 8.1.8
diff --git a/src/click/core.py b/src/click/core.py
index 666ad68..bf8967f 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -704,8 +704,8 @@ class Context:
         """Exits the application with a given exit code.
 
         .. versionchanged:: 8.2
-            Force closing of callbacks registered with
-            :meth:`call_on_close` before exiting the CLI.
+            Callbacks and context managers registered with :meth:`call_on_close`
+            and :meth:`with_resource` are closed before exiting.
         """
         self.close()
         raise Exit(code)
diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py
index 6fd9e54..c8655b1 100644
--- a/src/click/shell_completion.py
+++ b/src/click/shell_completion.py
@@ -544,44 +544,48 @@ def _resolve_context(
     :param args: List of complete args before the incomplete value.
     """
     ctx_args["resilient_parsing"] = True
-    ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
-    args = ctx._protected_args + ctx.args
+    with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx:
+        args = ctx._protected_args + ctx.args
 
-    while args:
-        command = ctx.command
+        while args:
+            command = ctx.command
 
-        if isinstance(command, Group):
-            if not command.chain:
-                name, cmd, args = command.resolve_command(ctx, args)
-
-                if cmd is None:
-                    return ctx
-
-                ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
-                args = ctx._protected_args + ctx.args
-            else:
-                sub_ctx = ctx
-
-                while args:
+            if isinstance(command, Group):
+                if not command.chain:
                     name, cmd, args = command.resolve_command(ctx, args)
 
                     if cmd is None:
                         return ctx
 
-                    sub_ctx = cmd.make_context(
-                        name,
-                        args,
-                        parent=ctx,
-                        allow_extra_args=True,
-                        allow_interspersed_args=False,
-                        resilient_parsing=True,
-                    )
-                    args = sub_ctx.args
-
-                ctx = sub_ctx
-                args = [*sub_ctx._protected_args, *sub_ctx.args]
-        else:
-            break
+                    with cmd.make_context(
+                        name, args, parent=ctx, resilient_parsing=True
+                    ) as sub_ctx:
+                        args = ctx._protected_args + ctx.args
+                        ctx = sub_ctx
+                else:
+                    sub_ctx = ctx
+
+                    while args:
+                        name, cmd, args = command.resolve_command(ctx, args)
+
+                        if cmd is None:
+                            return ctx
+
+                        with cmd.make_context(
+                            name,
+                            args,
+                            parent=ctx,
+                            allow_extra_args=True,
+                            allow_interspersed_args=False,
+                            resilient_parsing=True,
+                        ) as sub_sub_ctx:
+                            args = sub_ctx.args
+                            sub_ctx = sub_sub_ctx
+
+                    ctx = sub_ctx
+                    args = [*sub_ctx._protected_args, *sub_ctx.args]
+            else:
+                break
 
     return ctx
 


Test patch:
diff --git a/tests/test_shell_completion.py b/tests/test_shell_completion.py
index 3e7a2bd..3511f07 100644
--- a/tests/test_shell_completion.py
+++ b/tests/test_shell_completion.py
@@ -1,3 +1,5 @@
+import warnings
+
 import pytest
 
 import click.shell_completion
@@ -414,3 +416,27 @@ def test_add_completion_class_decorator():
     # Using `add_completion_class` as a decorator adds the new shell immediately
     assert "mysh" in click.shell_completion._available_shells
     assert click.shell_completion._available_shells["mysh"] is MyshComplete
+
+
+# Don't make the ResourceWarning give an error
+@pytest.mark.filterwarnings("default")
+def test_files_closed(runner) -> None:
+    with runner.isolated_filesystem():
+        config_file = "foo.txt"
+        with open(config_file, "w") as f:
+            f.write("bar")
+
+        @click.group()
+        @click.option(
+            "--config-file",
+            default=config_file,
+            type=click.File(mode="r"),
+        )
+        @click.pass_context
+        def cli(ctx, config_file):
+            pass
+
+        with warnings.catch_warnings(record=True) as current_warnings:
+            assert not current_warnings, "There should be no warnings to start"
+            _get_completions(cli, args=[], incomplete="")
+            assert not current_warnings, "There should be no warnings after either"