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
|
||||
=========
|
||||
|
||||
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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*"])
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue