Repository: pypa/pip

Issue:
Refactor out the use of atexit.register for PEP 517 metadata directory
https://github.com/pypa/pip/blob/c23c52da4f719d3cc709da97a9157f3862bdb70a/src/pip/_internal/req/req_install.py#L604

Related discussion at #6325.

---

This issue is a good point for a contributor with some amount of familiarity with pip's codebase. It is reasonable to expect the fix for this issue to be should be straight forward. Please feel free to initiate a discussion about how this can be fixed in this issue. :)


Discussion:
Could we use #6967 for this? By creating a global TempDirectoryProvider which cleans them all up on exit of the context, for example. Managing this globally means that we do not need another level of indentation or to create more complicated relationships between callers and `InstallationRequirement`.
@chrahunt Ahaha! I made a similar comment over there, except applying to the entire context of #6967.

Resolving patch:
diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py
--- a/src/pip/_internal/cli/base_command.py
+++ b/src/pip/_internal/cli/base_command.py
@@ -33,6 +33,7 @@
 from pip._internal.utils.deprecation import deprecated
 from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
 from pip._internal.utils.misc import get_prog
+from pip._internal.utils.temp_dir import global_tempdir_manager
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 from pip._internal.utils.virtualenv import running_under_virtualenv
 
@@ -106,6 +107,10 @@ def main(self, args):
 
     def _main(self, args):
         # type: (List[str]) -> int
+        # Intentionally set as early as possible so globally-managed temporary
+        # directories are available to the rest of the code.
+        self.enter_context(global_tempdir_manager())
+
         options, args = self.parse_args(args)
 
         # Set verbosity so that it can be used elsewhere.
diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py
--- a/src/pip/_internal/operations/build/metadata.py
+++ b/src/pip/_internal/operations/build/metadata.py
@@ -1,7 +1,6 @@
 """Metadata generation logic for source distributions.
 """
 
-import atexit
 import logging
 import os
 
@@ -25,9 +24,9 @@ def generate_metadata(install_req):
     build_env = install_req.build_env
     backend = install_req.pep517_backend
 
-    # NOTE: This needs to be refactored to stop using atexit
-    metadata_tmpdir = TempDirectory(kind="modern-metadata")
-    atexit.register(metadata_tmpdir.cleanup)
+    metadata_tmpdir = TempDirectory(
+        kind="modern-metadata", globally_managed=True
+    )
 
     metadata_dir = metadata_tmpdir.path
 
diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py
--- a/src/pip/_internal/utils/temp_dir.py
+++ b/src/pip/_internal/utils/temp_dir.py
@@ -8,17 +8,35 @@
 import logging
 import os.path
 import tempfile
+from contextlib import contextmanager
+
+from pip._vendor.contextlib2 import ExitStack
 
 from pip._internal.utils.misc import rmtree
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
 if MYPY_CHECK_RUNNING:
-    from typing import Optional
+    from typing import Iterator, Optional
 
 
 logger = logging.getLogger(__name__)
 
 
+_tempdir_manager = None  # type: Optional[ExitStack]
+
+
+@contextmanager
+def global_tempdir_manager():
+    # type: () -> Iterator[None]
+    global _tempdir_manager
+    with ExitStack() as stack:
+        old_tempdir_manager, _tempdir_manager = _tempdir_manager, stack
+        try:
+            yield
+        finally:
+            _tempdir_manager = old_tempdir_manager
+
+
 class TempDirectory(object):
     """Helper class that owns and cleans up a temporary directory.
 
@@ -44,7 +62,8 @@ def __init__(
         self,
         path=None,    # type: Optional[str]
         delete=None,  # type: Optional[bool]
-        kind="temp"
+        kind="temp",  # type: str
+        globally_managed=False,  # type: bool
     ):
         super(TempDirectory, self).__init__()
 
@@ -61,6 +80,10 @@ def __init__(
         self.delete = delete
         self.kind = kind
 
+        if globally_managed:
+            assert _tempdir_manager is not None
+            _tempdir_manager.enter_context(self)
+
     @property
     def path(self):
         # type: () -> str


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -14,6 +14,7 @@
 from setuptools.wheel import Wheel
 
 from pip._internal.main import main as pip_entry_point
+from pip._internal.utils.temp_dir import global_tempdir_manager
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 from tests.lib import DATA_DIR, SRC_DIR, TestData
 from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key
@@ -177,6 +178,17 @@ def isolate(tmpdir):
         )
 
 
+@pytest.fixture(autouse=True)
+def scoped_global_tempdir_manager():
+    """Make unit tests with globally-managed tempdirs easier
+
+    Each test function gets its own individual scope for globally-managed
+    temporary directories in the application.
+    """
+    with global_tempdir_manager():
+        yield
+
+
 @pytest.fixture(scope='session')
 def pip_src(tmpdir_factory):
     def not_code_files_and_folders(path, names):
diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py
--- a/tests/unit/test_utils_temp_dir.py
+++ b/tests/unit/test_utils_temp_dir.py
@@ -5,8 +5,13 @@
 
 import pytest
 
+from pip._internal.utils import temp_dir
 from pip._internal.utils.misc import ensure_dir
-from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
+from pip._internal.utils.temp_dir import (
+    AdjacentTempDirectory,
+    TempDirectory,
+    global_tempdir_manager,
+)
 
 
 # No need to test symlinked directories on Windows
@@ -188,3 +193,17 @@ def raising_mkdir(*args, **kwargs):
         with pytest.raises(OSError):
             with AdjacentTempDirectory(original):
                 pass
+
+
+def test_global_tempdir_manager():
+    with global_tempdir_manager():
+        d = TempDirectory(globally_managed=True)
+        path = d.path
+        assert os.path.exists(path)
+    assert not os.path.exists(path)
+
+
+def test_tempdirectory_asserts_global_tempdir(monkeypatch):
+    monkeypatch.setattr(temp_dir, "_tempdir_manager", None)
+    with pytest.raises(AssertionError):
+        TempDirectory(globally_managed=True)