2025, Oct 17 17:16

tempfile.gettempdir кэшируется: почему TMP не срабатывает под pytest

Почему в Python TMP не меняет tempfile.gettempdir под pytest: кэширование пути, приоритет TMPDIR/TEMP над TMP и как правильно задать каталог до запуска тестов.

Когда код, который на лету меняет TMP, срабатывает из командной строки, но падает под pytest, создаётся впечатление, будто тестовый раннер «вмешивается» в окружение. На деле всё проще и тоньше: tempfile кэширует временный каталог при первом использовании, и тесты обычно вызывают его раньше, чем ваша тестовая функция.

Минимальный пример

Следующий фрагмент ожидает, что установка TMP немедленно изменит значение, которое возвращает tempfile.gettempdir():

import os
import tempfile
def check_tmp_under_test():
    os.environ["TMP"] = "/home/fedora"
    assert tempfile.gettempdir() == "/home/fedora"
    print(tempfile.gettempdir())
check_tmp_under_test()

Запустите его напрямую — и вы увидите /home/fedora. Запустите через pytest — и он может напечатать /tmp или упасть на проверке. Одна и та же машина, тот же Python, но результат другой.

Что происходит на самом деле

Ключевая деталь — кэширование на уровне процесса внутри модуля tempfile. Первый вызов в процессе tempfile.gettempdir() определяет каталог, и это значение кэшируется на всю жизнь процесса. Изменение os.environ после первого вызова не влияет на кэшированный результат.

После первого вызова tempfile.gettempdir() прочитанное значение кэшируется — то есть если вы меняете os.environ после первого вызова в процессе, всё равно будет возвращено первоначальное значение.

Это кэширование можно наблюдать в интерактивной сессии:

import tempfile, os
print(tempfile.gettempdir())  # например, '/tmp'
os.environ["TMP"] = "/tmp/xis"
print(tempfile.gettempdir())  # по-прежнему '/tmp', потому что значение закэшировано

Под pytest вызов tempfile.gettempdir() часто происходит до запуска вашей тестовой функции. Его может спровоцировать сам раннер, плагин или любой код, создающий временный файл или каталог, потому что эти пути используют то же кэшированное значение. Как только значение закэшировано, последующие изменения TMP внутри теста уже не подействуют.

Есть и ещё один момент. Модуль tempfile смотрит на несколько переменных окружения, и у некоторых приоритет выше. TMPDIR и TEMP имеют преимущество над TMP. Если в процессе теста установлены TMPDIR или TEMP, изменение TMP ничего не изменит.

Внутри есть приватные помощники, вроде tempfile._candidate_tempdir_list(), которые при каждом вызове отражают текущее окружение, но и tempfile.gettempdir(), и tempfile._gettempdir() перестают быть динамичными после первого вызова в процессе. Поддерживаемого способа сбросить этот кэш посреди процесса нет.

Решение

Сделайте окружение детерминированным ещё до старта тестового процесса. Устанавливайте нужный временный каталог в переменных ОС при запуске pytest, а не внутри тестовой функции. Тогда первый вызов tempfile в процессе увидит корректное значение, и кэш совпадёт с вашими ожиданиями. Также убедитесь, что переменные с более высоким приоритетом, такие как TMPDIR или TEMP, не заданы конфликтующими значениями.

export TMP=/home/fedora
uv run pytest pytest.py

При установленном TMP на старте процесса tempfile.gettempdir() закэширует /home/fedora при первом использовании, и ваши проверки совпадут как при прямом запуске, так и под pytest.

Почему это важно

Тесты работают в долгоживущих процессах, которые делают больше, чем ваш код. Раннеры, плагины и утилиты могут обратиться к tempfile раньше, и из‑за кэширования этот ранний вызов фиксирует временный каталог для всего, что последует. Полагаться на изменение os.environ посреди теста для путей к временным файлам ненадёжно и приводит к недетерминированным сбоям.

Итоги

Если нужен конкретный временный каталог, устанавливайте TMP до запуска процесса Python. Помните, что TMPDIR и TEMP переопределяют TMP, поэтому проверьте, что в тестовой среде они не установлены во что‑то иное. Не рассчитывайте обновить tempfile.gettempdir(), меняя os.environ["TMP"] во время теста: после кэширования значение фиксируется на весь срок жизни процесса. Если нужно посмотреть, что бы tempfile выбрал прямо сейчас, не затрагивая кэш, можно взглянуть на динамический список кандидатов через приватные утилиты, но сброса кэшированного значения в поддерживаемом виде нет, поэтому самый надёжный путь — контролировать окружение на старте процесса.

Статья основана на вопросе на StackOverflow от Paul Dejean и на ответе jsbueno.