Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94e0e28
Fix getting module with --import-mode=importlib
The-Compiler Jul 27, 2020
a83313b
Trying to get --import-mode=importlib to work
youtux Sep 5, 2020
b480bbe
Fix _step_decorator not injecting the fixture in the caller module wh…
youtux Sep 5, 2020
eed25e0
Test importlib mode with "scenarios"
youtux Sep 5, 2020
f2b0d99
Fix "scenarios()" not injecting the test items in the caller module w…
youtux Sep 5, 2020
837cc5d
Test import_mode only if supported by pytest
youtux Sep 5, 2020
ea212e5
Update pytests in tox.ini
youtux Sep 5, 2020
d6fd471
Merge branch 'update-pytests' into work-with-importlib
youtux Sep 5, 2020
a868b7f
Fix missing import
youtux Sep 5, 2020
a1b05d5
Fix reversed check
youtux Sep 5, 2020
e942af5
Fix test
youtux Sep 5, 2020
eba1d70
Fix test
youtux Sep 5, 2020
c2453d5
Merge branch 'update-pytests' into work-with-importlib
youtux Sep 5, 2020
5f7a24b
Merge branch 'update-pytests' into work-with-importlib
youtux Sep 5, 2020
0e12d9a
Blackify code
youtux Sep 5, 2020
c051beb
Merge branch 'master' into work-with-importlib
youtux Sep 6, 2020
158142f
Rewrite `scenario` and `scenarios` tests so that we can easily parame…
youtux Sep 6, 2020
eb19c1d
Refactor (and simplify) functions to get details about the caller mod…
youtux Sep 6, 2020
b0506cd
Use helper functions, delete dead code
youtux Sep 6, 2020
dbf93a9
Fix test definition
youtux Sep 6, 2020
67e2439
Detect feature using six.PY2 (so that we can easily remove it in the …
youtux Sep 6, 2020
00f932f
Remove "caller_module" parameter from scenario signature
youtux Sep 6, 2020
82d2b73
Update changelog
youtux Sep 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
35 changes: 14 additions & 21 deletions pytest_bdd/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import inspect
import os
import re
import sys

import pytest

Expand All @@ -24,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+_*")
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)


Expand Down Expand Up @@ -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:
Expand All @@ -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__")
)

Expand All @@ -323,9 +316,9 @@ 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 caller_locals:
# found an unique test name
module.__dict__[test_name] = _scenario
caller_locals[test_name] = _scenario
break
found = True
if not found:
Expand Down
17 changes: 5 additions & 12 deletions pytest_bdd/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def given_beautiful_article(article):

from __future__ import absolute_import
import inspect
import sys

import pytest

Expand All @@ -49,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):
Expand Down Expand Up @@ -137,21 +136,15 @@ 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)
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.

Expand Down
21 changes: 17 additions & 4 deletions pytest_bdd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import inspect

import six

CONFIG_STACK = []


Expand All @@ -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
Expand All @@ -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
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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,
)
43 changes: 39 additions & 4 deletions tests/feature/test_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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",
Expand All @@ -30,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*')
Expand Down Expand Up @@ -90,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",
Expand All @@ -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)
17 changes: 10 additions & 7 deletions tests/feature/test_scenarios.py
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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"])
Expand All @@ -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(
"""
Expand All @@ -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*"])
7 changes: 3 additions & 4 deletions tests/steps/test_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."


Expand Down
18 changes: 9 additions & 9 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
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,
Expand All @@ -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
)


Expand All @@ -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,
)