Support for pytest 6 "--import-mode=importlib" (#384)

* Fix compatibility with pytest 6 "--import-mode=importlib"

* Rewrite `scenario` and `scenarios` tests so that we can easily parametrize with the different pytest --import-mode.

* Update changelog
This commit is contained in:
Alessio Bogon 2020-09-07 09:43:29 +02:00 committed by GitHub
parent 882291524b
commit 853c615748
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 73 deletions

View File

@ -1,14 +1,16 @@
Changelog
=========
This relase introduces breaking changes, please refer to the :ref:`Migration from 3.x.x`.
This release introduces breaking changes, please refer to the :ref:`Migration from 3.x.x`.
- Strict Gherkin option is removed. (olegpidsadnyi)
- Strict Gherkin option is removed (``@scenario()`` does not accept the ``strict_gherkin`` parameter). (olegpidsadnyi)
- ``@scenario()`` does not accept the undocumented parameter ``caller_module`` anymore. (youtux)
- Given step is no longer a fixture. The scope parameter is also removed. (olegpidsadnyi)
- Fixture parameter is removed from the given step declaration. (olegpidsadnyi)
- ``pytest_bdd_step_validation_error`` hook is removed. (olegpidsadnyi)
- Fix an error with pytest-pylint plugin #374. (toracle)
- Fix pytest-xdist 2.0 compatibility #369. (olegpidsadnyi)
- Fix compatibility with pytest 6 ``--import-mode=importlib`` option. (youtux)
3.4.0

View File

@ -14,6 +14,7 @@ import collections
import inspect
import os
import re
import sys
import pytest
@ -24,9 +25,8 @@ except ImportError:
from . import exceptions
from .feature import Feature, force_unicode, get_features
from .steps import get_caller_module, get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args
from .steps import get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
PYTHON_REPLACE_REGEX = re.compile(r"\W")
ALPHA_REGEX = re.compile(r"^\d+_*")
@ -197,14 +197,7 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, enco
return decorator
def scenario(
feature_name,
scenario_name,
encoding="utf-8",
example_converters=None,
caller_module=None,
features_base_dir=None,
):
def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None):
"""Scenario decorator.
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
@ -215,11 +208,11 @@ def scenario(
"""
scenario_name = force_unicode(scenario_name, encoding)
caller_module = caller_module or get_caller_module()
caller_module_path = get_caller_module_path()
# Get the feature
if features_base_dir is None:
features_base_dir = get_features_base_dir(caller_module)
features_base_dir = get_features_base_dir(caller_module_path)
feature = Feature.get_feature(features_base_dir, feature_name, encoding=encoding)
# Get the scenario
@ -242,8 +235,8 @@ def scenario(
)
def get_features_base_dir(caller_module):
default_base_dir = os.path.dirname(caller_module.__file__)
def get_features_base_dir(caller_module_path):
default_base_dir = os.path.dirname(caller_module_path)
return get_from_ini("bdd_features_base_dir", default_base_dir)
@ -293,12 +286,12 @@ def scenarios(*feature_paths, **kwargs):
:param *feature_paths: feature file paths to use for scenarios
"""
frame = inspect.stack()[1]
module = inspect.getmodule(frame[0])
caller_locals = get_caller_module_locals()
caller_path = get_caller_module_path()
features_base_dir = kwargs.get("features_base_dir")
if features_base_dir is None:
features_base_dir = get_features_base_dir(module)
features_base_dir = get_features_base_dir(caller_path)
abs_feature_paths = []
for path in feature_paths:
@ -309,7 +302,7 @@ def scenarios(*feature_paths, **kwargs):
module_scenarios = frozenset(
(attr.__scenario__.feature.filename, attr.__scenario__.name)
for name, attr in module.__dict__.items()
for name, attr in caller_locals.items()
if hasattr(attr, "__scenario__")
)
@ -323,9 +316,9 @@ def scenarios(*feature_paths, **kwargs):
pass # pragma: no cover
for test_name in get_python_name_generator(scenario_name):
if test_name not in module.__dict__:
if test_name not in caller_locals:
# found an unique test name
module.__dict__[test_name] = _scenario
caller_locals[test_name] = _scenario
break
found = True
if not found:

View File

@ -37,7 +37,6 @@ def given_beautiful_article(article):
from __future__ import absolute_import
import inspect
import sys
import pytest
@ -49,7 +48,7 @@ except ImportError:
from .feature import force_encode
from .types import GIVEN, WHEN, THEN
from .parsers import get_parser
from .utils import get_args
from .utils import get_args, get_caller_module_locals
def get_step_fixture_name(name, type_, encoding=None):
@ -137,21 +136,15 @@ def _step_decorator(step_type, step_name, converters=None, target_fixture=None):
step_func.target_fixture = lazy_step_func.target_fixture = target_fixture
lazy_step_func = pytest.fixture()(lazy_step_func)
setattr(get_caller_module(), get_step_fixture_name(parsed_step_name, step_type), lazy_step_func)
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
caller_locals = get_caller_module_locals()
caller_locals[fixture_step_name] = lazy_step_func
return func
return decorator
def get_caller_module(depth=2):
"""Return the module of the caller."""
frame = sys._getframe(depth)
module = inspect.getmodule(frame)
if module is None:
return get_caller_module(depth=depth)
return module
def inject_fixture(request, arg, value):
"""Inject fixture into pytest fixture request.

View File

@ -2,6 +2,8 @@
import inspect
import six
CONFIG_STACK = []
@ -17,12 +19,12 @@ def get_args(func):
:return: A list of argument names.
:rtype: list
"""
if hasattr(inspect, "signature"):
params = inspect.signature(func).parameters.values()
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
else:
if six.PY2:
return inspect.getargspec(func).args
params = inspect.signature(func).parameters.values()
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
def get_parametrize_markers_args(node):
"""In pytest 3.6 new API to access markers has been introduced and it deprecated
@ -31,3 +33,14 @@ def get_parametrize_markers_args(node):
This function uses that API if it is available otherwise it uses MarkInfo objects.
"""
return tuple(arg for mark in node.iter_markers("parametrize") for arg in mark.args)
def get_caller_module_locals(depth=2):
frame_info = inspect.stack()[depth]
frame = frame_info[0] # frame_info.frame
return frame.f_locals
def get_caller_module_path(depth=2):
frame_info = inspect.stack()[depth]
return frame_info[1] # frame_info.filename

View File

@ -1 +1,23 @@
from __future__ import absolute_import, unicode_literals
import pytest
from tests.utils import PYTEST_6
pytest_plugins = "pytester"
def pytest_generate_tests(metafunc):
if "pytest_params" in metafunc.fixturenames:
if PYTEST_6:
parametrizations = [
pytest.param([], id="no-import-mode"),
pytest.param(["--import-mode=prepend"], id="--import-mode=prepend"),
pytest.param(["--import-mode=append"], id="--import-mode=append"),
pytest.param(["--import-mode=importlib"], id="--import-mode=importlib"),
]
else:
parametrizations = [[]]
metafunc.parametrize(
"pytest_params",
parametrizations,
)

View File

@ -5,7 +5,7 @@ import textwrap
from tests.utils import assert_outcomes
def test_scenario_not_found(testdir):
def test_scenario_not_found(testdir, pytest_params):
"""Test the situation when scenario is not found."""
testdir.makefile(
".feature",
@ -30,7 +30,7 @@ def test_scenario_not_found(testdir):
"""
)
)
result = testdir.runpytest()
result = testdir.runpytest_subprocess(*pytest_params)
assert_outcomes(result, errors=1)
result.stdout.fnmatch_lines('*Scenario "NOT FOUND" in feature "Scenario is not found" in*')
@ -90,8 +90,12 @@ def test_scenario_comments(testdir):
)
)
result = testdir.runpytest()
def test_scenario_not_decorator(testdir):
result.assert_outcomes(passed=2)
def test_scenario_not_decorator(testdir, pytest_params):
"""Test scenario function is used not as decorator."""
testdir.makefile(
".feature",
@ -109,7 +113,38 @@ def test_scenario_not_decorator(testdir):
"""
)
result = testdir.runpytest()
result = testdir.runpytest_subprocess(*pytest_params)
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines("*ScenarioIsDecoratorOnly: scenario function can only be used as a decorator*")
def test_simple(testdir, pytest_params):
"""Test scenario decorator with a standard usage."""
testdir.makefile(
".feature",
simple="""
Feature: Simple feature
Scenario: Simple scenario
Given I have a bar
""",
)
testdir.makepyfile(
"""
from pytest_bdd import scenario, given, then
@scenario("simple.feature", "Simple scenario")
def test_simple():
pass
@given("I have a bar")
def bar():
return "bar"
@then("pass")
def bar():
pass
"""
)
result = testdir.runpytest_subprocess(*pytest_params)
result.assert_outcomes(passed=1)

View File

@ -1,9 +1,11 @@
"""Test scenarios shortcut."""
import textwrap
from tests.utils import assert_outcomes
def test_scenarios(testdir):
"""Test scenarios shortcut."""
def test_scenarios(testdir, pytest_params):
"""Test scenarios shortcut (used together with @scenario for individual test override)."""
testdir.makeini(
"""
[pytest]
@ -63,7 +65,8 @@ def test_scenarios(testdir):
scenarios('features')
"""
)
result = testdir.runpytest("-v", "-s")
result = testdir.runpytest_subprocess("-v", "-s", *pytest_params)
assert_outcomes(result, passed=4, failed=1)
result.stdout.fnmatch_lines(["*collected 5 items"])
result.stdout.fnmatch_lines(["*test_test_subfolder_scenario *bar!", "PASSED"])
result.stdout.fnmatch_lines(["*test_test_scenario *bar!", "PASSED"])
@ -72,7 +75,7 @@ def test_scenarios(testdir):
result.stdout.fnmatch_lines(["*test_test_scenario_1 *bar!", "PASSED"])
def test_scenarios_none_found(testdir):
def test_scenarios_none_found(testdir, pytest_params):
"""Test scenarios shortcut when no scenarios found."""
testpath = testdir.makepyfile(
"""
@ -82,6 +85,6 @@ def test_scenarios_none_found(testdir):
scenarios('.')
"""
)
reprec = testdir.inline_run(testpath)
reprec.assertoutcome(failed=1)
assert "NoScenariosFound" in str(reprec.getreports()[1].longrepr)
result = testdir.runpytest_subprocess(testpath, *pytest_params)
assert_outcomes(result, errors=1)
result.stdout.fnmatch_lines(["*NoScenariosFound*"])

View File

@ -53,12 +53,11 @@ def test_preserve_decorator(testdir, step, keyword):
from pytest_bdd import {step}
from pytest_bdd.steps import get_step_fixture_name
@{step}("{keyword}")
def func():
"""Doc string."""
def test_decorator():
@{step}("{keyword}")
def func():
"""Doc string."""
assert globals()[get_step_fixture_name("{keyword}", {step}.__name__)].__doc__ == "Doc string."

View File

@ -4,10 +4,27 @@ import pytest
from packaging.utils import Version
PYTEST_VERSION = Version(pytest.__version__)
PYTEST_6 = PYTEST_VERSION >= Version("6")
_errors_key = "error" if PYTEST_VERSION < Version("6") else "errors"
if PYTEST_VERSION < Version("6"):
if PYTEST_6:
def assert_outcomes(
result,
passed=0,
skipped=0,
failed=0,
errors=0,
xpassed=0,
xfailed=0,
):
"""Compatibility function for result.assert_outcomes"""
return result.assert_outcomes(
errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed
)
else:
def assert_outcomes(
result,
@ -27,20 +44,3 @@ if PYTEST_VERSION < Version("6"):
xpassed=xpassed,
xfailed=xfailed,
)
else:
def assert_outcomes(
result,
passed=0,
skipped=0,
failed=0,
errors=0,
xpassed=0,
xfailed=0,
):
"""Compatibility function for result.assert_outcomes"""
return result.assert_outcomes(
errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed
)