Skip to content

Commit 5e2f66f

Browse files
authored
fix: Temporary directories are not removed (#109)
1 parent 630e02f commit 5e2f66f

File tree

3 files changed

+84
-5
lines changed

3 files changed

+84
-5
lines changed

src/common/core/main.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
import logging
33
import os
44
import sys
5-
import tempfile
65
import typing
76

87
from django.core.management import (
98
execute_from_command_line as django_execute_from_command_line,
109
)
10+
from environs import Env
1111

1212
from common.core.cli import healthcheck
13+
from common.core.utils import TemporaryDirectory
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -30,6 +31,7 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
3031
main()
3132
```
3233
"""
34+
env = Env()
3335
ctx = contextlib.ExitStack()
3436

3537
# TODO @khvn26 Move logging setup to here
@@ -43,9 +45,11 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
4345
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
4446

4547
# Set up Prometheus' multiprocess mode
46-
if "PROMETHEUS_MULTIPROC_DIR" not in os.environ:
47-
prometheus_multiproc_dir_name = tempfile.mkdtemp()
48-
48+
if not env.str("PROMETHEUS_MULTIPROC_DIR", ""):
49+
delete = not env.bool("PROMETHEUS_MULTIPROC_DIR_KEEP", False)
50+
prometheus_multiproc_dir_name = ctx.enter_context(
51+
TemporaryDirectory(delete=delete)
52+
)
4953
logger.info(
5054
"Created %s for Prometheus multi-process mode",
5155
prometheus_multiproc_dir_name,

src/common/core/utils.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@
22
import logging
33
import pathlib
44
import random
5+
import sys
6+
import tempfile
57
from functools import lru_cache
68
from itertools import cycle
7-
from typing import Iterator, Literal, NotRequired, TypedDict, TypeVar, get_args
9+
from typing import (
10+
Iterator,
11+
Literal,
12+
NotRequired,
13+
TypedDict,
14+
TypeVar,
15+
get_args,
16+
)
817

918
from django.conf import settings
1019
from django.contrib.auth import get_user_model
@@ -186,3 +195,40 @@ def using_database_replica(
186195
return manager
187196

188197
return manager.db_manager(chosen_replica)
198+
199+
200+
if sys.version_info >= (3, 12):
201+
# Already has the desired behavior; re-export for uniform imports.
202+
TemporaryDirectory = tempfile.TemporaryDirectory
203+
else:
204+
import contextlib
205+
from typing import ContextManager, Generator
206+
207+
def TemporaryDirectory(
208+
suffix: str | None = None,
209+
prefix: str | None = None,
210+
dir: str | None = None,
211+
*,
212+
delete: bool = True,
213+
) -> ContextManager[str]:
214+
"""
215+
Create a temporary directory with optional cleanup control.
216+
217+
This wrapper exists because Python 3.12 changed TemporaryDirectory's behavior
218+
by adding a 'delete' parameter, which doesn't exist in Python 3.11. This
219+
function provides a consistent API across both versions.
220+
221+
When delete=True, uses the stdlib's TemporaryDirectory (auto-cleanup).
222+
When delete=False, creates a directory with mkdtemp that persists after
223+
the context manager exits, matching Python 3.12's delete=False behavior.
224+
225+
See https://docs.python.org/3.12/library/tempfile.html#tempfile.TemporaryDirectory for usage details.
226+
"""
227+
if delete:
228+
return tempfile.TemporaryDirectory(suffix, prefix, dir)
229+
230+
@contextlib.contextmanager
231+
def _tmpdir() -> Generator[str, None, None]:
232+
yield tempfile.mkdtemp(suffix, prefix, dir)
233+
234+
return _tmpdir()

tests/integration/core/test_main.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import os
2+
from pathlib import Path
3+
14
import django
25
import pytest
36
from django.core.management import ManagementUtility
7+
from pyfakefs.fake_filesystem import FakeFilesystem
48
from pytest_httpserver import HTTPServer
59

610
from common.core.main import main
@@ -104,3 +108,28 @@ def test_main__healthcheck_http__server_invalid_response__runs_expected(
104108
# When & Then
105109
with pytest.raises(Exception):
106110
main(argv)
111+
112+
113+
def test_main__prometheus_multiproc_remove_dir_on_exit_default__expected() -> None:
114+
# Given
115+
os.environ.pop("PROMETHEUS_MULTIPROC_DIR_KEEP", None)
116+
117+
# When
118+
main(["flagsmith"])
119+
120+
# Then
121+
assert not Path(os.environ["PROMETHEUS_MULTIPROC_DIR"]).exists()
122+
123+
124+
def test_main__prometheus_multiproc_remove_dir_on_exit_true__expected(
125+
fs: FakeFilesystem,
126+
) -> None:
127+
# Given
128+
os.environ.pop("PROMETHEUS_MULTIPROC_DIR", None)
129+
os.environ["PROMETHEUS_MULTIPROC_DIR_KEEP"] = "true"
130+
131+
# When
132+
main(["flagsmith"])
133+
134+
# Then
135+
assert Path(os.environ["PROMETHEUS_MULTIPROC_DIR"]).exists()

0 commit comments

Comments
 (0)