Skip to content

Commit 853c615

Browse files
authored
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
1 parent 8822915 commit 853c615

File tree

9 files changed

+123
-63
lines changed

9 files changed

+123
-63
lines changed

CHANGES.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
Changelog
22
=========
33

4-
This relase introduces breaking changes, please refer to the :ref:`Migration from 3.x.x`.
4+
This release introduces breaking changes, please refer to the :ref:`Migration from 3.x.x`.
55

6-
- Strict Gherkin option is removed. (olegpidsadnyi)
6+
- Strict Gherkin option is removed (``@scenario()`` does not accept the ``strict_gherkin`` parameter). (olegpidsadnyi)
7+
- ``@scenario()`` does not accept the undocumented parameter ``caller_module`` anymore. (youtux)
78
- Given step is no longer a fixture. The scope parameter is also removed. (olegpidsadnyi)
89
- Fixture parameter is removed from the given step declaration. (olegpidsadnyi)
910
- ``pytest_bdd_step_validation_error`` hook is removed. (olegpidsadnyi)
1011
- Fix an error with pytest-pylint plugin #374. (toracle)
1112
- Fix pytest-xdist 2.0 compatibility #369. (olegpidsadnyi)
13+
- Fix compatibility with pytest 6 ``--import-mode=importlib`` option. (youtux)
1214

1315

1416
3.4.0

pytest_bdd/scenario.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import inspect
1515
import os
1616
import re
17+
import sys
1718

1819
import pytest
1920

@@ -24,9 +25,8 @@
2425

2526
from . import exceptions
2627
from .feature import Feature, force_unicode, get_features
27-
from .steps import get_caller_module, get_step_fixture_name, inject_fixture
28-
from .utils import CONFIG_STACK, get_args
29-
28+
from .steps import get_step_fixture_name, inject_fixture
29+
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
3030

3131
PYTHON_REPLACE_REGEX = re.compile(r"\W")
3232
ALPHA_REGEX = re.compile(r"^\d+_*")
@@ -197,14 +197,7 @@ def scenario_wrapper(request):
197197
return decorator
198198

199199

200-
def scenario(
201-
feature_name,
202-
scenario_name,
203-
encoding="utf-8",
204-
example_converters=None,
205-
caller_module=None,
206-
features_base_dir=None,
207-
):
200+
def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None):
208201
"""Scenario decorator.
209202
210203
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
@@ -215,11 +208,11 @@ def scenario(
215208
"""
216209

217210
scenario_name = force_unicode(scenario_name, encoding)
218-
caller_module = caller_module or get_caller_module()
211+
caller_module_path = get_caller_module_path()
219212

220213
# Get the feature
221214
if features_base_dir is None:
222-
features_base_dir = get_features_base_dir(caller_module)
215+
features_base_dir = get_features_base_dir(caller_module_path)
223216
feature = Feature.get_feature(features_base_dir, feature_name, encoding=encoding)
224217

225218
# Get the scenario
@@ -242,8 +235,8 @@ def scenario(
242235
)
243236

244237

245-
def get_features_base_dir(caller_module):
246-
default_base_dir = os.path.dirname(caller_module.__file__)
238+
def get_features_base_dir(caller_module_path):
239+
default_base_dir = os.path.dirname(caller_module_path)
247240
return get_from_ini("bdd_features_base_dir", default_base_dir)
248241

249242

@@ -293,12 +286,12 @@ def scenarios(*feature_paths, **kwargs):
293286
294287
:param *feature_paths: feature file paths to use for scenarios
295288
"""
296-
frame = inspect.stack()[1]
297-
module = inspect.getmodule(frame[0])
289+
caller_locals = get_caller_module_locals()
290+
caller_path = get_caller_module_path()
298291

299292
features_base_dir = kwargs.get("features_base_dir")
300293
if features_base_dir is None:
301-
features_base_dir = get_features_base_dir(module)
294+
features_base_dir = get_features_base_dir(caller_path)
302295

303296
abs_feature_paths = []
304297
for path in feature_paths:
@@ -309,7 +302,7 @@ def scenarios(*feature_paths, **kwargs):
309302

310303
module_scenarios = frozenset(
311304
(attr.__scenario__.feature.filename, attr.__scenario__.name)
312-
for name, attr in module.__dict__.items()
305+
for name, attr in caller_locals.items()
313306
if hasattr(attr, "__scenario__")
314307
)
315308

@@ -323,9 +316,9 @@ def _scenario():
323316
pass # pragma: no cover
324317

325318
for test_name in get_python_name_generator(scenario_name):
326-
if test_name not in module.__dict__:
319+
if test_name not in caller_locals:
327320
# found an unique test name
328-
module.__dict__[test_name] = _scenario
321+
caller_locals[test_name] = _scenario
329322
break
330323
found = True
331324
if not found:

pytest_bdd/steps.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def given_beautiful_article(article):
3737

3838
from __future__ import absolute_import
3939
import inspect
40-
import sys
4140

4241
import pytest
4342

@@ -49,7 +48,7 @@ def given_beautiful_article(article):
4948
from .feature import force_encode
5049
from .types import GIVEN, WHEN, THEN
5150
from .parsers import get_parser
52-
from .utils import get_args
51+
from .utils import get_args, get_caller_module_locals
5352

5453

5554
def get_step_fixture_name(name, type_, encoding=None):
@@ -137,21 +136,15 @@ def lazy_step_func():
137136
step_func.target_fixture = lazy_step_func.target_fixture = target_fixture
138137

139138
lazy_step_func = pytest.fixture()(lazy_step_func)
140-
setattr(get_caller_module(), get_step_fixture_name(parsed_step_name, step_type), lazy_step_func)
139+
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
140+
141+
caller_locals = get_caller_module_locals()
142+
caller_locals[fixture_step_name] = lazy_step_func
141143
return func
142144

143145
return decorator
144146

145147

146-
def get_caller_module(depth=2):
147-
"""Return the module of the caller."""
148-
frame = sys._getframe(depth)
149-
module = inspect.getmodule(frame)
150-
if module is None:
151-
return get_caller_module(depth=depth)
152-
return module
153-
154-
155148
def inject_fixture(request, arg, value):
156149
"""Inject fixture into pytest fixture request.
157150

pytest_bdd/utils.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import inspect
44

5+
import six
6+
57
CONFIG_STACK = []
68

79

@@ -17,12 +19,12 @@ def get_args(func):
1719
:return: A list of argument names.
1820
:rtype: list
1921
"""
20-
if hasattr(inspect, "signature"):
21-
params = inspect.signature(func).parameters.values()
22-
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
23-
else:
22+
if six.PY2:
2423
return inspect.getargspec(func).args
2524

25+
params = inspect.signature(func).parameters.values()
26+
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
27+
2628

2729
def get_parametrize_markers_args(node):
2830
"""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):
3133
This function uses that API if it is available otherwise it uses MarkInfo objects.
3234
"""
3335
return tuple(arg for mark in node.iter_markers("parametrize") for arg in mark.args)
36+
37+
38+
def get_caller_module_locals(depth=2):
39+
frame_info = inspect.stack()[depth]
40+
frame = frame_info[0] # frame_info.frame
41+
return frame.f_locals
42+
43+
44+
def get_caller_module_path(depth=2):
45+
frame_info = inspect.stack()[depth]
46+
return frame_info[1] # frame_info.filename

tests/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
1+
from __future__ import absolute_import, unicode_literals
2+
import pytest
3+
4+
from tests.utils import PYTEST_6
5+
16
pytest_plugins = "pytester"
7+
8+
9+
def pytest_generate_tests(metafunc):
10+
if "pytest_params" in metafunc.fixturenames:
11+
if PYTEST_6:
12+
parametrizations = [
13+
pytest.param([], id="no-import-mode"),
14+
pytest.param(["--import-mode=prepend"], id="--import-mode=prepend"),
15+
pytest.param(["--import-mode=append"], id="--import-mode=append"),
16+
pytest.param(["--import-mode=importlib"], id="--import-mode=importlib"),
17+
]
18+
else:
19+
parametrizations = [[]]
20+
metafunc.parametrize(
21+
"pytest_params",
22+
parametrizations,
23+
)

tests/feature/test_scenario.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from tests.utils import assert_outcomes
66

77

8-
def test_scenario_not_found(testdir):
8+
def test_scenario_not_found(testdir, pytest_params):
99
"""Test the situation when scenario is not found."""
1010
testdir.makefile(
1111
".feature",
@@ -30,7 +30,7 @@ def test_not_found():
3030
"""
3131
)
3232
)
33-
result = testdir.runpytest()
33+
result = testdir.runpytest_subprocess(*pytest_params)
3434

3535
assert_outcomes(result, errors=1)
3636
result.stdout.fnmatch_lines('*Scenario "NOT FOUND" in feature "Scenario is not found" in*')
@@ -90,8 +90,12 @@ def a_comment(acomment):
9090
)
9191
)
9292

93+
result = testdir.runpytest()
94+
95+
result.assert_outcomes(passed=2)
96+
9397

94-
def test_scenario_not_decorator(testdir):
98+
def test_scenario_not_decorator(testdir, pytest_params):
9599
"""Test scenario function is used not as decorator."""
96100
testdir.makefile(
97101
".feature",
@@ -109,7 +113,38 @@ def test_scenario_not_decorator(testdir):
109113
"""
110114
)
111115

112-
result = testdir.runpytest()
116+
result = testdir.runpytest_subprocess(*pytest_params)
113117

114118
result.assert_outcomes(failed=1)
115119
result.stdout.fnmatch_lines("*ScenarioIsDecoratorOnly: scenario function can only be used as a decorator*")
120+
121+
122+
def test_simple(testdir, pytest_params):
123+
"""Test scenario decorator with a standard usage."""
124+
testdir.makefile(
125+
".feature",
126+
simple="""
127+
Feature: Simple feature
128+
Scenario: Simple scenario
129+
Given I have a bar
130+
""",
131+
)
132+
testdir.makepyfile(
133+
"""
134+
from pytest_bdd import scenario, given, then
135+
136+
@scenario("simple.feature", "Simple scenario")
137+
def test_simple():
138+
pass
139+
140+
@given("I have a bar")
141+
def bar():
142+
return "bar"
143+
144+
@then("pass")
145+
def bar():
146+
pass
147+
"""
148+
)
149+
result = testdir.runpytest_subprocess(*pytest_params)
150+
result.assert_outcomes(passed=1)

tests/feature/test_scenarios.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Test scenarios shortcut."""
22
import textwrap
33

4+
from tests.utils import assert_outcomes
45

5-
def test_scenarios(testdir):
6-
"""Test scenarios shortcut."""
6+
7+
def test_scenarios(testdir, pytest_params):
8+
"""Test scenarios shortcut (used together with @scenario for individual test override)."""
79
testdir.makeini(
810
"""
911
[pytest]
@@ -63,7 +65,8 @@ def test_already_bound():
6365
scenarios('features')
6466
"""
6567
)
66-
result = testdir.runpytest("-v", "-s")
68+
result = testdir.runpytest_subprocess("-v", "-s", *pytest_params)
69+
assert_outcomes(result, passed=4, failed=1)
6770
result.stdout.fnmatch_lines(["*collected 5 items"])
6871
result.stdout.fnmatch_lines(["*test_test_subfolder_scenario *bar!", "PASSED"])
6972
result.stdout.fnmatch_lines(["*test_test_scenario *bar!", "PASSED"])
@@ -72,7 +75,7 @@ def test_already_bound():
7275
result.stdout.fnmatch_lines(["*test_test_scenario_1 *bar!", "PASSED"])
7376

7477

75-
def test_scenarios_none_found(testdir):
78+
def test_scenarios_none_found(testdir, pytest_params):
7679
"""Test scenarios shortcut when no scenarios found."""
7780
testpath = testdir.makepyfile(
7881
"""
@@ -82,6 +85,6 @@ def test_scenarios_none_found(testdir):
8285
scenarios('.')
8386
"""
8487
)
85-
reprec = testdir.inline_run(testpath)
86-
reprec.assertoutcome(failed=1)
87-
assert "NoScenariosFound" in str(reprec.getreports()[1].longrepr)
88+
result = testdir.runpytest_subprocess(testpath, *pytest_params)
89+
assert_outcomes(result, errors=1)
90+
result.stdout.fnmatch_lines(["*NoScenariosFound*"])

tests/steps/test_steps.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,11 @@ def test_preserve_decorator(testdir, step, keyword):
5353
from pytest_bdd import {step}
5454
from pytest_bdd.steps import get_step_fixture_name
5555
56+
@{step}("{keyword}")
57+
def func():
58+
"""Doc string."""
5659
5760
def test_decorator():
58-
@{step}("{keyword}")
59-
def func():
60-
"""Doc string."""
61-
6261
assert globals()[get_step_fixture_name("{keyword}", {step}.__name__)].__doc__ == "Doc string."
6362
6463

tests/utils.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from packaging.utils import Version
55

66
PYTEST_VERSION = Version(pytest.__version__)
7+
PYTEST_6 = PYTEST_VERSION >= Version("6")
78

8-
_errors_key = "error" if PYTEST_VERSION < Version("6") else "errors"
99

10-
if PYTEST_VERSION < Version("6"):
10+
if PYTEST_6:
1111

1212
def assert_outcomes(
1313
result,
@@ -20,12 +20,7 @@ def assert_outcomes(
2020
):
2121
"""Compatibility function for result.assert_outcomes"""
2222
return result.assert_outcomes(
23-
error=errors, # Pytest < 6 uses the singular form
24-
passed=passed,
25-
skipped=skipped,
26-
failed=failed,
27-
xpassed=xpassed,
28-
xfailed=xfailed,
23+
errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed
2924
)
3025

3126

@@ -42,5 +37,10 @@ def assert_outcomes(
4237
):
4338
"""Compatibility function for result.assert_outcomes"""
4439
return result.assert_outcomes(
45-
errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed
40+
error=errors, # Pytest < 6 uses the singular form
41+
passed=passed,
42+
skipped=skipped,
43+
failed=failed,
44+
xpassed=xpassed,
45+
xfailed=xfailed,
4646
)

0 commit comments

Comments
 (0)