From 94e0e28da33dbfe082178679ecc9e013bb2f6d21 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 27 Jul 2020 12:02:59 +0200 Subject: [PATCH 01/19] Fix getting module with --import-mode=importlib --- pytest_bdd/scenario.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 8d525d8bf..22030aee4 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -14,6 +14,7 @@ import inspect import os import re +import sys import pytest @@ -283,13 +284,39 @@ def get_name(): suffix = "_{0}".format(index) +def find_module(frame): + """Get the module object for the given frame.""" + module = inspect.getmodule(frame[0]) + if module is not None: + return module + + # Probably using pytest's importlib mode, let's try to get the module + # from the filename. + # The imports will only work on Python 3 (hence why they are here rather + # than at module level). However, we should only ever get here on Python 3, + # because pytest's --import-module=importlib is in a Python 3 only release + # of pytest. + import pathlib + import importlib.util + + path = pathlib.Path(frame.filename) + module_name = path.stem + for meta_importer in sys.meta_path: + spec = meta_importer.find_spec(module_name, [str(path.parent)]) + if spec is not None: + break + assert spec is not None + + return importlib.util.module_from_spec(spec) + + def scenarios(*feature_paths, **kwargs): """Parse features from the paths and put all found scenarios in the caller module. :param *feature_paths: feature file paths to use for scenarios """ frame = inspect.stack()[1] - module = inspect.getmodule(frame[0]) + module = find_module(frame) features_base_dir = kwargs.get("features_base_dir") if features_base_dir is None: From a83313b0e58c615b90dd65fbcd2103e0211c64a7 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 15:57:53 +0200 Subject: [PATCH 02/19] Trying to get --import-mode=importlib to work --- pytest_bdd/scenario.py | 6 +++--- pytest_bdd/steps.py | 9 +++++++-- tests/feature/test_scenario.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 22030aee4..bad009731 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -284,9 +284,9 @@ def get_name(): suffix = "_{0}".format(index) -def find_module(frame): +def find_module(frame_info): """Get the module object for the given frame.""" - module = inspect.getmodule(frame[0]) + module = inspect.getmodule(frame_info.frame) if module is not None: return module @@ -299,7 +299,7 @@ def find_module(frame): import pathlib import importlib.util - path = pathlib.Path(frame.filename) + path = pathlib.Path(frame_info.filename) module_name = path.stem for meta_importer in sys.meta_path: spec = meta_importer.find_spec(module_name, [str(path.parent)]) diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index b95c70a91..aa1d46653 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -137,7 +137,9 @@ def lazy_step_func(): 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) + caller_module = get_caller_module() + fixture_step_name = get_step_fixture_name(parsed_step_name, step_type) + setattr(caller_module, fixture_step_name, lazy_step_func) return func return decorator @@ -146,7 +148,10 @@ def lazy_step_func(): def get_caller_module(depth=2): """Return the module of the caller.""" frame = sys._getframe(depth) - module = inspect.getmodule(frame) + frame_info = inspect.stack()[depth] + from pytest_bdd.scenario import find_module + + module = find_module(frame_info) if module is None: return get_caller_module(depth=depth) return module diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 0bc8c17fe..afaee4998 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -111,3 +111,31 @@ def test_scenario_not_decorator(testdir): result.assert_outcomes(failed=1) result.stdout.fnmatch_lines("*ScenarioIsDecoratorOnly: scenario function can only be used as a decorator*") + + +def test_importlib(testdir): + """Test scenario function with importlib import mode.""" + testdir.makefile( + ".feature", + simple=""" + Feature: Simple feature + Scenario: Simple scenario + Given I have a bar + """, + ) + testdir.makepyfile( + """ + from pytest_bdd import scenario, given + + @scenario("simple.feature", "Simple scenario") + def test_simple(): + pass + + @given("I have a bar") + def bar(): + return "bar" + """ + ) + + result = testdir.runpytest_subprocess("--import-mode=importlib") + result.assert_outcomes(passed=1) From b480bbe869ffdd373d111aeede09062a53e1f471 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 18:15:02 +0200 Subject: [PATCH 03/19] Fix _step_decorator not injecting the fixture in the caller module when using --import-mode=importlib --- pytest_bdd/scenario.py | 2 +- pytest_bdd/steps.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index bad009731..c8caa58ee 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -286,7 +286,7 @@ def get_name(): def find_module(frame_info): """Get the module object for the given frame.""" - module = inspect.getmodule(frame_info.frame) + module = inspect.getmodule(frame_info[0]) if module is not None: return module diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index aa1d46653..4a451c929 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -137,9 +137,12 @@ def lazy_step_func(): step_func.target_fixture = lazy_step_func.target_fixture = target_fixture lazy_step_func = pytest.fixture()(lazy_step_func) - caller_module = get_caller_module() fixture_step_name = get_step_fixture_name(parsed_step_name, step_type) - setattr(caller_module, fixture_step_name, lazy_step_func) + + prev_frame = inspect.stack()[1] + prev_frame[0].f_locals[fixture_step_name] = lazy_step_func + # caller_module = get_caller_module() + # setattr(caller_module, fixture_step_name, lazy_step_func) return func return decorator From eed25e0c5b50e991e81488db265d6809f07b0d01 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 18:22:59 +0200 Subject: [PATCH 04/19] Test importlib mode with "scenarios" --- tests/feature/test_scenario.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index afaee4998..9b59a85ea 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -2,6 +2,8 @@ import textwrap +import pytest + def test_scenario_not_found(testdir): """Test the situation when scenario is not found.""" @@ -113,7 +115,8 @@ def test_scenario_not_decorator(testdir): result.stdout.fnmatch_lines("*ScenarioIsDecoratorOnly: scenario function can only be used as a decorator*") -def test_importlib(testdir): +@pytest.mark.parametrize('importmode', [None, 'prepend', 'importlib', 'append']) +def test_import_mode(testdir, importmode): """Test scenario function with importlib import mode.""" testdir.makefile( ".feature", @@ -122,10 +125,20 @@ def test_importlib(testdir): Scenario: Simple scenario Given I have a bar """, + many_scenarios=""" + Feature: Many scenarios + Scenario: Scenario A + Given I have a bar + Then pass + Scenario: Scenario B + Then pass + """, ) testdir.makepyfile( """ - from pytest_bdd import scenario, given + from pytest_bdd import scenario, scenarios, given, then + + scenarios("many_scenarios.feature") @scenario("simple.feature", "Simple scenario") def test_simple(): @@ -134,8 +147,15 @@ def test_simple(): @given("I have a bar") def bar(): return "bar" + + @then("pass") + def bar(): + pass """ ) - - result = testdir.runpytest_subprocess("--import-mode=importlib") - result.assert_outcomes(passed=1) + if importmode is None: + params = [] + else: + params = ['--import-mode=' + importmode] + result = testdir.runpytest_subprocess(*params) + result.assert_outcomes(passed=3) From f2b0d99bb9c97c6813d72404c9bebd6bc11ef556 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 18:25:59 +0200 Subject: [PATCH 05/19] Fix "scenarios()" not injecting the test items in the caller module when using --import-mode=importlib --- pytest_bdd/scenario.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index c8caa58ee..7d21932f5 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -345,9 +345,13 @@ def _scenario(): 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 module.__dict__: + # # found an unique test name + # module.__dict__[test_name] = _scenario + # break + if test_name not in frame[0].f_locals: # found an unique test name - module.__dict__[test_name] = _scenario + frame[0].f_locals[test_name] = _scenario break found = True if not found: From 837cc5dc2238454bfbf99d0c62e105bb5b4620ea Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 18:38:59 +0200 Subject: [PATCH 06/19] Test import_mode only if supported by pytest --- requirements-testing.txt | 2 +- tests/feature/test_scenario.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 932a8957f..748809f75 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1 +1 @@ -mock +packaging diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 9b59a85ea..bd394754b 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -3,6 +3,7 @@ import textwrap import pytest +from packaging.version import Version def test_scenario_not_found(testdir): @@ -115,8 +116,12 @@ def test_scenario_not_decorator(testdir): result.stdout.fnmatch_lines("*ScenarioIsDecoratorOnly: scenario function can only be used as a decorator*") -@pytest.mark.parametrize('importmode', [None, 'prepend', 'importlib', 'append']) -def test_import_mode(testdir, importmode): +@pytest.mark.skipif( + Version(pytest.__version__) < Version('6'), + reason="--import-mode not supported on this pytest version", +) +@pytest.mark.parametrize('import_mode', [None, 'prepend', 'importlib', 'append']) +def test_import_mode(testdir, import_mode): """Test scenario function with importlib import mode.""" testdir.makefile( ".feature", @@ -153,9 +158,9 @@ def bar(): pass """ ) - if importmode is None: + if import_mode is None: params = [] else: - params = ['--import-mode=' + importmode] + params = ['--import-mode=' + import_mode] result = testdir.runpytest_subprocess(*params) result.assert_outcomes(passed=3) From ea212e53879863654c19b477dcbd6d1f0e826bbd Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 18:45:54 +0200 Subject: [PATCH 07/19] Update pytests in tox.ini --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cf9c2c22e..4725667fd 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ distshare = {homedir}/.tox/distshare envlist = py38-pytestlatest-linters, py27-pytest{43,44,45,46}-coverage, - py38-pytest{43,44,45,46,50,51,52, latest}-coverage, + py38-pytest{43,44,45,46,50,51,52,53,54,60, latest}-coverage, py{35,36,38}-pytestlatest-coverage, py27-pytestlatest-xdist-coverage skip_missing_interpreters = true @@ -13,6 +13,9 @@ setenv = xdist: _PYTEST_MORE_ARGS=-n3 -rfsxX deps = pytestlatest: pytest + pytest60: pytest~=6.0.0 + pytest54: pytest~=5.4.0 + pytest53: pytest~=5.3.0 pytest52: pytest~=5.2.0 pytest51: pytest~=5.1.0 pytest50: pytest~=5.0.0 From a868b7fd30c53c47194a760e1eace429c66e5cc1 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 19:12:38 +0200 Subject: [PATCH 08/19] Fix missing import --- tests/feature/test_scenario.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index b8e31eea9..e6cc240e5 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -2,6 +2,8 @@ import textwrap +import pytest + from tests.utils import assert_outcomes, PYTEST_6 From a1b05d55df97490c71eae8de852d32e8e639039e Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 19:15:09 +0200 Subject: [PATCH 09/19] Fix reversed check --- tests/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 7794c2f93..621bcac3a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -20,12 +20,7 @@ def assert_outcomes( ): """Compatibility function for result.assert_outcomes""" return result.assert_outcomes( - error=errors, # Pytest < 6 uses the singular form - passed=passed, - skipped=skipped, - failed=failed, - xpassed=xpassed, - xfailed=xfailed, + errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed ) @@ -42,5 +37,10 @@ def assert_outcomes( ): """Compatibility function for result.assert_outcomes""" return result.assert_outcomes( - errors=errors, passed=passed, skipped=skipped, failed=failed, xpassed=xpassed, xfailed=xfailed + error=errors, # Pytest < 6 uses the singular form + passed=passed, + skipped=skipped, + failed=failed, + xpassed=xpassed, + xfailed=xfailed, ) From e942af5320d2043863b357e121b973383eaf55f4 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 19:16:58 +0200 Subject: [PATCH 10/19] Fix test --- tests/steps/test_steps.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/steps/test_steps.py b/tests/steps/test_steps.py index a16812f33..acfd279b8 100644 --- a/tests/steps/test_steps.py +++ b/tests/steps/test_steps.py @@ -52,12 +52,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." From eba1d70fcc61ace9b4cc05397e497680672a7b6a Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 19:28:21 +0200 Subject: [PATCH 11/19] Fix test --- tests/test_hooks.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index b586c0bc3..2e2aa7f30 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -52,7 +52,12 @@ def test_item_collection_does_not_break_on_non_function_items(testdir): @pytest.mark.tryfirst def pytest_collection_modifyitems(session, config, items): - items[:] = [CustomItem(name=item.name, parent=item.parent) for item in items] + try: + item_creator = CustomItem.from_parent # Only available in pytest >= 5.4.0 + except AttributeError: + item_creator = CustomItem + + items[:] = [item_creator(name=item.name, parent=item.parent) for item in items] class CustomItem(pytest.Item): def runtest(self): From 0e12d9ac17633f1996b5214ac3cb1d983e6a5ffc Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 5 Sep 2020 20:18:19 +0200 Subject: [PATCH 12/19] Blackify code --- tests/feature/test_scenario.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index e6cc240e5..11ae47a11 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -121,7 +121,7 @@ def test_scenario_not_decorator(testdir): not PYTEST_6, reason="--import-mode not supported on this pytest version", ) -@pytest.mark.parametrize('import_mode', [None, 'prepend', 'importlib', 'append']) +@pytest.mark.parametrize("import_mode", [None, "prepend", "importlib", "append"]) def test_import_mode(testdir, import_mode): """Test scenario function with importlib import mode.""" testdir.makefile( @@ -162,6 +162,6 @@ def bar(): if import_mode is None: params = [] else: - params = ['--import-mode=' + import_mode] + params = ["--import-mode=" + import_mode] result = testdir.runpytest_subprocess(*params) result.assert_outcomes(passed=3) From 158142fbdc2a6c16f695b4178db61a1dd2d28350 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 09:31:53 +0200 Subject: [PATCH 13/19] Rewrite `scenario` and `scenarios` tests so that we can easily parametrize with the different pytest --import-mode. Now tests are (correctly) failing when `scenarios` and `@scenario` are used together. --- tests/conftest.py | 22 ++++++++++++++++ tests/feature/test_scenario.py | 45 ++++++++++----------------------- tests/feature/test_scenarios.py | 17 ++++++++----- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 694d7d58d..bf04c9932 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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, + ) diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 11ae47a11..5ed84f731 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -2,12 +2,10 @@ import textwrap -import pytest +from tests.utils import assert_outcomes -from tests.utils import assert_outcomes, PYTEST_6 - -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", @@ -32,7 +30,7 @@ def test_not_found(): """ ) ) - 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*') @@ -92,8 +90,12 @@ def a_comment(acomment): ) ) + result = testdir.runpytest() + + result.assert_outcomes(passed=2) -def test_scenario_not_decorator(testdir): + +def test_scenario_not_decorator(testdir, pytest_params): """Test scenario function is used not as decorator.""" testdir.makefile( ".feature", @@ -111,19 +113,14 @@ 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*") -@pytest.mark.skipif( - not PYTEST_6, - reason="--import-mode not supported on this pytest version", -) -@pytest.mark.parametrize("import_mode", [None, "prepend", "importlib", "append"]) -def test_import_mode(testdir, import_mode): - """Test scenario function with importlib import mode.""" +def test_simple(testdir, pytest_params): + """Test scenario decorator with a standard usage.""" testdir.makefile( ".feature", simple=""" @@ -131,20 +128,10 @@ def test_import_mode(testdir, import_mode): Scenario: Simple scenario Given I have a bar """, - many_scenarios=""" - Feature: Many scenarios - Scenario: Scenario A - Given I have a bar - Then pass - Scenario: Scenario B - Then pass - """, ) testdir.makepyfile( """ - from pytest_bdd import scenario, scenarios, given, then - - scenarios("many_scenarios.feature") + from pytest_bdd import scenario, given, then @scenario("simple.feature", "Simple scenario") def test_simple(): @@ -159,9 +146,5 @@ def bar(): pass """ ) - if import_mode is None: - params = [] - else: - params = ["--import-mode=" + import_mode] - result = testdir.runpytest_subprocess(*params) - result.assert_outcomes(passed=3) + result = testdir.runpytest_subprocess(*pytest_params) + result.assert_outcomes(passed=1) diff --git a/tests/feature/test_scenarios.py b/tests/feature/test_scenarios.py index 82135b7d3..4e4ab5907 100644 --- a/tests/feature/test_scenarios.py +++ b/tests/feature/test_scenarios.py @@ -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_already_bound(): 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_already_bound(): 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*"]) From eb19c1d3301d20f915a9de17e014d91a59b08cef Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 09:58:15 +0200 Subject: [PATCH 14/19] Refactor (and simplify) functions to get details about the caller module. --- pytest_bdd/scenario.py | 51 ++++++++++-------------------------------- pytest_bdd/steps.py | 13 ----------- pytest_bdd/utils.py | 11 +++++++++ 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 01482fd07..4f4bbe9af 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -25,9 +25,8 @@ 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+_*") @@ -216,11 +215,11 @@ def scenario( """ scenario_name = force_unicode(scenario_name, encoding) - caller_module = caller_module or get_caller_module() + caller_module_path = caller_module.__file__ if caller_module is not None else 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 @@ -243,8 +242,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) @@ -289,43 +288,17 @@ def get_name(): suffix = "_{0}".format(index) -def find_module(frame_info): - """Get the module object for the given frame.""" - module = inspect.getmodule(frame_info[0]) - if module is not None: - return module - - # Probably using pytest's importlib mode, let's try to get the module - # from the filename. - # The imports will only work on Python 3 (hence why they are here rather - # than at module level). However, we should only ever get here on Python 3, - # because pytest's --import-module=importlib is in a Python 3 only release - # of pytest. - import pathlib - import importlib.util - - path = pathlib.Path(frame_info.filename) - module_name = path.stem - for meta_importer in sys.meta_path: - spec = meta_importer.find_spec(module_name, [str(path.parent)]) - if spec is not None: - break - assert spec is not None - - return importlib.util.module_from_spec(spec) - - def scenarios(*feature_paths, **kwargs): """Parse features from the paths and put all found scenarios in the caller module. :param *feature_paths: feature file paths to use for scenarios """ - frame = inspect.stack()[1] - module = find_module(frame) + 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: @@ -336,7 +309,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__") ) @@ -354,9 +327,9 @@ def _scenario(): # # found an unique test name # module.__dict__[test_name] = _scenario # break - if test_name not in frame[0].f_locals: + if test_name not in caller_locals: # found an unique test name - frame[0].f_locals[test_name] = _scenario + caller_locals[test_name] = _scenario break found = True if not found: diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index 4a451c929..efeb2cd52 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -37,7 +37,6 @@ def given_beautiful_article(article): from __future__ import absolute_import import inspect -import sys import pytest @@ -148,18 +147,6 @@ def lazy_step_func(): return decorator -def get_caller_module(depth=2): - """Return the module of the caller.""" - frame = sys._getframe(depth) - frame_info = inspect.stack()[depth] - from pytest_bdd.scenario import find_module - - module = find_module(frame_info) - if module is None: - return get_caller_module(depth=depth) - return module - - def inject_fixture(request, arg, value): """Inject fixture into pytest fixture request. diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 879f282b3..646c21045 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -31,3 +31,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 From b0506cd540b61c90063e0faa26e87a8b36b8bdb3 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 10:05:06 +0200 Subject: [PATCH 15/19] Use helper functions, delete dead code --- pytest_bdd/scenario.py | 4 ---- pytest_bdd/steps.py | 8 +++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 4f4bbe9af..d5db442f3 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -323,10 +323,6 @@ def _scenario(): pass # pragma: no cover for test_name in get_python_name_generator(scenario_name): - # if test_name not in module.__dict__: - # # found an unique test name - # module.__dict__[test_name] = _scenario - # break if test_name not in caller_locals: # found an unique test name caller_locals[test_name] = _scenario diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index efeb2cd52..483f744b3 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -48,7 +48,7 @@ def given_beautiful_article(article): 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): @@ -138,10 +138,8 @@ def lazy_step_func(): lazy_step_func = pytest.fixture()(lazy_step_func) fixture_step_name = get_step_fixture_name(parsed_step_name, step_type) - prev_frame = inspect.stack()[1] - prev_frame[0].f_locals[fixture_step_name] = lazy_step_func - # caller_module = get_caller_module() - # setattr(caller_module, fixture_step_name, lazy_step_func) + caller_locals = get_caller_module_locals() + caller_locals[fixture_step_name] = lazy_step_func return func return decorator From dbf93a99cf2229ace3215ae1e8bda96f592ed692 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 10:08:59 +0200 Subject: [PATCH 16/19] Fix test definition --- tests/feature/test_scenario.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 5ed84f731..2caeee7ea 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -5,7 +5,7 @@ from tests.utils import assert_outcomes -def test_scenario_not_found(testdir, *pytest_params): +def test_scenario_not_found(testdir, pytest_params): """Test the situation when scenario is not found.""" testdir.makefile( ".feature", From 67e2439a726970db8828879880636917953aefe0 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 10:13:31 +0200 Subject: [PATCH 17/19] Detect feature using six.PY2 (so that we can easily remove it in the future) --- pytest_bdd/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 646c21045..8cbe8f785 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -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 From 00f932f7fc671716f5014346e02af178bdb16d81 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 10:17:38 +0200 Subject: [PATCH 18/19] Remove "caller_module" parameter from scenario signature --- pytest_bdd/scenario.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index d5db442f3..65407bec6 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -197,14 +197,7 @@ def scenario_wrapper(request): 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,7 +208,7 @@ def scenario( """ scenario_name = force_unicode(scenario_name, encoding) - caller_module_path = caller_module.__file__ if caller_module is not None else get_caller_module_path() + caller_module_path = get_caller_module_path() # Get the feature if features_base_dir is None: From 82d2b73177ed8a09c4f9ec90d17fc2804139c268 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 6 Sep 2020 10:23:06 +0200 Subject: [PATCH 19/19] Update changelog --- CHANGES.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a85b8fbdc..12d148d6b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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