Импорт правильно с помощью pytest

Я только что настроил для использования pytest с Python 2.6. До сих пор он работал хорошо, за исключением обработки "импортных" заявлений: я не могу заставить pytest отвечать на импорт так же, как это делает моя программа.

Моя структура каталогов выглядит следующим образом:

src/
    main.py
    util.py
    test/
        test_util.py
    geom/
        vector.py
        region.py
        test/
            test_vector.py
            test_region.py

Для запуска я вызываю python main.py из src/.

В main.py я импортирую как вектор, так и область с помощью

from geom.region import Region
from geom.vector import Vector

В vector.py, я импортирую область с

from geom.region import Region

Все они работают нормально, когда я запускаю код в стандартном прогоне. Однако, когда я вызываю "py.test" из src/, он последовательно выходит с ошибками импорта.


Некоторые проблемы и попытки моего решения

Моя первая проблема заключалась в том, что при запуске "test/test_foo.py" py.test не смог напрямую импортировать foo.py. Я решил это, используя инструмент "imp". В "test_util.py":

import imp
util = imp.load_source("util", "util.py")

Это отлично работает для многих файлов. Также кажется, что когда pytest запускает "path/test/test_foo.py" для проверки "path/foo.py", он находится в каталоге "путь".

Однако это невозможно для "test_vector.py". Pytest может найти и импортировать модуль vector, но не может найти какой-либо из vector импорта. Следующий импорт (из "vector.py" ) не работает при использовании pytest:

from geom.region import *
from region import *

Они оба дают ошибки формы

ImportError: No module named [geom.region / region]

Я не знаю, что делать дальше, чтобы решить эту проблему; мое понимание импорта в Python ограничено.

Каков правильный способ обработки импорта при использовании pytest?


Редактировать: Чрезвычайно хакерское решение

В vector.py я изменил оператор импорта из

from geom.region import Region

просто

from region import Region

Это делает импорт относительно каталога "vector.py" .

Далее, в "test/test_vector.py", я добавляю каталог "vector.py" в путь следующим образом:

import sys, os
sys.path.append(os.path.realpath(os.path.dirname(__file__)+"/.."))

Это позволяет Python находить "../region.py" из "geom/test/test_vector.py".

Это работает, но кажется чрезвычайно проблематичным, потому что я добавляю тонну новых каталогов на путь. То, что я ищу, это либо

1) Стратегия импорта, совместимая с pytest, или

2) Опция в pytest, которая делает ее совместимой с моей стратегией импорта

Поэтому я оставляю этот вопрос открытым для ответов такого рода.

Ответ 1

import ищет следующие каталоги для поиска модуля:

  • домашний каталог программы. Это каталог вашего корня script. Когда вы запускаете pytest, ваш домашний каталог находится там, где он установлен (возможно,/usr/local/bin). Независимо от того, что вы используете его из каталога src, потому что местоположение вашего pytest определяет ваш домашний каталог. Именно по этой причине он не находит модули.
  • PYTHONPATH. Это переменная среды. Вы можете установить его из командной строки операционной системы. В системах Linux/Unix вы можете сделать это, выполнив: "export PYTHONPATH =/your/custom/path". Если вы хотите, чтобы Python находил ваши модули из тестового каталога, вы должны включить путь src в эту переменную.
  • Каталог стандартных библиотек. Это каталог, в котором установлены все ваши библиотеки.
  • Существует менее распространенный вариант с использованием файла pth.

sys.path - результат объединения домашнего каталога, PYTHONPATH и стандартных библиотек. То, что вы делаете, изменяет sys.path правильно. Это то, что я делаю регулярно. Вы можете попробовать использовать PYTHONPATH, если вам не нравится возиться с sys.path

Ответ 2

Проблема здесь в том, что Pytest обходит файловую систему, чтобы обнаружить файлы, содержащие тесты, но затем ему нужно сгенерировать имя модуля, которое приведет к import чтобы загрузить этот файл. (Помните, что файлы не являются модулями.)

Pytest предлагает имя этого тестового пакета, найдя первый каталог на уровне или выше уровня файла, который не содержит файл __init__.py и объявив, что "basedir" для дерева модулей содержит модуль, созданный из этого файла. Затем он добавляет basedir в sys.path и импортирует, используя имя модуля, которое найдет этот файл относительно basedir.

Есть некоторые последствия этого, о которых вы должны остерегаться:

  1. Базовый путь может не совпадать с вашим предполагаемым базовым путем, и в этом случае модуль будет иметь имя, которое не соответствует тому, что вы обычно используете. Например, то, что вы geom.test.test_vector будет фактически называться просто test_vector во время запуска Pytest, потому что он не нашел __init__.py в src/geom/test/ и поэтому добавил этот каталог в sys.path.

  2. Вы можете столкнуться с конфликтами именования модулей, если два файла в разных каталогах имеют одинаковые имена. Например, если в любом месте отсутствуют файлы __init__.py, добавление geom/test/test_util.py будет конфликтовать с test/test_util.py поскольку оба загружаются как import test_util.py, где в пути import test_util.py как test/ и geom/test/.

Система, которую вы используете здесь, без явных модулей __init__.py, __init__.py Python создавать неявные пакеты пространства имен для ваших каталогов. (Пакет - это модуль с подмодулями.) В идеале мы должны настроить Pytest на путь, по которому он также будет генерировать это, но он, похоже, не знает, как это сделать.

Самое простое решение - просто добавить пустые файлы __init__.py во все подкаталоги в каталоге src/; это заставит Pytest импортировать все, используя имена пакетов/модулей, которые начинаются с имен каталогов в src/.

Ответ 3

Мне было интересно, что делать с этой проблемой. Прочитав этот пост и немного поиграв, я понял элегантное решение. Я создал файл под названием "test_setup.py" и поместил в него следующий код:

import sys, os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

Я помещаю этот файл в каталог верхнего уровня (например, src). Когда pytest запускается из каталога верхнего уровня, он будет запускать все тестовые файлы, включая этот, поскольку файл имеет префикс "тест". В файле нет тестов, но он все еще запущен, так как он начинается с "теста".

Код добавит текущее имя каталога файла test_setup.py к системному пути в тестовой среде. Это будет сделано только один раз, поэтому на пути не будет добавлено ничего.

Затем из любой тестовой функции вы можете импортировать модули относительно этой папки верхнего уровня (например, import geom.region), и она знает, где ее найти, поскольку каталог src был добавлен в путь.

Если вы хотите запустить один тестовый файл (например, test_util.py) вместо всех файлов, вы должны использовать:

pytest test_setup.py test\test_util.py

Это запускает код test_setup и test_util, чтобы код test_setup все еще можно было использовать.