diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 697f1fab..b51ebbb4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,10 +39,12 @@ jobs: run: | python -m pip install --upgrade pip pip install -U setuptools - pip install "tox~=4.0" "tox-gh-actions~=3.0" codecov + pip install "tox>=4.0" "tox-gh-actions>=3.2" codecov - name: Test with tox run: | tox + - name: Gather codecov + run: | codecov - name: Build checking if: "matrix.python-version == '3.12'" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7ae032af..6217935d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,7 +15,7 @@ jobs: submodules: 'recursive' - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip build twine diff --git a/src/pytest_bdd/allure_logging.py b/src/pytest_bdd/allure_logging.py index 6bc6eddb..0672648f 100644 --- a/src/pytest_bdd/allure_logging.py +++ b/src/pytest_bdd/allure_logging.py @@ -9,6 +9,7 @@ from pydantic import BaseModel as PydanticBaseModel from pytest_bdd.compatibility.allure import ALLURE_INSTALLED +from pytest_bdd.compatibility.pytest import PYTEST81 if ALLURE_INSTALLED: from allure_commons import hookimpl @@ -34,7 +35,7 @@ def __init__(self, allure_logger, allure_cache): def register_if_allure_accessible(cls, config): pluginmanager = config.pluginmanager allure_accessible = pluginmanager.hasplugin("allure_pytest") and config.option.allure_report_dir - if allure_accessible: + if allure_accessible and not PYTEST81: allure_plugin_manager.get_plugins() listener = next( diff --git a/src/pytest_bdd/compatibility/path.py b/src/pytest_bdd/compatibility/path.py new file mode 100644 index 00000000..1ad7b44d --- /dev/null +++ b/src/pytest_bdd/compatibility/path.py @@ -0,0 +1,12 @@ +import os +import sys + + +def relpath(path, start=os.curdir): + try: + return os.path.relpath(path, start) + except ValueError: + if sys.platform == "win32": + return path + else: + raise diff --git a/src/pytest_bdd/hook.py b/src/pytest_bdd/hook.py index 7cfb7784..fc2fa5da 100644 --- a/src/pytest_bdd/hook.py +++ b/src/pytest_bdd/hook.py @@ -2,7 +2,6 @@ from enum import Enum from inspect import isfunction, isgeneratorfunction, signature from itertools import count, product, starmap -from operator import attrgetter from typing import Optional, Union from _pytest.mark import Mark @@ -10,7 +9,7 @@ from makefun import wraps from pytest import fixture -from pytest_bdd.compatibility.pytest import FixtureRequest +from pytest_bdd.compatibility.pytest import PYTEST7, FixtureRequest from pytest_bdd.tag_expression import GherkinTagExpression, MarksTagExpression, TagExpression expression_count_gen = count() @@ -57,7 +56,9 @@ def hook(request: FixtureRequest, *args, **kwargs): { HookKind.mark: request.node.iter_markers(), HookKind.tag: map( - lambda tag: Mark(tag.name, args=tuple(), kwargs={}), # type: ignore[no-any-return] + lambda tag: Mark( # type: ignore[no-any-return] + tag.name, args=tuple(), kwargs={}, **({"_ispytest": True} if PYTEST7 else {}) + ), request.getfixturevalue("scenario").tags, ), }[_kind] diff --git a/src/pytest_bdd/message_plugin.py b/src/pytest_bdd/message_plugin.py index d3e16bdc..dbafe41c 100644 --- a/src/pytest_bdd/message_plugin.py +++ b/src/pytest_bdd/message_plugin.py @@ -42,6 +42,7 @@ TestStepStarted, Timestamp, ) +from pytest_bdd.compatibility.path import relpath from pytest_bdd.compatibility.pytest import ( Config, FixtureDef, @@ -252,7 +253,7 @@ def pytest_fixture_setup(self, fixturedef: FixtureDef, request): id=cast(PytestBDDIdGeneratorHandler, config).pytest_bdd_id_generator.get_next_id(), **({"name": hook_name} if hook_name is not None else {}), source_reference=SourceReference( - uri=os.path.relpath( + uri=relpath( getfile(func), str(get_config_root_path(cast(Config, config))), ), diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index 8e1b5be1..873ec5a3 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -2,7 +2,6 @@ from functools import partial from itertools import filterfalse from operator import contains, itemgetter, methodcaller -from os.path import relpath from pathlib import Path from typing import Callable, List, Sequence, Set, Tuple, Union @@ -13,6 +12,7 @@ from gherkin.pickles.compiler import Compiler as PicklesCompiler from pytest_bdd.compatibility.parser import ParserProtocol +from pytest_bdd.compatibility.path import relpath from pytest_bdd.compatibility.pytest import Config from pytest_bdd.compatibility.struct_bdd import STRUCT_BDD_INSTALLED from pytest_bdd.exceptions import FeatureError diff --git a/src/pytest_bdd/plugin.py b/src/pytest_bdd/plugin.py index 666e9d8a..4998e459 100644 --- a/src/pytest_bdd/plugin.py +++ b/src/pytest_bdd/plugin.py @@ -20,6 +20,7 @@ from pytest_bdd.collector import FeatureFileModule as FeatureFileCollector from pytest_bdd.collector import Module as ModuleCollector from pytest_bdd.compatibility.pytest import ( + PYTEST7, Config, FixtureRequest, Mark, @@ -144,12 +145,24 @@ def pytest_unconfigure(config: Config) -> None: cucumber_json.unconfigure(config) -@pytest.hookimpl(hookwrapper=True) -def pytest_pycollect_makemodule(path, parent, module_path=None): +def _pytest_pycollect_makemodule(): with patch("_pytest.python.Module", new=ModuleCollector): yield +if PYTEST7: + + @pytest.hookimpl(hookwrapper=True) + def pytest_pycollect_makemodule(parent, module_path): + yield from _pytest_pycollect_makemodule() + +else: + + @pytest.hookimpl(hookwrapper=True) + def pytest_pycollect_makemodule(path, parent): + yield from _pytest_pycollect_makemodule() + + @pytest.hookimpl(tryfirst=True) def pytest_plugin_registered(plugin, manager): if hasattr(plugin, "__file__") and isinstance(plugin, (type, ModuleType)): @@ -298,8 +311,8 @@ def pytest_cmdline_main(config: Config) -> Optional[int]: return generation.cmdline_main(config) -def pytest_collect_file(parent: Collector, path, file_path=None): - file_path = file_path or Path(path) +def _pytest_collect_file(parent: Collector, file_path=None): + file_path = Path(file_path) config = parent.session.config is_enabled_feature_autoload = config.getoption("feature_autoload") if is_enabled_feature_autoload is None: @@ -314,6 +327,17 @@ def pytest_collect_file(parent: Collector, path, file_path=None): return FeatureFileCollector.build(parent=parent, file_path=file_path) +if PYTEST7: # Done intentionally because of API change + + def pytest_collect_file(parent: Collector, file_path): + return _pytest_collect_file(parent=parent, file_path=file_path) + +else: + + def pytest_collect_file(parent: Collector, path): # type: ignore[misc] + return _pytest_collect_file(parent=parent, file_path=path) + + @pytest.mark.trylast def pytest_bdd_convert_tag_to_marks(feature, scenario, tag) -> Optional[Collection[Union[Mark, MarkDecorator]]]: return [getattr(pytest.mark, tag)] diff --git a/src/pytest_bdd/steps.py b/src/pytest_bdd/steps.py index 84e9d5b0..05e60fa2 100644 --- a/src/pytest_bdd/steps.py +++ b/src/pytest_bdd/steps.py @@ -51,6 +51,7 @@ def given_beautiful_article(article): from messages import ExpressionType, Location, Pickle # type:ignore[attr-defined] from messages import PickleStep as Step # type:ignore[attr-defined] from messages import SourceReference, StepDefinition, StepDefinitionPattern # type:ignore[attr-defined] +from pytest_bdd.compatibility.path import relpath from pytest_bdd.compatibility.pytest import Config, Parser, TypeAlias, get_config_root_path from pytest_bdd.model import Feature, StepType from pytest_bdd.model.messages_extension import ExpressionType as ExpressionTypeExtension @@ -383,7 +384,7 @@ def as_message(self, config: Union[Config, PytestBDDIdGeneratorHandler]): id=self.id, pattern=pattern, source_reference=SourceReference( # type: ignore[call-arg] # migration to pydantic2 - uri=os.path.relpath( + uri=relpath( getfile(self.func), str(get_config_root_path(cast(Config, config))), ), diff --git a/src/pytest_bdd/struct_bdd/plugin.py b/src/pytest_bdd/struct_bdd/plugin.py index 475a66f3..5f5b9666 100644 --- a/src/pytest_bdd/struct_bdd/plugin.py +++ b/src/pytest_bdd/struct_bdd/plugin.py @@ -5,7 +5,7 @@ import pytest -from pytest_bdd.compatibility.pytest import Config, Module +from pytest_bdd.compatibility.pytest import PYTEST7, Config, Module from pytest_bdd.mimetypes import Mimetype from pytest_bdd.struct_bdd.model import StepPrototype from pytest_bdd.struct_bdd.parser import StructBDDParser @@ -45,11 +45,22 @@ def pytest_bdd_is_collectible(self, config: Config, path: Path): if str(path).endswith(f".bdd.{extension_suffix.value}"): return True - @pytest.hookimpl(hookwrapper=True) - def pytest_pycollect_makemodule(self, path, parent, module_path=None): + def _pytest_pycollect_makemodule(self): outcome = yield res = outcome.get_result() if isinstance(res, Module): for member_name, member in getmembers(res.module): if isinstance(member, StepPrototype) and member_name.startswith("test_"): setattr(res.module, member_name, member.as_test(res.module.__file__)) + + if PYTEST7: + + @pytest.hookimpl(hookwrapper=True) + def pytest_pycollect_makemodule(self, parent, module_path): + yield from self._pytest_pycollect_makemodule() + + else: + + @pytest.hookimpl(hookwrapper=True) + def pytest_pycollect_makemodule(self, path, parent): + yield from self._pytest_pycollect_makemodule() diff --git a/src/pytest_bdd/utils.py b/src/pytest_bdd/utils.py index a30b827e..0b1be81c 100644 --- a/src/pytest_bdd/utils.py +++ b/src/pytest_bdd/utils.py @@ -29,7 +29,9 @@ runtime_checkable, ) -from pytest_bdd.compatibility.pytest import PYTEST8, PYTEST81, FixtureDef, fail +from _pytest.fixtures import FixtureDef, FixtureRequest + +from pytest_bdd.compatibility.pytest import PYTEST7, PYTEST8, PYTEST81, fail from pytest_bdd.const import ALPHA_REGEX, PYTHON_REPLACE_REGEX if TYPE_CHECKING: # pragma: no cover @@ -147,9 +149,8 @@ def instantiate_from_collection_or_bool( return cls(bool_or_items, warm_up_keys=warm_up_keys) -def inject_fixture(request, arg, value): +def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None: """Inject fixture into pytest fixture request. - :param request: pytest fixture request :param arg: argument name :param value: argument value @@ -162,6 +163,7 @@ def inject_fixture(request, arg, value): func=lambda: value, scope="function", params=None, + **({"_ispytest": True} if PYTEST8 else {}), ) fd.cached_result = (value, 0, None) diff --git a/tests/allure_/test_allure_outline.py b/tests/allure_/test_allure_outline.py index dbc069a4..3f2fdf93 100644 --- a/tests/allure_/test_allure_outline.py +++ b/tests/allure_/test_allure_outline.py @@ -2,9 +2,11 @@ from pytest_bdd import scenario from pytest_bdd.compatibility.allure import ALLURE_INSTALLED +from pytest_bdd.compatibility.pytest import PYTEST81 @scenario("testdata/allure_/outline.feature", "Scenario outline") @mark.skipif(not ALLURE_INSTALLED, reason="Allure is not installed") +@mark.skipif(PYTEST81, reason="Allure uses deprecated APIs") def test_scenario_outline(): pass diff --git a/tests/allure_/test_allure_scenario.py b/tests/allure_/test_allure_scenario.py index 6d408b41..b0c9f03b 100644 --- a/tests/allure_/test_allure_scenario.py +++ b/tests/allure_/test_allure_scenario.py @@ -2,9 +2,11 @@ from pytest_bdd import scenario from pytest_bdd.compatibility.allure import ALLURE_INSTALLED +from pytest_bdd.compatibility.pytest import PYTEST81 @scenario("testdata/allure_//scenario.feature", "Simple passed scenario") @mark.skipif(not ALLURE_INSTALLED, reason="Allure is not installed") +@mark.skipif(PYTEST81, reason="Allure uses deprecated APIs") def test_simple_passed_scenario(): pass diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py index 69b762f4..eb5e51a1 100644 --- a/tests/feature/test_steps.py +++ b/tests/feature/test_steps.py @@ -121,7 +121,7 @@ def test_steps( second_foo, request ): - # Original fixture values are recieved from test parameters + # Original fixture values are received from test parameters assert first_foo == 'first_foo' assert second_foo == 'second_foo' diff --git a/tox.ini b/tox.ini index ef59fb48..181fb939 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py312-pytest{625, 83, 82, 81, 80, 74, 73, 72, 71, 70, latest}-mypy-lin py312-pytest{625, 83, 82, 81, 80, 74, 73, 72, 71, 70, latest}-coverage-lin py312-pytestlatest-gherkin{24, latest}-xdist-coverage-{lin, win, mac} - py312-pytestlatets-allure-coverage-{lin, win, mac} + py312-pytest80-allure-coverage-{lin, win, mac} py39-pytest{62, 61, 60, 54, 53, 52, 51, 50}-coverage-lin py38-pytest{62, 54}-coverage-{win, mac} py{py39, py38, 38}-pytest{62, 54}-coverage-lin