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:
parent
882291524b
commit
853c615748
|
@ -1,14 +1,16 @@
|
||||||
Changelog
|
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)
|
- Given step is no longer a fixture. The scope parameter is also removed. (olegpidsadnyi)
|
||||||
- Fixture parameter is removed from the given step declaration. (olegpidsadnyi)
|
- Fixture parameter is removed from the given step declaration. (olegpidsadnyi)
|
||||||
- ``pytest_bdd_step_validation_error`` hook is removed. (olegpidsadnyi)
|
- ``pytest_bdd_step_validation_error`` hook is removed. (olegpidsadnyi)
|
||||||
- Fix an error with pytest-pylint plugin #374. (toracle)
|
- Fix an error with pytest-pylint plugin #374. (toracle)
|
||||||
- Fix pytest-xdist 2.0 compatibility #369. (olegpidsadnyi)
|
- Fix pytest-xdist 2.0 compatibility #369. (olegpidsadnyi)
|
||||||
|
- Fix compatibility with pytest 6 ``--import-mode=importlib`` option. (youtux)
|
||||||
|
|
||||||
|
|
||||||
3.4.0
|
3.4.0
|
||||||
|
|
|
@ -14,6 +14,7 @@ import collections
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -24,9 +25,8 @@ except ImportError:
|
||||||
|
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
from .feature import Feature, force_unicode, get_features
|
from .feature import Feature, force_unicode, get_features
|
||||||
from .steps import get_caller_module, get_step_fixture_name, inject_fixture
|
from .steps import get_step_fixture_name, inject_fixture
|
||||||
from .utils import CONFIG_STACK, get_args
|
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||||
|
|
||||||
|
|
||||||
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||||
|
@ -197,14 +197,7 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, enco
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def scenario(
|
def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None):
|
||||||
feature_name,
|
|
||||||
scenario_name,
|
|
||||||
encoding="utf-8",
|
|
||||||
example_converters=None,
|
|
||||||
caller_module=None,
|
|
||||||
features_base_dir=None,
|
|
||||||
):
|
|
||||||
"""Scenario decorator.
|
"""Scenario decorator.
|
||||||
|
|
||||||
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
|
: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)
|
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
|
# Get the feature
|
||||||
if features_base_dir is None:
|
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)
|
feature = Feature.get_feature(features_base_dir, feature_name, encoding=encoding)
|
||||||
|
|
||||||
# Get the scenario
|
# Get the scenario
|
||||||
|
@ -242,8 +235,8 @@ def scenario(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_features_base_dir(caller_module):
|
def get_features_base_dir(caller_module_path):
|
||||||
default_base_dir = os.path.dirname(caller_module.__file__)
|
default_base_dir = os.path.dirname(caller_module_path)
|
||||||
return get_from_ini("bdd_features_base_dir", default_base_dir)
|
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
|
:param *feature_paths: feature file paths to use for scenarios
|
||||||
"""
|
"""
|
||||||
frame = inspect.stack()[1]
|
caller_locals = get_caller_module_locals()
|
||||||
module = inspect.getmodule(frame[0])
|
caller_path = get_caller_module_path()
|
||||||
|
|
||||||
features_base_dir = kwargs.get("features_base_dir")
|
features_base_dir = kwargs.get("features_base_dir")
|
||||||
if features_base_dir is None:
|
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 = []
|
abs_feature_paths = []
|
||||||
for path in feature_paths:
|
for path in feature_paths:
|
||||||
|
@ -309,7 +302,7 @@ def scenarios(*feature_paths, **kwargs):
|
||||||
|
|
||||||
module_scenarios = frozenset(
|
module_scenarios = frozenset(
|
||||||
(attr.__scenario__.feature.filename, attr.__scenario__.name)
|
(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__")
|
if hasattr(attr, "__scenario__")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -323,9 +316,9 @@ def scenarios(*feature_paths, **kwargs):
|
||||||
pass # pragma: no cover
|
pass # pragma: no cover
|
||||||
|
|
||||||
for test_name in get_python_name_generator(scenario_name):
|
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
|
# found an unique test name
|
||||||
module.__dict__[test_name] = _scenario
|
caller_locals[test_name] = _scenario
|
||||||
break
|
break
|
||||||
found = True
|
found = True
|
||||||
if not found:
|
if not found:
|
||||||
|
|
|
@ -37,7 +37,6 @@ def given_beautiful_article(article):
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -49,7 +48,7 @@ except ImportError:
|
||||||
from .feature import force_encode
|
from .feature import force_encode
|
||||||
from .types import GIVEN, WHEN, THEN
|
from .types import GIVEN, WHEN, THEN
|
||||||
from .parsers import get_parser
|
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):
|
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
|
step_func.target_fixture = lazy_step_func.target_fixture = target_fixture
|
||||||
|
|
||||||
lazy_step_func = pytest.fixture()(lazy_step_func)
|
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 func
|
||||||
|
|
||||||
return decorator
|
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):
|
def inject_fixture(request, arg, value):
|
||||||
"""Inject fixture into pytest fixture request.
|
"""Inject fixture into pytest fixture request.
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
CONFIG_STACK = []
|
CONFIG_STACK = []
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,12 +19,12 @@ def get_args(func):
|
||||||
:return: A list of argument names.
|
:return: A list of argument names.
|
||||||
:rtype: list
|
:rtype: list
|
||||||
"""
|
"""
|
||||||
if hasattr(inspect, "signature"):
|
if six.PY2:
|
||||||
params = inspect.signature(func).parameters.values()
|
|
||||||
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
|
|
||||||
else:
|
|
||||||
return inspect.getargspec(func).args
|
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):
|
def get_parametrize_markers_args(node):
|
||||||
"""In pytest 3.6 new API to access markers has been introduced and it deprecated
|
"""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.
|
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)
|
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
|
||||||
|
|
|
@ -1 +1,23 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.utils import PYTEST_6
|
||||||
|
|
||||||
pytest_plugins = "pytester"
|
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,
|
||||||
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import textwrap
|
||||||
from tests.utils import assert_outcomes
|
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."""
|
"""Test the situation when scenario is not found."""
|
||||||
testdir.makefile(
|
testdir.makefile(
|
||||||
".feature",
|
".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)
|
assert_outcomes(result, errors=1)
|
||||||
result.stdout.fnmatch_lines('*Scenario "NOT FOUND" in feature "Scenario is not found" in*')
|
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."""
|
"""Test scenario function is used not as decorator."""
|
||||||
testdir.makefile(
|
testdir.makefile(
|
||||||
".feature",
|
".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.assert_outcomes(failed=1)
|
||||||
result.stdout.fnmatch_lines("*ScenarioIsDecoratorOnly: scenario function can only be used as a decorator*")
|
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)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Test scenarios shortcut."""
|
"""Test scenarios shortcut."""
|
||||||
import textwrap
|
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(
|
testdir.makeini(
|
||||||
"""
|
"""
|
||||||
[pytest]
|
[pytest]
|
||||||
|
@ -63,7 +65,8 @@ def test_scenarios(testdir):
|
||||||
scenarios('features')
|
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(["*collected 5 items"])
|
||||||
result.stdout.fnmatch_lines(["*test_test_subfolder_scenario *bar!", "PASSED"])
|
result.stdout.fnmatch_lines(["*test_test_subfolder_scenario *bar!", "PASSED"])
|
||||||
result.stdout.fnmatch_lines(["*test_test_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"])
|
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."""
|
"""Test scenarios shortcut when no scenarios found."""
|
||||||
testpath = testdir.makepyfile(
|
testpath = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -82,6 +85,6 @@ def test_scenarios_none_found(testdir):
|
||||||
scenarios('.')
|
scenarios('.')
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
reprec = testdir.inline_run(testpath)
|
result = testdir.runpytest_subprocess(testpath, *pytest_params)
|
||||||
reprec.assertoutcome(failed=1)
|
assert_outcomes(result, errors=1)
|
||||||
assert "NoScenariosFound" in str(reprec.getreports()[1].longrepr)
|
result.stdout.fnmatch_lines(["*NoScenariosFound*"])
|
||||||
|
|
|
@ -53,12 +53,11 @@ def test_preserve_decorator(testdir, step, keyword):
|
||||||
from pytest_bdd import {step}
|
from pytest_bdd import {step}
|
||||||
from pytest_bdd.steps import get_step_fixture_name
|
from pytest_bdd.steps import get_step_fixture_name
|
||||||
|
|
||||||
|
@{step}("{keyword}")
|
||||||
|
def func():
|
||||||
|
"""Doc string."""
|
||||||
|
|
||||||
def test_decorator():
|
def test_decorator():
|
||||||
@{step}("{keyword}")
|
|
||||||
def func():
|
|
||||||
"""Doc string."""
|
|
||||||
|
|
||||||
assert globals()[get_step_fixture_name("{keyword}", {step}.__name__)].__doc__ == "Doc string."
|
assert globals()[get_step_fixture_name("{keyword}", {step}.__name__)].__doc__ == "Doc string."
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,27 @@ import pytest
|
||||||
from packaging.utils import Version
|
from packaging.utils import Version
|
||||||
|
|
||||||
PYTEST_VERSION = Version(pytest.__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(
|
def assert_outcomes(
|
||||||
result,
|
result,
|
||||||
|
@ -27,20 +44,3 @@ if PYTEST_VERSION < Version("6"):
|
||||||
xpassed=xpassed,
|
xpassed=xpassed,
|
||||||
xfailed=xfailed,
|
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
|
|
||||||
)
|
|
||||||
|
|
Loading…
Reference in New Issue