Тест для импорта необязательных зависимостей в __init__.py с pytest: Python 3.5 /3.6 отличается по поведению - PullRequest
0 голосов
/ 26 июня 2018

У меня есть пакет для python 3.5 и 3.6, который имеет необязательные зависимости, для которых я хочу тесты (pytest), которые выполняются на любой версии.

Я сделал приведенный ниже сокращенный пример, состоящий из двух файлов, простого __init__.py, где импортируется необязательный пакет «запросы» (просто пример) и установлен флаг для указания доступности запросов.

mypackage/
├── mypackage
│   └── __init__.py
└── test_init.py

Содержимое файла __init__.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

requests_available = True

try:
    import requests
except ImportError:
    requests_available = False

Содержимое файла test_init.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest, sys

def test_requests_missing(monkeypatch):
    import mypackage
    import copy
    fakesysmodules = copy.copy(sys.modules)
    fakesysmodules["requests"] = None
    monkeypatch.delitem(sys.modules,"requests")
    monkeypatch.setattr("sys.modules", fakesysmodules)
    from importlib import reload
    reload(mypackage)
    assert mypackage.requests_available == False


if __name__ == '__main__':
    pytest.main([__file__, "-vv", "-s"])

Тест test_requests_missing работает на Python 3.6.5:

runfile('/home/bjorn/python_packages/mypackage/test_init.py', wdir='/home/bjorn/python_packages/mypackage')
============================= test session starts ==============================
platform linux -- Python 3.6.5, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- /home/bjorn/anaconda3/envs/bjorn36/bin/python
cachedir: .pytest_cache
rootdir: /home/bjorn/python_packages/mypackage, inifile:
plugins: requests-mock-1.5.0, mock-1.10.0, cov-2.5.1, nbval-0.9.0, hypothesis-3.38.5
collecting ... collected 1 item

test_init.py::test_requests_missing PASSED

=========================== 1 passed in 0.02 seconds ===========================

Но не в Python 3.5.4:

runfile('/home/bjorn/python_packages/mypackage/test_init.py', wdir='/home/bjorn/python_packages/mypackage')
========================================================= test session starts ==========================================================
platform linux -- Python 3.5.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- /home/bjorn/anaconda3/envs/bjorn35/bin/python
cachedir: .pytest_cache
rootdir: /home/bjorn/python_packages/mypackage, inifile:
plugins: requests-mock-1.5.0, mock-1.10.0, cov-2.5.1, nbval-0.9.1, hypothesis-3.38.5
collecting ... collected 1 item

test_init.py::test_requests_missing FAILED

=============================================================== FAILURES ===============================================================
________________________________________________________ test_requests_missing _________________________________________________________

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f9a2953acc0>

    def test_requests_missing(monkeypatch):
        import mypackage
        import copy
        fakesysmodules = copy.copy(sys.modules)
        fakesysmodules["requests"] = None
        monkeypatch.delitem(sys.modules,"requests")
        monkeypatch.setattr("sys.modules", fakesysmodules)
        from importlib import reload
>       reload(mypackage)

test_init.py:13: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../anaconda3/envs/bjorn35/lib/python3.5/importlib/__init__.py:166: in reload
    _bootstrap._exec(spec, module)
<frozen importlib._bootstrap>:626: in _exec
    ???
<frozen importlib._bootstrap_external>:697: in exec_module
    ???
<frozen importlib._bootstrap>:222: in _call_with_frames_removed
    ???
mypackage/__init__.py:8: in <module>
    import requests
../../anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py:97: in <module>
    from . import utils

.... VERY LONG OUTPUT ....

    from . import utils
../../anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py:97: in <module>
    from . import utils
<frozen importlib._bootstrap>:968: in _find_and_load
    ???
<frozen importlib._bootstrap>:953: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:896: in _find_spec
    ???
<frozen importlib._bootstrap_external>:1171: in find_spec
    ???
<frozen importlib._bootstrap_external>:1145: in _get_spec
    ???
<frozen importlib._bootstrap_external>:1273: in find_spec
    ???
<frozen importlib._bootstrap_external>:1245: in _get_spec
    ???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'requests', location = '/home/bjorn/anaconda3/envs/bjorn35/lib/python3.5/site-packages/requests/__init__.py'

>   ???
E   RecursionError: maximum recursion depth exceeded

<frozen importlib._bootstrap_external>:575: RecursionError
======================================================= 1 failed in 2.01 seconds =======================================================

У меня два вопроса:

  1. Почему я вижу эту разницу? Соответствующие пакеты, по-видимому, имеют одинаковую версию на 3.5 и 3.6.

  2. Есть ли лучший способ сделать то, что я хочу? Код, который у меня сейчас есть, собран из примеров, найденных в Интернете. Я попытался пропатчить механизм импорта, чтобы избежать «перезагрузки», но мне не удалось.

Ответы [ 2 ]

0 голосов
/ 26 июня 2018

Я бы посмеялся над __import__ функцией (той, которая вызывается после оператора import modname). Пример:

import builtins
import sys
import pytest


@pytest.fixture
def no_requests(monkeypatch):
    import_orig = builtins.__import__
    def mocked_import(name, globals, locals, fromlist, level):
        if name == 'requests':
            raise ImportError()
        return import_orig(name, locals, fromlist, level)
    monkeypatch.setattr(builtins, '__import__', mocked_import)


@pytest.fixture(autouse=True)
def cleanup_imports():
    yield
    sys.modules.pop('mypackage', None)


def test_requests_available():
    import mypackage
    assert mypackage.requests_available


@pytest.mark.usefixtures('no_requests')
def test_requests_missing():
    import mypackage
    assert not mypackage.requests_available

Приспособление no_requests отвечает за замену функции __import__ на функцию, которая будет вызываться при попытке import requests, и отлично работает с остальным импортом (мы не можем поднять при любом импорте, или pytest сам будет перерыв). cleanup_imports просто для гарантии того, что mypackage будет повторно импортироваться в каждом тесте.

0 голосов
/ 26 июня 2018

Если тест проверяет дополнительную функциональность, его следует пропустить, а не пропустить, если эта функциональность отсутствует.

test.support.import_module() - это функция, используемая в пакете автотестов Python для пропуска теста или файла теста, если модуль отсутствует:

import test.support
import unittest

nonexistent = test.support.import_module("nonexistent")

class TestDummy(unittest.testCase):
    def test_dummy():
        self.assertTrue(nonexistent.vaporware())

Затем при запуске:

> python -m py.test -rs t.py

<...>
collected 0 items / 1 skipped

=========================== short test summary info ===========================
SKIP [1] C:\Python27\lib\test\support\__init__.py:90: SkipTest: No module named
nonexistent
========================== 1 skipped in 0.05 seconds ==========================
...