Как Pytest проект, использующий пакеты пространства имен PEP 420? - PullRequest
0 голосов
/ 04 мая 2018

Я пытаюсь использовать Pytest для тестирования крупного проекта (~ 100k LOC, 1k файлов), и есть несколько других похожих проектов, для которых мне бы хотелось в конечном итоге сделать то же самое. Это не стандартный пакет Python ; это часть сильно настроенной системы, которую я мало что могу изменить, по крайней мере, в ближайшей перспективе. Тестовые модули интегрированы с кодом, а не находятся в отдельном каталоге, и это важно для нас. Конфигурация очень похожа на этот вопрос и мой ответ , что также может дать полезную информацию.

Проблема, с которой я сталкиваюсь, заключается в том, что проекты используют неявные пакеты пространства имен PEP 420 почти исключительно; то есть, в любом из каталогов пакетов почти нет файлов __init__.py. Я еще не видел случаев, когда пакеты имели в качестве пакетов пространства имен, но, учитывая, что этот проект объединен с другими проектами, которые также имеют код Python, это может произойти (или уже происходит, и я ' мы просто этого не заметили).

Рассмотрим хранилище, которое выглядит следующим образом. (Для работоспособной его копии, включая тесты, описанные ниже, клон 0cjs/pytest-impl-ns-pkg от GitHub.) Предполагается, что все тесты ниже находятся в project/thing/thing_test.py.

repo/
    project/
        util/
            thing.py
            thing_test.py

У меня достаточно контроля над конфигурациями тестирования, поэтому я могу убедиться, что sys.path настроен правильно для правильной работы импорта тестируемого кода. То есть пройдёт следующий тест:

def test_good_import():
    import project.util.thing

Однако Pytest определяет имена пакетов из файлов, используя свою обычную систему , предоставляя имена пакетов, которые не являются стандартными для моей конфигурации, и добавляя подкаталоги моего проекта в sys.path. Поэтому следующие два теста не пройдены:

def test_modulename():
    assert 'project.util.thing_test' == __name__
    # Result: AssertionError: assert 'project.util.thing_test' == 'thing_test'

def test_bad_import():
    ''' While we have a `project.util.thing` deep in our hierarchy, we do
        not have a top-level `thing` module, so this import should fail.
    '''
    with raises(ImportError):
        import thing
    # Result: Failed: DID NOT RAISE <class 'ImportError'>

Как вы можете видеть, хотя thing.py всегда можно импортировать как project.util.thing, thing_test.py это project.util.thing_test вне Pytest, но при запуске Pytest project/util добавляется к sys.path и модуль по имени thing_test.

Это создает ряд проблем:

  1. Коллизии пространства имен модуля (например, между project/util/thing_test.py и project/otherstuff/thing_test.py).
  2. Неправильные операторы импорта не обнаруживаются, поскольку тестируемый код также использует эти непроизводственные пути импорта.
  3. Относительный импорт может не работать в тестовом коде, поскольку модуль был «перемещен» в иерархии.
  4. В общем, я довольно нервничаю из-за того, что в тестирование добавляется большое количество дополнительных путей к sys.path, которые будут отсутствовать при производстве, так как я вижу в этом большую вероятность ошибок. Но давайте назовем это первым (и на данный момент, я думаю, стандартным) вариантом.

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

Третий вариант (после того, как вы просто живете с текущей ситуацией и меняете pytest, как указано выше), просто добавляет в проект десятки __init__.py файлов. Однако, хотя с использованием extend_path в них будет (я думаю) иметь дело с проблемой пространства имен по сравнению с обычными пакетами в обычном мире Python, я думаю, что это сломает нашу необычную систему выпуска пакетов, объявленных в нескольких проектах. , (То есть, если бы другой проект имел модуль project.util.other и был объединен для выпуска с нашим проектом, столкновение между их project/util/__init__.py и нашим project/util/__init__.py было бы серьезной проблемой.) Исправление этого было бы серьезной проблемой, так как мы пришлось бы, помимо прочего, добавить какой-то способ объявить, что некоторые каталоги, содержащие __init__.py, на самом деле являются пакетами пространства имен.

Есть ли способы улучшить вышеуказанные опции? Есть ли другие варианты, которые я пропускаю?

1 Ответ

0 голосов
/ 04 мая 2018

Проблема, с которой вы сталкиваетесь, заключается в том, что вы помещаете тесты вне производственного кода в пакеты пространства имен. Как указано здесь , pytest распознает ваши настройки как отдельные тестовые модули:

Автономные тестовые модули / файлы conftest.py

...

pytest найдет foo/bar/tests/test_foo.py и поймет, что это НЕ часть пакета, учитывая, что в этой папке нет файла __init__.py. Затем он добавит root/foo/bar/tests к sys.path, чтобы импортировать test_foo.py в качестве модуля test_foo. То же самое можно сделать с файлом conftest.py, добавив root/foo к sys.path, чтобы импортировать его как conftest.

Таким образом, правильный способ решить (хотя бы часть) этого состоит в том, чтобы настроить sys.path и отделить тесты от производственного кода, например, перемещение тестового модуля thing_test.py в отдельный каталог project/util/tests. Поскольку вы не можете этого сделать, у вас нет другого выбора, кроме как связываться с внутренностями pytest (так как вы не сможете переопределить поведение импорта модуля через ловушки). Вот предложение: создайте repo/conftest.py с пропатченным LocalPath классом:

# repo/conftest.py

import pathlib
import py._path.local


# the original pypkgpath method can't deal with namespace packages,
# considering only dirs with __init__.py as packages
pypkgpath_orig = py._path.local.LocalPath.pypkgpath

# we consider all dirs in repo/ to be namespace packages
rootdir = pathlib.Path(__file__).parent.resolve()
namespace_pkg_dirs = [str(d) for d in rootdir.iterdir() if d.is_dir()]

# patched method
def pypkgpath(self):
    # call original lookup
    pkgpath = pypkgpath_orig(self)
    if pkgpath is not None:
        return pkgpath
    # original lookup failed, check if we are subdir of a namespace package
    # if yes, return the namespace package we belong to
    for parent in self.parts(reverse=True):
        if str(parent) in namespace_pkg_dirs:
            return parent
    return None

# apply patch
py._path.local.LocalPath.pypkgpath = pypkgpath
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...