본문으로 건너뛰기

[pytest] 캐시 디렉터리 생성 로직 단순화 — 원자적 생성 함수 추출

PR 링크: pytest-dev/pytest#14121 상태: Merged | 변경: +76 / -49

들어가며

pytest는 .pytest_cache 디렉터리를 생성할 때 TemporaryDirectory로 임시 디렉터리를 만들고, 보조 파일(README.md, .gitignore, CACHEDIR.TAG)을 작성한 후 원자적으로 rename합니다. 기존 코드는 _ensure_cache_dir_and_supporting_files() 메서드 안에 모든 로직이 인라인되어 있었고, TemporaryDirectory의 정리 로직이 rename 후 빈 디렉터리를 다시 만들어야 하는 workaround를 포함했습니다.

핵심 코드 분석

1. _make_cachedir() 함수 추출

Before (cacheprovider.py):

def _ensure_cache_dir_and_supporting_files(self) -> None:
    if self._cachedir.is_dir():
        return
    self._cachedir.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.TemporaryDirectory(prefix="pytest-cache-files-", dir=self._cachedir.parent) as newpath:
        path = Path(newpath)
        # ... 파일 작성 ...
        try:
            path.rename(self._cachedir)
        except OSError as e:
            if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
                raise
        else:
            # TemporaryDirectory cleanup이 실패하지 않도록 빈 디렉터리 재생성
            path.mkdir()

After:

def _make_cachedir(target: Path) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent))
    try:
        umask = os.umask(0o022)
        os.umask(umask)
        path.chmod(0o777 - umask)
        for name, content in CACHEDIR_FILES.items():
            path.joinpath(name).write_bytes(content)
        path.rename(target)
    except OSError as e:
        if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
            raise
    finally:
        shutil.rmtree(path, ignore_errors=True)

2. 보조 파일을 딕셔너리로 통합

Before:

README_CONTENT = """\
# pytest cache directory #
..."""

CACHEDIR_TAG_CONTENT = b"""\
Signature: 8a477f597d28d172789f06886806bc55
..."""

After:

CACHEDIR_FILES: dict[str, bytes] = {
    "README.md": b"# pytest cache directory #\n...",
    ".gitignore": b"# Created by pytest automatically.\n*\n",
    "CACHEDIR.TAG": b"Signature: 8a477f597d28d172789f06886806bc55\n...",
}

세 개의 상수가 하나의 딕셔너리로 통합되어, 반복문으로 일괄 작성합니다.

3. finally 블록으로 정리 보장

TemporaryDirectory 컨텍스트 매니저 대신 mkdtemp() + finally: shutil.rmtree()를 사용합니다. rename이 성공하면 원본이 이동되어 rmtree는 ENOENT로 조용히 실패하고, rename이 실패하거나 예외가 발생하면 임시 디렉터리를 확실히 정리합니다. KeyboardInterrupt 같은 BaseException에서도 정리가 보장됩니다.

왜 이게 좋은가

  • TemporaryDirectory의 rename 후 빈 디렉터리 재생성 workaround가 불필요해졌습니다.
  • finally 블록으로 BaseException(KeyboardInterrupt 등)에서도 임시 디렉터리가 정리됩니다.
  • 독립 함수로 추출하여 단위 테스트가 가능해졌습니다.

정리

  • 임시 리소스는 finally에서 정리하라: TemporaryDirectory의 컨텍스트 매니저보다 mkdtemp() + finally 조합이 더 유연한 정리를 제공할 수 있습니다.
  • 관련 상수는 하나의 자료구조로 묶어라: 개별 상수를 딕셔너리로 통합하면 일괄 처리가 간결해집니다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글