From 6b2bb3ed8550df4ac1f12213e397d0165b41c9af Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 2 Dec 2023 22:10:41 +0100 Subject: [PATCH 01/25] Must cast return type for `setdefault` --- src/pytest_bdd/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pytest_bdd/utils.py b/src/pytest_bdd/utils.py index eb243e5d2..067e8d81a 100644 --- a/src/pytest_bdd/utils.py +++ b/src/pytest_bdd/utils.py @@ -6,7 +6,7 @@ import re from inspect import getframeinfo, signature from sys import _getframe -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, TypeVar, cast if TYPE_CHECKING: from typing import Any, Callable @@ -78,7 +78,7 @@ def collect_dumped_objects(result: RunResult) -> list: def setdefault(obj: object, name: str, default: T) -> T: """Just like dict.setdefault, but for objects.""" try: - return getattr(obj, name) + return cast(T, getattr(obj, name)) except AttributeError: setattr(obj, name, default) return default From c0147d6f42d5017926a54cf3f62a9114c0aa1fcb Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 2 Dec 2023 23:08:56 +0100 Subject: [PATCH 02/25] Use WeakKeyDictionary instead of storing private attributes. Fixes mypy errors --- src/pytest_bdd/cucumber_json.py | 8 +++--- src/pytest_bdd/feature.py | 3 ++- src/pytest_bdd/gherkin_terminal_reporter.py | 19 +++++++++----- src/pytest_bdd/parser.py | 2 +- src/pytest_bdd/registry.py | 15 +++++++++++ src/pytest_bdd/reporting.py | 28 +++++++++++++-------- src/pytest_bdd/scenario.py | 5 ++-- src/pytest_bdd/steps.py | 11 ++++---- tests/feature/test_report.py | 28 +++++++++++++-------- 9 files changed, 81 insertions(+), 38 deletions(-) create mode 100644 src/pytest_bdd/registry.py diff --git a/src/pytest_bdd/cucumber_json.py b/src/pytest_bdd/cucumber_json.py index 60a3e84db..4281893ab 100644 --- a/src/pytest_bdd/cucumber_json.py +++ b/src/pytest_bdd/cucumber_json.py @@ -7,6 +7,8 @@ import time import typing +from pytest_bdd.registry import test_report_context + if typing.TYPE_CHECKING: from typing import Any @@ -87,8 +89,8 @@ def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]: def pytest_runtest_logreport(self, report: TestReport) -> None: try: - scenario = report.scenario - except AttributeError: + scenario = test_report_context[report].scenario + except KeyError: # skip reporting for non-bdd tests return @@ -127,7 +129,7 @@ def stepmap(step: dict[str, Any]) -> dict[str, Any]: self.features[scenario["feature"]["filename"]]["elements"].append( { "keyword": "Scenario", - "id": report.item["name"], + "id": test_report_context[report].name, "name": scenario["name"], "line": scenario["line_number"], "description": "", diff --git a/src/pytest_bdd/feature.py b/src/pytest_bdd/feature.py index 733d11967..3f8fef4ce 100644 --- a/src/pytest_bdd/feature.py +++ b/src/pytest_bdd/feature.py @@ -27,6 +27,7 @@ import glob import os.path +from typing import Iterable from .parser import Feature, parse_feature @@ -56,7 +57,7 @@ def get_feature(base_path: str, filename: str, encoding: str = "utf-8") -> Featu return feature -def get_features(paths: list[str], **kwargs) -> list[Feature]: +def get_features(paths: Iterable[str], **kwargs) -> list[Feature]: """Get features for given paths. :param list paths: `list` of paths (file or dirs) diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index b26a8a7db..f1dcf4b0b 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -4,6 +4,8 @@ from _pytest.terminal import TerminalReporter +from pytest_bdd.registry import test_report_context + if typing.TYPE_CHECKING: from typing import Any @@ -67,28 +69,33 @@ def pytest_runtest_logreport(self, report: TestReport) -> Any: feature_markup = {"blue": True} scenario_markup = word_markup - if self.verbosity <= 0 or not hasattr(report, "scenario"): + try: + scenario = test_report_context[report].scenario + except KeyError: + scenario = None + + if self.verbosity <= 0 or scenario is None: return super().pytest_runtest_logreport(rep) if self.verbosity == 1: self.ensure_newline() self._tw.write("Feature: ", **feature_markup) - self._tw.write(report.scenario["feature"]["name"], **feature_markup) + self._tw.write(scenario["feature"]["name"], **feature_markup) self._tw.write("\n") self._tw.write(" Scenario: ", **scenario_markup) - self._tw.write(report.scenario["name"], **scenario_markup) + self._tw.write(scenario["name"], **scenario_markup) self._tw.write(" ") self._tw.write(word, **word_markup) self._tw.write("\n") elif self.verbosity > 1: self.ensure_newline() self._tw.write("Feature: ", **feature_markup) - self._tw.write(report.scenario["feature"]["name"], **feature_markup) + self._tw.write(scenario["feature"]["name"], **feature_markup) self._tw.write("\n") self._tw.write(" Scenario: ", **scenario_markup) - self._tw.write(report.scenario["name"], **scenario_markup) + self._tw.write(scenario["name"], **scenario_markup) self._tw.write("\n") - for step in report.scenario["steps"]: + for step in scenario["steps"]: self._tw.write(f" {step['keyword']} {step['name']}\n", **scenario_markup) self._tw.write(f" {word}", **word_markup) self._tw.write("\n\n") diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index ccdaec737..b21cec5e8 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -226,7 +226,7 @@ class ScenarioTemplate: line_number: int templated: bool tags: set[str] = field(default_factory=set) - examples: Examples | None = field(default_factory=lambda: Examples()) + examples: Examples = field(default_factory=lambda: Examples()) _steps: list[Step] = field(init=False, default_factory=list) _description_lines: list[str] = field(init=False, default_factory=list) diff --git a/src/pytest_bdd/registry.py b/src/pytest_bdd/registry.py new file mode 100644 index 000000000..ac94ec86a --- /dev/null +++ b/src/pytest_bdd/registry.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable +from weakref import WeakKeyDictionary + +if TYPE_CHECKING: + from _pytest.nodes import Item + from _pytest.reports import TestReport + + from pytest_bdd.reporting import ReportContext, ScenarioReport + from pytest_bdd.steps import StepFunctionContext + +scenario_reports: WeakKeyDictionary[Item, ScenarioReport] = WeakKeyDictionary() +step_function_marker_context: WeakKeyDictionary[Callable[..., Any], StepFunctionContext] = WeakKeyDictionary() +test_report_context: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index 95254f648..d205b7f60 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -6,8 +6,11 @@ from __future__ import annotations import time +from dataclasses import dataclass from typing import TYPE_CHECKING +from .registry import scenario_reports, test_report_context + if TYPE_CHECKING: from typing import Any, Callable @@ -134,20 +137,25 @@ def fail(self) -> None: self.add_step_report(report) +@dataclass +class ReportContext: + scenario: dict[str, Any] + name: str + + def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None: """Store item in the report object.""" try: - scenario_report: ScenarioReport = item.__scenario_report__ - except AttributeError: - pass - else: - rep.scenario = scenario_report.serialize() - rep.item = {"name": item.name} + scenario_report: ScenarioReport = scenario_reports[item] + except KeyError: + return + + test_report_context[rep] = ReportContext(scenario=scenario_report.serialize(), name=item.name) def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None: """Create scenario report for the item.""" - request.node.__scenario_report__ = ScenarioReport(scenario=scenario) + scenario_reports[request.node] = ScenarioReport(scenario=scenario) def step_error( @@ -160,7 +168,7 @@ def step_error( exception: Exception, ) -> None: """Finalize the step report as failed.""" - request.node.__scenario_report__.fail() + scenario_reports[request.node].fail() def before_step( @@ -171,7 +179,7 @@ def before_step( step_func: Callable[..., Any], ) -> None: """Store step start time.""" - request.node.__scenario_report__.add_step_report(StepReport(step=step)) + scenario_reports[request.node].add_step_report(StepReport(step=step)) def after_step( @@ -183,4 +191,4 @@ def after_step( step_func_args: dict, ) -> None: """Finalize the step report as successful.""" - request.node.__scenario_report__.current_step_report.finalize(failed=False) + scenario_reports[request.node].current_step_report.finalize(failed=False) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index d64b3f61a..720050639 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -25,6 +25,7 @@ from . import exceptions from .feature import get_feature, get_features +from .registry import step_function_marker_context from .steps import StepFunctionContext, get_step_fixture_name, inject_fixture from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path @@ -49,7 +50,7 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items()) for fixturename, fixturedefs in fixture_def_by_name: for pos, fixturedef in enumerate(fixturedefs): - step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None) + step_func_context = step_function_marker_context.get(fixturedef.func) if step_func_context is None: continue @@ -294,7 +295,7 @@ def get_features_base_dir(caller_module_path: str) -> str: return os.path.join(rootdir, d) -def get_from_ini(key: str, default: str) -> str: +def get_from_ini(key: str, default: T) -> str | T: """Get value from ini config. Return default if value has not been set. Use if the default value is dynamic. Otherwise set default on addini call. diff --git a/src/pytest_bdd/steps.py b/src/pytest_bdd/steps.py index 98f116d63..1bed9046a 100644 --- a/src/pytest_bdd/steps.py +++ b/src/pytest_bdd/steps.py @@ -47,6 +47,7 @@ def _(article): from .parser import Step from .parsers import StepParser, get_parser +from .registry import step_function_marker_context from .types import GIVEN, THEN, WHEN from .utils import get_caller_module_locals @@ -90,7 +91,7 @@ def given( :return: Decorator function for the step. """ - return step(name, GIVEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel) + return step(name, "given", converters=converters, target_fixture=target_fixture, stacklevel=stacklevel) def when( @@ -109,7 +110,7 @@ def when( :return: Decorator function for the step. """ - return step(name, WHEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel) + return step(name, "when", converters=converters, target_fixture=target_fixture, stacklevel=stacklevel) def then( @@ -128,7 +129,7 @@ def then( :return: Decorator function for the step. """ - return step(name, THEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel) + return step(name, "then", converters=converters, target_fixture=target_fixture, stacklevel=stacklevel) def step( @@ -171,7 +172,7 @@ def decorator(func: Callable[P, T]) -> Callable[P, T]: def step_function_marker() -> StepFunctionContext: return context - step_function_marker._pytest_bdd_step_context = context + step_function_marker_context[step_function_marker] = context caller_locals = get_caller_module_locals(stacklevel=stacklevel) fixture_step_name = find_unique_name( @@ -183,7 +184,7 @@ def step_function_marker() -> StepFunctionContext: return decorator -def find_unique_name(name: str, seen: Iterable[str]) -> str: +def find_unique_name(name: str, seen: Iterable[str]) -> str: # type: ignore[return] """Find unique name among a set of strings. New names are generated by appending an increasing number at the end of the name. diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 727a7486d..98bddd63f 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -3,6 +3,8 @@ import pytest +from pytest_bdd.registry import test_report_context + class OfType: """Helper object comparison to which is always 'equal'.""" @@ -100,7 +102,8 @@ def _(cucumbers, left): ) result = pytester.inline_run("-vvl") assert result.ret - report = result.matchreport("test_passing", when="call").scenario + report = result.matchreport("test_passing", when="call") + scenario = test_report_context[report].scenario expected = { "feature": { "description": "", @@ -133,9 +136,10 @@ def _(cucumbers, left): "tags": ["scenario-passing-tag"], } - assert report == expected + assert scenario == expected - report = result.matchreport("test_failing", when="call").scenario + report = result.matchreport("test_failing", when="call") + scenario = test_report_context[report].scenario expected = { "feature": { "description": "", @@ -167,9 +171,10 @@ def _(cucumbers, left): ], "tags": ["scenario-failing-tag"], } - assert report == expected + assert scenario == expected - report = result.matchreport("test_outlined[12-5-7]", when="call").scenario + report = result.matchreport("test_outlined[12-5-7]", when="call") + scenario = test_report_context[report].scenario expected = { "feature": { "description": "", @@ -209,9 +214,10 @@ def _(cucumbers, left): ], "tags": [], } - assert report == expected + assert scenario == expected - report = result.matchreport("test_outlined[5-4-1]", when="call").scenario + report = result.matchreport("test_outlined[5-4-1]", when="call") + scenario = test_report_context[report].scenario expected = { "feature": { "description": "", @@ -251,7 +257,7 @@ def _(cucumbers, left): ], "tags": [], } - assert report == expected + assert scenario == expected def test_complex_types(pytester, pytestconfig): @@ -316,5 +322,7 @@ def test_complex(alien): result = pytester.inline_run("-vvl") report = result.matchreport("test_complex[10,20-alien0]", when="call") assert report.passed - assert execnet.gateway_base.dumps(report.item) - assert execnet.gateway_base.dumps(report.scenario) + # TODO: Use test_report_context + report_context = test_report_context[report] + assert execnet.gateway_base.dumps(report_context.name) + assert execnet.gateway_base.dumps(report_context.scenario) From 95915bc3886b4b985b1d3742ef4ff434319ffd26 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 2 Dec 2023 23:21:18 +0100 Subject: [PATCH 03/25] Fix typing --- src/pytest_bdd/generation.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pytest_bdd/generation.py b/src/pytest_bdd/generation.py index a4c96a56f..aa24bf295 100644 --- a/src/pytest_bdd/generation.py +++ b/src/pytest_bdd/generation.py @@ -20,7 +20,7 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureDef, FixtureManager from _pytest.main import Session - from _pytest.python import Function + from _pytest.nodes import Node from .parser import Feature, ScenarioTemplate, Step @@ -123,9 +123,7 @@ def print_missing_code(scenarios: list[ScenarioTemplate], steps: list[Step]) -> tw.write(code) -def _find_step_fixturedef( - fixturemanager: FixtureManager, item: Function, step: Step -) -> Sequence[FixtureDef[Any]] | None: +def _find_step_fixturedef(fixturemanager: FixtureManager, item: Node, step: Step) -> Sequence[FixtureDef[Any]] | None: """Find step fixturedef.""" with inject_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, nodeid=item.nodeid): bdd_name = get_step_fixture_name(step=step) From fb5d80042d64e1c839977257b303e90380fe4b11 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 2 Dec 2023 23:21:27 +0100 Subject: [PATCH 04/25] Add todo --- src/pytest_bdd/scenario.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 720050639..406765523 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -237,6 +237,8 @@ def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper) scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}" + + # TODO: Use a WeakKeyDictionary to store the scenario object instead of attaching it to the function scenario_wrapper.__scenario__ = templated_scenario return cast(Callable[P, T], scenario_wrapper) From 53c1cc2f72a8bb48ed675c74b3171871857f67c2 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 2 Dec 2023 23:23:08 +0100 Subject: [PATCH 05/25] Bump deps --- poetry.lock | 142 ++++++++++++++++++++++++++-------------------------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/poetry.lock b/poetry.lock index a52c6a9f8..9aa6ae2ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "cachetools" -version = "5.3.1" +version = "5.3.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, ] [[package]] @@ -113,13 +113,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -141,19 +141,19 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.12.4" +version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, - {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] -typing = ["typing-extensions (>=4.7.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "iniconfig" @@ -168,13 +168,13 @@ files = [ [[package]] name = "mako" -version = "1.2.4" +version = "1.3.0" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, - {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, + {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, + {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, ] [package.dependencies] @@ -256,38 +256,38 @@ files = [ [[package]] name = "mypy" -version = "1.6.0" +version = "1.7.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:091f53ff88cb093dcc33c29eee522c087a438df65eb92acd371161c1f4380ff0"}, - {file = "mypy-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb7ff4007865833c470a601498ba30462b7374342580e2346bf7884557e40531"}, - {file = "mypy-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49499cf1e464f533fc45be54d20a6351a312f96ae7892d8e9f1708140e27ce41"}, - {file = "mypy-1.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c192445899c69f07874dabda7e931b0cc811ea055bf82c1ababf358b9b2a72c"}, - {file = "mypy-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:3df87094028e52766b0a59a3e46481bb98b27986ed6ded6a6cc35ecc75bb9182"}, - {file = "mypy-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c8835a07b8442da900db47ccfda76c92c69c3a575872a5b764332c4bacb5a0a"}, - {file = "mypy-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24f3de8b9e7021cd794ad9dfbf2e9fe3f069ff5e28cb57af6f873ffec1cb0425"}, - {file = "mypy-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856bad61ebc7d21dbc019b719e98303dc6256cec6dcc9ebb0b214b81d6901bd8"}, - {file = "mypy-1.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89513ddfda06b5c8ebd64f026d20a61ef264e89125dc82633f3c34eeb50e7d60"}, - {file = "mypy-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f8464ed410ada641c29f5de3e6716cbdd4f460b31cf755b2af52f2d5ea79ead"}, - {file = "mypy-1.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:971104bcb180e4fed0d7bd85504c9036346ab44b7416c75dd93b5c8c6bb7e28f"}, - {file = "mypy-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab98b8f6fdf669711f3abe83a745f67f50e3cbaea3998b90e8608d2b459fd566"}, - {file = "mypy-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a69db3018b87b3e6e9dd28970f983ea6c933800c9edf8c503c3135b3274d5ad"}, - {file = "mypy-1.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dccd850a2e3863891871c9e16c54c742dba5470f5120ffed8152956e9e0a5e13"}, - {file = "mypy-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8598307150b5722854f035d2e70a1ad9cc3c72d392c34fffd8c66d888c90f17"}, - {file = "mypy-1.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fea451a3125bf0bfe716e5d7ad4b92033c471e4b5b3e154c67525539d14dc15a"}, - {file = "mypy-1.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e28d7b221898c401494f3b77db3bac78a03ad0a0fff29a950317d87885c655d2"}, - {file = "mypy-1.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4b7a99275a61aa22256bab5839c35fe8a6887781862471df82afb4b445daae6"}, - {file = "mypy-1.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7469545380dddce5719e3656b80bdfbb217cfe8dbb1438532d6abc754b828fed"}, - {file = "mypy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7807a2a61e636af9ca247ba8494031fb060a0a744b9fee7de3a54bed8a753323"}, - {file = "mypy-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2dad072e01764823d4b2f06bc7365bb1d4b6c2f38c4d42fade3c8d45b0b4b67"}, - {file = "mypy-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b19006055dde8a5425baa5f3b57a19fa79df621606540493e5e893500148c72f"}, - {file = "mypy-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eba8a7a71f0071f55227a8057468b8d2eb5bf578c8502c7f01abaec8141b2f"}, - {file = "mypy-1.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e0db37ac4ebb2fee7702767dfc1b773c7365731c22787cb99f507285014fcaf"}, - {file = "mypy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:c69051274762cccd13498b568ed2430f8d22baa4b179911ad0c1577d336ed849"}, - {file = "mypy-1.6.0-py3-none-any.whl", hash = "sha256:9e1589ca150a51d9d00bb839bfeca2f7a04f32cd62fad87a847bc0818e15d7dc"}, - {file = "mypy-1.6.0.tar.gz", hash = "sha256:4f3d27537abde1be6d5f2c96c29a454da333a2a271ae7d5bc7110e6d4b7beb3f"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, ] [package.dependencies] @@ -298,6 +298,7 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -324,13 +325,13 @@ files = [ [[package]] name = "parse" -version = "1.19.1" +version = "1.20.0" description = "parse() is the opposite of format()" optional = false python-versions = "*" files = [ - {file = "parse-1.19.1-py2.py3-none-any.whl", hash = "sha256:371ed3800dc63983832159cc9373156613947707bc448b5215473a219dbd4362"}, - {file = "parse-1.19.1.tar.gz", hash = "sha256:cc3a47236ff05da377617ddefa867b7ba983819c664e1afe46249e5b469be464"}, + {file = "parse-1.20.0-py2.py3-none-any.whl", hash = "sha256:5e171b001452fa9f004c5a58a93525175468daf69b493e9fa915347ed7ff6968"}, + {file = "parse-1.20.0.tar.gz", hash = "sha256:bd28bae37714b45d5894d77160a16e2be36b64a3b618c81168b3684676aa498b"}, ] [[package]] @@ -355,13 +356,13 @@ testing = ["pytest (<5.0)", "pytest (>=5.0)", "pytest-html (>=1.19.0)"] [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, ] [package.extras] @@ -385,17 +386,18 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyproject-api" @@ -418,13 +420,13 @@ testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytes [[package]] name = "pytest" -version = "7.4.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -440,13 +442,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-xdist" -version = "3.3.1" +version = "3.5.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"}, - {file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"}, + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, ] [package.dependencies] @@ -482,13 +484,13 @@ files = [ [[package]] name = "tox" -version = "4.11.3" +version = "4.11.4" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.11.3-py3-none-any.whl", hash = "sha256:599af5e5bb0cad0148ac1558a0b66f8fff219ef88363483b8d92a81e4246f28f"}, - {file = "tox-4.11.3.tar.gz", hash = "sha256:5039f68276461fae6a9452a3b2c7295798f00a0e92edcd9a3b78ba1a73577951"}, + {file = "tox-4.11.4-py3-none-any.whl", hash = "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229"}, + {file = "tox-4.11.4.tar.gz", hash = "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1"}, ] [package.dependencies] @@ -509,13 +511,13 @@ testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pol [[package]] name = "types-setuptools" -version = "68.2.0.0" +version = "69.0.0.0" description = "Typing stubs for setuptools" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "types-setuptools-68.2.0.0.tar.gz", hash = "sha256:a4216f1e2ef29d089877b3af3ab2acf489eb869ccaf905125c69d2dc3932fd85"}, - {file = "types_setuptools-68.2.0.0-py3-none-any.whl", hash = "sha256:77edcc843e53f8fc83bb1a840684841f3dc804ec94562623bfa2ea70d5a2ba1b"}, + {file = "types-setuptools-69.0.0.0.tar.gz", hash = "sha256:b0a06219f628c6527b2f8ce770a4f47550e00d3e8c3ad83e2dc31bc6e6eda95d"}, + {file = "types_setuptools-69.0.0.0-py3-none-any.whl", hash = "sha256:8c86195bae2ad81e6dea900a570fe9d64a59dbce2b11cc63c046b03246ea77bf"}, ] [[package]] @@ -531,19 +533,19 @@ files = [ [[package]] name = "virtualenv" -version = "20.24.5" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, - {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] From 566d066102ec44c2a2ea2f5cf060873372da75a4 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:44:16 +0100 Subject: [PATCH 06/25] Move registry vars into the correct modules --- src/pytest_bdd/registry.py | 6 +----- src/pytest_bdd/reporting.py | 15 +++++++++------ src/pytest_bdd/scenario.py | 5 ++--- src/pytest_bdd/steps.py | 7 ++++--- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/pytest_bdd/registry.py b/src/pytest_bdd/registry.py index ac94ec86a..ff33a22d3 100644 --- a/src/pytest_bdd/registry.py +++ b/src/pytest_bdd/registry.py @@ -4,12 +4,8 @@ from weakref import WeakKeyDictionary if TYPE_CHECKING: - from _pytest.nodes import Item from _pytest.reports import TestReport - from pytest_bdd.reporting import ReportContext, ScenarioReport - from pytest_bdd.steps import StepFunctionContext + from pytest_bdd.reporting import ReportContext -scenario_reports: WeakKeyDictionary[Item, ScenarioReport] = WeakKeyDictionary() -step_function_marker_context: WeakKeyDictionary[Callable[..., Any], StepFunctionContext] = WeakKeyDictionary() test_report_context: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index d205b7f60..c02dca1bb 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -8,8 +8,9 @@ import time from dataclasses import dataclass from typing import TYPE_CHECKING +from weakref import WeakKeyDictionary -from .registry import scenario_reports, test_report_context +from .registry import test_report_context if TYPE_CHECKING: from typing import Any, Callable @@ -21,6 +22,8 @@ from .parser import Feature, Scenario, Step +scenario_reports_registry: WeakKeyDictionary[Item, ScenarioReport] = WeakKeyDictionary() + class StepReport: """Step execution report.""" @@ -146,7 +149,7 @@ class ReportContext: def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None: """Store item in the report object.""" try: - scenario_report: ScenarioReport = scenario_reports[item] + scenario_report: ScenarioReport = scenario_reports_registry[item] except KeyError: return @@ -155,7 +158,7 @@ def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None: def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None: """Create scenario report for the item.""" - scenario_reports[request.node] = ScenarioReport(scenario=scenario) + scenario_reports_registry[request.node] = ScenarioReport(scenario=scenario) def step_error( @@ -168,7 +171,7 @@ def step_error( exception: Exception, ) -> None: """Finalize the step report as failed.""" - scenario_reports[request.node].fail() + scenario_reports_registry[request.node].fail() def before_step( @@ -179,7 +182,7 @@ def before_step( step_func: Callable[..., Any], ) -> None: """Store step start time.""" - scenario_reports[request.node].add_step_report(StepReport(step=step)) + scenario_reports_registry[request.node].add_step_report(StepReport(step=step)) def after_step( @@ -191,4 +194,4 @@ def after_step( step_func_args: dict, ) -> None: """Finalize the step report as successful.""" - scenario_reports[request.node].current_step_report.finalize(failed=False) + scenario_reports_registry[request.node].current_step_report.finalize(failed=False) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 406765523..bb5885f8f 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -25,8 +25,7 @@ from . import exceptions from .feature import get_feature, get_features -from .registry import step_function_marker_context -from .steps import StepFunctionContext, get_step_fixture_name, inject_fixture +from .steps import StepFunctionContext, get_step_fixture_name, inject_fixture, step_function_context_registry from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if TYPE_CHECKING: @@ -50,7 +49,7 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items()) for fixturename, fixturedefs in fixture_def_by_name: for pos, fixturedef in enumerate(fixturedefs): - step_func_context = step_function_marker_context.get(fixturedef.func) + step_func_context = step_function_context_registry.get(fixturedef.func) if step_func_context is None: continue diff --git a/src/pytest_bdd/steps.py b/src/pytest_bdd/steps.py index 1bed9046a..507cfe3c1 100644 --- a/src/pytest_bdd/steps.py +++ b/src/pytest_bdd/steps.py @@ -40,6 +40,7 @@ def _(article): from dataclasses import dataclass, field from itertools import count from typing import Any, Callable, Iterable, Literal, TypeVar +from weakref import WeakKeyDictionary import pytest from _pytest.fixtures import FixtureDef, FixtureRequest @@ -47,13 +48,13 @@ def _(article): from .parser import Step from .parsers import StepParser, get_parser -from .registry import step_function_marker_context -from .types import GIVEN, THEN, WHEN from .utils import get_caller_module_locals P = ParamSpec("P") T = TypeVar("T") +step_function_context_registry: WeakKeyDictionary[Callable[..., Any], StepFunctionContext] = WeakKeyDictionary() + @enum.unique class StepNamePrefix(enum.Enum): @@ -172,7 +173,7 @@ def decorator(func: Callable[P, T]) -> Callable[P, T]: def step_function_marker() -> StepFunctionContext: return context - step_function_marker_context[step_function_marker] = context + step_function_context_registry[step_function_marker] = context caller_locals = get_caller_module_locals(stacklevel=stacklevel) fixture_step_name = find_unique_name( From d27485b9aae5689d128870a2fe91d4b9ad56b3d7 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:46:29 +0100 Subject: [PATCH 07/25] Move `test_report_context` into `reporting.py` module --- src/pytest_bdd/cucumber_json.py | 2 +- src/pytest_bdd/gherkin_terminal_reporter.py | 2 +- src/pytest_bdd/registry.py | 11 ----------- src/pytest_bdd/reporting.py | 3 +-- tests/feature/test_report.py | 2 +- 5 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 src/pytest_bdd/registry.py diff --git a/src/pytest_bdd/cucumber_json.py b/src/pytest_bdd/cucumber_json.py index 4281893ab..1b03544b3 100644 --- a/src/pytest_bdd/cucumber_json.py +++ b/src/pytest_bdd/cucumber_json.py @@ -7,7 +7,7 @@ import time import typing -from pytest_bdd.registry import test_report_context +from .reporting import test_report_context if typing.TYPE_CHECKING: from typing import Any diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index f1dcf4b0b..7c7fa8344 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -4,7 +4,7 @@ from _pytest.terminal import TerminalReporter -from pytest_bdd.registry import test_report_context +from .reporting import test_report_context if typing.TYPE_CHECKING: from typing import Any diff --git a/src/pytest_bdd/registry.py b/src/pytest_bdd/registry.py deleted file mode 100644 index ff33a22d3..000000000 --- a/src/pytest_bdd/registry.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable -from weakref import WeakKeyDictionary - -if TYPE_CHECKING: - from _pytest.reports import TestReport - - from pytest_bdd.reporting import ReportContext - -test_report_context: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index c02dca1bb..c4a223d5c 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -10,8 +10,6 @@ from typing import TYPE_CHECKING from weakref import WeakKeyDictionary -from .registry import test_report_context - if TYPE_CHECKING: from typing import Any, Callable @@ -23,6 +21,7 @@ from .parser import Feature, Scenario, Step scenario_reports_registry: WeakKeyDictionary[Item, ScenarioReport] = WeakKeyDictionary() +test_report_context: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() class StepReport: diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 98bddd63f..658f68926 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -3,7 +3,7 @@ import pytest -from pytest_bdd.registry import test_report_context +from pytest_bdd.reporting import test_report_context class OfType: From fb0efdcf40ec28a9de0c005c5abbe3019fc508c4 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:47:52 +0100 Subject: [PATCH 08/25] Bump `mypy` version --- poetry.lock | 58 +++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9aa6ae2ce..46226cf28 100644 --- a/poetry.lock +++ b/poetry.lock @@ -256,38 +256,38 @@ files = [ [[package]] name = "mypy" -version = "1.7.1" +version = "1.8.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, - {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, - {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, - {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, - {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, - {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, - {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, - {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, - {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, - {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, - {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, - {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, - {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, - {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, - {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, - {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, - {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, - {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, - {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, ] [package.dependencies] @@ -554,4 +554,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "5b7935aa2b2d579d4fdb8361b414d37fff142a359332d859ed1513209ca3b1a6" +content-hash = "337c62658ca9a378b010c4b879524eeef161e06a07e69c1030b2f7db6e507f5d" diff --git a/pyproject.toml b/pyproject.toml index 0ecc528f1..646d628ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ typing-extensions = "*" [tool.poetry.group.dev.dependencies] tox = ">=4.11.3" -mypy = ">=1.6.0" +mypy = "^1.8.0" types-setuptools = ">=68.2.0.0" pytest-xdist = ">=3.3.1" coverage = {extras = ["toml"], version = ">=6.5.0"} From 4efdd5e6c555dc56c8df31c1bbdf6cc785d8cef0 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:51:14 +0100 Subject: [PATCH 09/25] Fix return type of `scenario_wrapper` --- src/pytest_bdd/scenario.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index bb5885f8f..d3a066eb4 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -216,7 +216,7 @@ def decorator(*args: Callable[P, T]) -> Callable[P, T]: # We need to tell pytest that the original function requires its fixtures, # otherwise indirect fixtures would not work. @pytest.mark.usefixtures(*func_args) - def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str]) -> Any: + def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str]) -> T: __tracebackhide__ = True scenario = templated_scenario.render(_pytest_bdd_example) _execute_scenario(feature, scenario, request) From 425b9e5917efca1329fe4108cdfc546f236104e9 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:07:01 +0100 Subject: [PATCH 10/25] Use WeakKeyDictionary instead of storing private attribute `__scenario__` --- src/pytest_bdd/generation.py | 10 ++++++++-- src/pytest_bdd/scenario.py | 12 +++++++----- src/pytest_bdd/utils.py | 11 +++++++++++ tests/feature/test_description.py | 7 +++++-- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/pytest_bdd/generation.py b/src/pytest_bdd/generation.py index aa24bf295..bc6d1cf2a 100644 --- a/src/pytest_bdd/generation.py +++ b/src/pytest_bdd/generation.py @@ -9,7 +9,13 @@ from mako.lookup import TemplateLookup from .feature import get_features -from .scenario import inject_fixturedefs_for_step, make_python_docstring, make_python_name, make_string_literal +from .scenario import ( + inject_fixturedefs_for_step, + make_python_docstring, + make_python_name, + make_string_literal, + scenario_wrapper_template_registry, +) from .steps import get_step_fixture_name from .types import STEP_TYPES @@ -177,7 +183,7 @@ def _show_missing_code_main(config: Config, session: Session) -> None: features, scenarios, steps = parse_feature_files(config.option.features) for item in session.items: - if scenario := getattr(item.obj, "__scenario__", None): + if (scenario := scenario_wrapper_template_registry.get(item.obj)) is not None: if scenario in scenarios: scenarios.remove(scenario) for step in scenario.steps: diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index d3a066eb4..926100be5 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -17,6 +17,7 @@ import os import re from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, TypeVar, cast +from weakref import WeakKeyDictionary import pytest from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func @@ -26,7 +27,7 @@ from . import exceptions from .feature import get_feature, get_features from .steps import StepFunctionContext, get_step_fixture_name, inject_fixture, step_function_context_registry -from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path +from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path, registry_get_safe if TYPE_CHECKING: from _pytest.mark.structures import ParameterSet @@ -42,6 +43,8 @@ PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") +scenario_wrapper_template_registry: WeakKeyDictionary[Callable[..., Any], ScenarioTemplate] = WeakKeyDictionary() + def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid: str) -> Iterable[FixtureDef[Any]]: """Find the fixture defs that can parse a step.""" @@ -237,8 +240,7 @@ def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}" - # TODO: Use a WeakKeyDictionary to store the scenario object instead of attaching it to the function - scenario_wrapper.__scenario__ = templated_scenario + scenario_wrapper_template_registry[scenario_wrapper] = templated_scenario return cast(Callable[P, T], scenario_wrapper) return decorator @@ -359,9 +361,9 @@ def scenarios(*feature_paths: str, **kwargs: Any) -> None: found = False module_scenarios = frozenset( - (attr.__scenario__.feature.filename, attr.__scenario__.name) + (s.feature.filename, s.name) for name, attr in caller_locals.items() - if hasattr(attr, "__scenario__") + if (s := registry_get_safe(scenario_wrapper_template_registry, attr)) is not None ) for feature in get_features(abs_feature_paths): diff --git a/src/pytest_bdd/utils.py b/src/pytest_bdd/utils.py index 067e8d81a..52aeeb914 100644 --- a/src/pytest_bdd/utils.py +++ b/src/pytest_bdd/utils.py @@ -7,6 +7,7 @@ from inspect import getframeinfo, signature from sys import _getframe from typing import TYPE_CHECKING, TypeVar, cast +from weakref import WeakKeyDictionary if TYPE_CHECKING: from typing import Any, Callable @@ -82,3 +83,13 @@ def setdefault(obj: object, name: str, default: T) -> T: except AttributeError: setattr(obj, name, default) return default + + +def registry_get_safe(registry: WeakKeyDictionary[Any, T], key: Any, default=None) -> T | None: + """Get a value from a registry, or None if the key is not in the registry. + It ensures that this works even if the key cannot be weak-referenced (normally this would raise a TypeError). + """ + try: + return registry.get(key, default) + except TypeError: + return None diff --git a/tests/feature/test_description.py b/tests/feature/test_description.py index 5d0dcb96f..c6f637fcf 100644 --- a/tests/feature/test_description.py +++ b/tests/feature/test_description.py @@ -33,6 +33,7 @@ def test_description(pytester): """\ import textwrap from pytest_bdd import given, scenario + from pytest_bdd.scenario import scenario_wrapper_template_registry @scenario("description.feature", "Description") def test_description(): @@ -44,7 +45,8 @@ def _(): return "bar" def test_feature_description(): - assert test_description.__scenario__.feature.description == textwrap.dedent( + scenario = scenario_wrapper_template_registry[test_description] + assert scenario.feature.description == textwrap.dedent( \"\"\"\\ In order to achieve something I want something @@ -55,7 +57,8 @@ def test_feature_description(): ) def test_scenario_description(): - assert test_description.__scenario__.description == textwrap.dedent( + scenario = scenario_wrapper_template_registry[test_description] + assert scenario.description == textwrap.dedent( \"\"\"\\ Also, the scenario can have a description. From 6cfce0de078b1fb4b4d6e36c91754c810419c159 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:07:23 +0100 Subject: [PATCH 11/25] Remove addressed TODO --- tests/feature/test_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 658f68926..d6e2ddcb2 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -322,7 +322,7 @@ def test_complex(alien): result = pytester.inline_run("-vvl") report = result.matchreport("test_complex[10,20-alien0]", when="call") assert report.passed - # TODO: Use test_report_context + report_context = test_report_context[report] assert execnet.gateway_base.dumps(report_context.name) assert execnet.gateway_base.dumps(report_context.scenario) From 1c1741be1f95409b23c8e8045eee9857b823e93d Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:18:19 +0100 Subject: [PATCH 12/25] Fix type annotation for `scenario`. It was never supposed to return a callable with the same ParamSpec --- src/pytest_bdd/scenario.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 926100be5..535bf0b05 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -34,7 +34,6 @@ from .parser import Feature, Scenario, ScenarioTemplate, Step -P = ParamSpec("P") T = TypeVar("T") logger = logging.getLogger(__name__) @@ -201,14 +200,14 @@ def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequ def _get_scenario_decorator( feature: Feature, feature_name: str, templated_scenario: ScenarioTemplate, scenario_name: str -) -> Callable[[Callable[P, T]], Callable[P, T]]: +) -> Callable[[Callable[..., T]], Callable[..., T]]: # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception # when the decorator is misused. # Pytest inspect the signature to determine the required fixtures, and in that case it would look # for a fixture called "fn" that doesn't exist (if it exists then it's even worse). # It will error with a "fixture 'fn' not found" message instead. # We can avoid this hack by using a pytest hook and check for misuse instead. - def decorator(*args: Callable[P, T]) -> Callable[P, T]: + def decorator(*args: Callable[..., T]) -> Callable[..., T]: if not args: raise exceptions.ScenarioIsDecoratorOnly( "scenario function can only be used as a decorator. Refer to the documentation." @@ -241,7 +240,7 @@ def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}" scenario_wrapper_template_registry[scenario_wrapper] = templated_scenario - return cast(Callable[P, T], scenario_wrapper) + return scenario_wrapper return decorator @@ -260,7 +259,7 @@ def scenario( scenario_name: str, encoding: str = "utf-8", features_base_dir: str | None = None, -) -> Callable[[Callable[P, T]], Callable[P, T]]: +) -> Callable[[Callable[..., T]], Callable[..., T]]: """Scenario decorator. :param str feature_name: Feature file name. Absolute or relative to the configured feature base path. From 5115a98afba5b256ff887bdcdf993f7748dfcc87 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:23:27 +0100 Subject: [PATCH 13/25] Fix accessing `item.obj` (mypy was rightfully complaining) --- src/pytest_bdd/generation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pytest_bdd/generation.py b/src/pytest_bdd/generation.py index bc6d1cf2a..edf6dbf76 100644 --- a/src/pytest_bdd/generation.py +++ b/src/pytest_bdd/generation.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, cast from _pytest._io import TerminalWriter +from _pytest.python import Function from mako.lookup import TemplateLookup from .feature import get_features @@ -183,6 +184,8 @@ def _show_missing_code_main(config: Config, session: Session) -> None: features, scenarios, steps = parse_feature_files(config.option.features) for item in session.items: + if not isinstance(item, Function): + continue if (scenario := scenario_wrapper_template_registry.get(item.obj)) is not None: if scenario in scenarios: scenarios.remove(scenario) From 51c21737fd61c4344eb094756b5fa5a8d4d16c5f Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:46:53 +0100 Subject: [PATCH 14/25] Add changelog entry --- CHANGES.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a99865800..9ba01d27d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,10 +3,17 @@ Changelog Unreleased ---------- +- Address many ``mypy`` warnings. The following private attributes are not available anymore: + + * ``_pytest.reports.TestReport.scenario`` (replaced by ``pytest_bdd.reporting.test_report_context`` WeakKeyDictionary) + * ``__scenario__`` attribute of test functions generated by the ``@scenario`` (and ``@scenarios``) decorator (replaced by ``pytest_bdd.scenario.scenario_wrapper_template_registry`` WeakKeyDictionary) + * ``_pytest.nodes.Item.__scenario_report__`` (replaced by ``pytest_bdd.reporting.scenario_reports_registry`` WeakKeyDictionary) + * ``_pytest_bdd_step_context`` attribute of internal test function markers (replaced by ``pytest_bdd.steps.step_function_context_registry`` WeakKeyDictionary) + `#658 `_ 7.0.1 ----- -- Fix errors occurring if `pytest_unconfigure` is called before `pytest_configure`. `#362 `_ `#641 `_ +- Fix errors occurring if ``pytest_unconfigure`` is called before `pytest_configure`. `#362 `_ `#641 `_ 7.0.0 ---------- From 55db00545b1ea188e4b1fb8976cfc53cf0a3dde3 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:14:47 +0100 Subject: [PATCH 15/25] Fix typing for registry_get_safe --- src/pytest_bdd/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pytest_bdd/utils.py b/src/pytest_bdd/utils.py index 7d4813118..e4f55dbd0 100644 --- a/src/pytest_bdd/utils.py +++ b/src/pytest_bdd/utils.py @@ -17,6 +17,8 @@ from _pytest.pytester import RunResult T = TypeVar("T") +K = TypeVar("K") +V = TypeVar("V") CONFIG_STACK: list[Config] = [] @@ -86,7 +88,7 @@ def setdefault(obj: object, name: str, default: T) -> T: return default -def registry_get_safe(registry: WeakKeyDictionary[Any, T], key: Any, default: T | None = None) -> T | None: +def registry_get_safe(registry: WeakKeyDictionary[K, V], key: K, default: V | None = None) -> V | None: """Get a value from a registry, or None if the key is not in the registry. It ensures that this works even if the key cannot be weak-referenced (normally this would raise a TypeError). """ From 2cb65729997698a1a186ad5af5b10c2c56c9161d Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:16:50 +0100 Subject: [PATCH 16/25] Fix typing for `StepReport` args --- src/pytest_bdd/reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index fa85e0b3b..666195245 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -28,8 +28,8 @@ class StepReport: """Step execution report.""" - failed = False - stopped = None + failed: bool = False + stopped: float | None = None def __init__(self, step: Step) -> None: """Step report constructor. From f52341c698c13ef7e5719e9bdbb483ffaa04169f Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:17:56 +0100 Subject: [PATCH 17/25] Fix changelog --- CHANGES.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 75dd79c8c..a60b1d41f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,13 +8,6 @@ and this project adheres to `Semantic Versioning `_ Added +++++ @@ -31,6 +24,11 @@ Removed Fixed +++++ * Fixed an issue with the upcoming pytest release related to the use of ``@pytest.mark.usefixtures`` with an empty list. +* Address many ``mypy`` warnings. The following private attributes are not available anymore (`#658 `_): + * ``_pytest.reports.TestReport.scenario`` (replaced by ``pytest_bdd.reporting.test_report_context`` WeakKeyDictionary) + * ``__scenario__`` attribute of test functions generated by the ``@scenario`` (and ``@scenarios``) decorator (replaced by ``pytest_bdd.scenario.scenario_wrapper_template_registry`` WeakKeyDictionary) + * ``_pytest.nodes.Item.__scenario_report__`` (replaced by ``pytest_bdd.reporting.scenario_reports_registry`` WeakKeyDictionary) + * ``_pytest_bdd_step_context`` attribute of internal test function markers (replaced by ``pytest_bdd.steps.step_function_context_registry`` WeakKeyDictionary) Security ++++++++ From 02296c26444e58c8e88ec1e22971722b0a6394fa Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:20:05 +0100 Subject: [PATCH 18/25] Undo changes to `pyproject.toml` and `poetry.lock` --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0ebb01157..bf8f8df2f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1316,4 +1316,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9" -content-hash = "93c758cd2d90dfa1bee9ac49dd7052f9f6a6989a8a758d435b14a6b49588dd7d" +content-hash = "c14dd53bbf4f9bc1284da14ba9fab7b96c05be005ef3565a53a26d203a745397" diff --git a/pyproject.toml b/pyproject.toml index 88266a414..677260a60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ gherkin-official = "^29.0.0" [tool.poetry.group.dev.dependencies] tox = ">=4.11.3" -mypy = "^1.8.0" +mypy = ">=1.6.0" types-setuptools = ">=68.2.0.0" pytest-xdist = ">=3.3.1" coverage = {extras = ["toml"], version = ">=6.5.0"} From 6f3dd9db1dca2bfb8965e63d0baee0c0aac62c8c Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:22:56 +0100 Subject: [PATCH 19/25] `test_report_context` -> `test_report_context_registry` This way we are consistent with the other registries --- src/pytest_bdd/cucumber_json.py | 6 +++--- src/pytest_bdd/gherkin_terminal_reporter.py | 4 ++-- src/pytest_bdd/reporting.py | 4 ++-- tests/feature/test_report.py | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pytest_bdd/cucumber_json.py b/src/pytest_bdd/cucumber_json.py index 03d2d5a31..c9a40818f 100644 --- a/src/pytest_bdd/cucumber_json.py +++ b/src/pytest_bdd/cucumber_json.py @@ -8,7 +8,7 @@ import time import typing -from .reporting import test_report_context +from .reporting import test_report_context_registry if typing.TYPE_CHECKING: from typing import Any @@ -89,7 +89,7 @@ def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]: def pytest_runtest_logreport(self, report: TestReport) -> None: try: - scenario = test_report_context[report].scenario + scenario = test_report_context_registry[report].scenario except KeyError: # skip reporting for non-bdd tests return @@ -130,7 +130,7 @@ def stepmap(step: dict[str, Any]) -> dict[str, Any]: self.features[scenario["feature"]["filename"]]["elements"].append( { "keyword": scenario["keyword"], - "id": test_report_context[report].name, + "id": test_report_context_registry[report].name, "name": scenario["name"], "line": scenario["line_number"], "description": "", diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index 79e2908b4..95fbfd9df 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -4,7 +4,7 @@ from _pytest.terminal import TerminalReporter -from .reporting import test_report_context +from .reporting import test_report_context_registry if typing.TYPE_CHECKING: from typing import Any @@ -72,7 +72,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> Any: rule_markup = {"purple": True} try: - scenario = test_report_context[report].scenario + scenario = test_report_context_registry[report].scenario except KeyError: scenario = None diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index 666195245..4958b4da7 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -22,7 +22,7 @@ from .parser import Feature, Scenario, Step scenario_reports_registry: WeakKeyDictionary[Item, ScenarioReport] = WeakKeyDictionary() -test_report_context: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() +test_report_context_registry: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() class StepReport: @@ -165,7 +165,7 @@ def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None: except KeyError: return - test_report_context[rep] = ReportContext(scenario=scenario_report.serialize(), name=item.name) + test_report_context_registry[rep] = ReportContext(scenario=scenario_report.serialize(), name=item.name) def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenario) -> None: diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 4f6c5e99c..23405a671 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -5,7 +5,7 @@ import pytest -from pytest_bdd.reporting import test_report_context +from pytest_bdd.reporting import test_report_context_registry class OfType: @@ -105,7 +105,7 @@ def _(cucumbers, left): result = pytester.inline_run("-vvl") assert result.ret report = result.matchreport("test_passing", when="call") - scenario = test_report_context[report].scenario + scenario = test_report_context_registry[report].scenario expected = { "feature": { "description": "", @@ -144,7 +144,7 @@ def _(cucumbers, left): assert scenario == expected report = result.matchreport("test_failing", when="call") - scenario = test_report_context[report].scenario + scenario = test_report_context_registry[report].scenario expected = { "feature": { "description": "", @@ -182,7 +182,7 @@ def _(cucumbers, left): assert scenario == expected report = result.matchreport("test_outlined[12-5-7]", when="call") - scenario = test_report_context[report].scenario + scenario = test_report_context_registry[report].scenario expected = { "feature": { "description": "", @@ -228,7 +228,7 @@ def _(cucumbers, left): assert scenario == expected report = result.matchreport("test_outlined[5-4-1]", when="call") - scenario = test_report_context[report].scenario + scenario = test_report_context_registry[report].scenario expected = { "feature": { "description": "", @@ -337,6 +337,6 @@ def test_complex(alien): report = result.matchreport("test_complex[10,20-alien0]", when="call") assert report.passed - report_context = test_report_context[report] + report_context = test_report_context_registry[report] assert execnet.gateway_base.dumps(report_context.name) assert execnet.gateway_base.dumps(report_context.scenario) From 49f363e06ecf5a5619d715794d175d33beca978e Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:26:20 +0100 Subject: [PATCH 20/25] Improve type specificity --- src/pytest_bdd/scenario.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 3f9804af0..79c2baad7 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -257,14 +257,14 @@ def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequ def _get_scenario_decorator( feature: Feature, feature_name: str, templated_scenario: ScenarioTemplate, scenario_name: str -) -> Callable[[Callable[..., T]], Callable[..., T]]: +) -> Callable[[Callable[..., T]], Callable[[FixtureRequest, dict[str, str]], T]]: # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception # when the decorator is misused. # Pytest inspect the signature to determine the required fixtures, and in that case it would look # for a fixture called "fn" that doesn't exist (if it exists then it's even worse). # It will error with a "fixture 'fn' not found" message instead. # We can avoid this hack by using a pytest hook and check for misuse instead. - def decorator(*args: Callable[..., T]) -> Callable[..., T]: + def decorator(*args: Callable[..., T]) -> Callable[[FixtureRequest, dict[str, str]], T]: if not args: raise exceptions.ScenarioIsDecoratorOnly( "scenario function can only be used as a decorator. Refer to the documentation." From 023c6d66a703ca56f1d265a4d2db7296a7588f01 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:11:56 +0100 Subject: [PATCH 21/25] Remove usages of `typing.Any` when possible --- src/pytest_bdd/compat.py | 13 ++++--- src/pytest_bdd/cucumber_json.py | 40 +++++++++++++-------- src/pytest_bdd/generation.py | 11 +++--- src/pytest_bdd/gherkin_terminal_reporter.py | 5 +-- src/pytest_bdd/parser.py | 13 ++++--- src/pytest_bdd/plugin.py | 12 +++---- src/pytest_bdd/reporting.py | 8 ++--- src/pytest_bdd/scenario.py | 6 ++-- src/pytest_bdd/steps.py | 16 ++++----- src/pytest_bdd/utils.py | 20 ++++++----- 10 files changed, 81 insertions(+), 63 deletions(-) diff --git a/src/pytest_bdd/compat.py b/src/pytest_bdd/compat.py index bdce0074c..18829d46d 100644 --- a/src/pytest_bdd/compat.py +++ b/src/pytest_bdd/compat.py @@ -2,7 +2,6 @@ from collections.abc import Sequence from importlib.metadata import version -from typing import Any from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest from _pytest.nodes import Node @@ -14,10 +13,12 @@ if pytest_version.release >= (8, 1): - def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None: + def getfixturedefs( + fixturemanager: FixtureManager, fixturename: str, node: Node + ) -> Sequence[FixtureDef[object]] | None: return fixturemanager.getfixturedefs(fixturename, node) - def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None: + def inject_fixture(request: FixtureRequest, arg: str, value: object) -> None: """Inject fixture into pytest fixture request. :param request: pytest fixture request @@ -38,10 +39,12 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None: else: - def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None: + def getfixturedefs( + fixturemanager: FixtureManager, fixturename: str, node: Node + ) -> Sequence[FixtureDef[object]] | None: return fixturemanager.getfixturedefs(fixturename, node.nodeid) # type: ignore - def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None: + def inject_fixture(request: FixtureRequest, arg: str, value: object) -> None: """Inject fixture into pytest fixture request. :param request: pytest fixture request diff --git a/src/pytest_bdd/cucumber_json.py b/src/pytest_bdd/cucumber_json.py index c9a40818f..5d016ec21 100644 --- a/src/pytest_bdd/cucumber_json.py +++ b/src/pytest_bdd/cucumber_json.py @@ -6,13 +6,13 @@ import math import os import time -import typing +from typing import TYPE_CHECKING, Any, Literal, TypedDict -from .reporting import test_report_context_registry +from typing_extensions import NotRequired -if typing.TYPE_CHECKING: - from typing import Any +from .reporting import test_report_context_registry +if TYPE_CHECKING: from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.reports import TestReport @@ -48,6 +48,12 @@ def unconfigure(config: Config) -> None: config.pluginmanager.unregister(xml) +class Result(TypedDict): + status: Literal["passed", "failed", "skipped"] + duration: int # in nanoseconds + error_message: NotRequired[str] + + class LogBDDCucumberJSON: """Logging plugin for cucumber like json output.""" @@ -56,22 +62,28 @@ def __init__(self, logfile: str) -> None: self.logfile = os.path.normpath(os.path.abspath(logfile)) self.features: dict[str, dict] = {} - def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> dict[str, Any]: + def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> Result: """Get scenario test run result. :param step: `Step` step we get result for :param report: pytest `Report` object :return: `dict` in form {"status": "", ["error_message": ""]} """ - result: dict[str, Any] = {} - if report.passed or not step["failed"]: # ignore setup/teardown - result = {"status": "passed"} - elif report.failed: - result = {"status": "failed", "error_message": str(report.longrepr) if error_message else ""} - elif report.skipped: - result = {"status": "skipped"} - result["duration"] = int(math.floor((10**9) * step["duration"])) # nanosec - return result + status: Literal["passed", "failed", "skipped"] + res_message = None + if report.outcome == "passed" or not step["failed"]: # ignore setup/teardown + status = "passed" + elif report.outcome == "failed": + status = "failed" + res_message = str(report.longrepr) if error_message else "" + elif report.outcome == "skipped": + status = "skipped" + else: + raise ValueError(f"Unknown test outcome {report.outcome}") + res: Result = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec + if res_message is not None: + res["error_message"] = res_message + return res def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]: """Serialize item's tags. diff --git a/src/pytest_bdd/generation.py b/src/pytest_bdd/generation.py index 12f1af774..9add0c72f 100644 --- a/src/pytest_bdd/generation.py +++ b/src/pytest_bdd/generation.py @@ -25,7 +25,6 @@ if TYPE_CHECKING: from collections.abc import Sequence - from typing import Any from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -133,14 +132,18 @@ def print_missing_code(scenarios: list[ScenarioTemplate], steps: list[Step]) -> tw.write(code) -def _find_step_fixturedef(fixturemanager: FixtureManager, item: Node, step: Step) -> Sequence[FixtureDef[Any]] | None: +def _find_step_fixturedef( + fixturemanager: FixtureManager, item: Node, step: Step +) -> Sequence[FixtureDef[object]] | None: """Find step fixturedef.""" with inject_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, node=item): bdd_name = get_step_fixture_name(step=step) return getfixturedefs(fixturemanager, bdd_name, item) -def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]: +def parse_feature_files( + paths: list[str], encoding: str = "utf-8" +) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]: """Parse feature files of given paths. :param paths: `list` of paths (file or dirs) @@ -148,7 +151,7 @@ def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], :return: `list` of `tuple` in form: (`list` of `Feature` objects, `list` of `Scenario` objects, `list` of `Step` objects). """ - features = get_features(paths, **kwargs) + features = get_features(paths, encoding=encoding) scenarios = sorted( itertools.chain.from_iterable(feature.scenarios.values() for feature in features), key=lambda scenario: (scenario.feature.name or scenario.feature.filename, scenario.name), diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index 95fbfd9df..e92808cde 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -7,8 +7,6 @@ from .reporting import test_report_context_registry if typing.TYPE_CHECKING: - from typing import Any - from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.reports import TestReport @@ -50,7 +48,7 @@ def __init__(self, config: Config) -> None: super().__init__(config) self.current_rule = None - def pytest_runtest_logreport(self, report: TestReport) -> Any: + def pytest_runtest_logreport(self, report: TestReport) -> None: rep = report res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) cat, letter, word = res @@ -120,4 +118,3 @@ def pytest_runtest_logreport(self, report: TestReport) -> Any: self._tw.write("\n\n") self.stats.setdefault(cat, []).append(rep) - return None diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index ff4a0619f..4c252e601 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -6,7 +6,6 @@ from collections import OrderedDict from collections.abc import Generator, Iterable, Mapping, Sequence from dataclasses import dataclass, field -from typing import Any from .exceptions import StepError from .gherkin_parser import Background as GherkinBackground @@ -95,11 +94,11 @@ def add_example(self, values: Sequence[str]) -> None: """ self.examples.append([str(value) if value is not None else "" for value in values]) - def as_contexts(self) -> Iterable[dict[str, Any]]: + def as_contexts(self) -> Generator[dict[str, str]]: """Generate contexts for the examples. Yields: - Dict[str, Any]: A dictionary mapping parameter names to their values for each example row. + dict[str, str]: A dictionary mapping parameter names to their values for each example row. """ for row in self.examples: assert len(self.example_params) == len(row) @@ -180,11 +179,11 @@ def steps(self) -> list[Step]: """ return self.all_background_steps + self._steps - def render(self, context: Mapping[str, Any]) -> Scenario: + def render(self, context: Mapping[str, object]) -> Scenario: """Render the scenario with the given context. Args: - context (Mapping[str, Any]): The context for rendering steps. + context (Mapping[str, object]): The context for rendering steps. Returns: Scenario: A Scenario object with steps rendered based on the context. @@ -308,11 +307,11 @@ def params(self) -> tuple[str, ...]: """ return tuple(frozenset(STEP_PARAM_RE.findall(self.name))) - def render(self, context: Mapping[str, Any]) -> str: + def render(self, context: Mapping[str, object]) -> str: """Render the step name with the given context, but avoid replacing text inside angle brackets if context is missing. Args: - context (Mapping[str, Any]): The context for rendering the step name. + context (Mapping[str, object]): The context for rendering the step name. Returns: str: The rendered step name with parameters replaced only if they exist in the context. diff --git a/src/pytest_bdd/plugin.py b/src/pytest_bdd/plugin.py index c397134bb..a9f4cff91 100644 --- a/src/pytest_bdd/plugin.py +++ b/src/pytest_bdd/plugin.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Generator -from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast +from typing import TYPE_CHECKING, Callable, TypeVar, cast import pytest from typing_extensions import ParamSpec @@ -99,8 +99,8 @@ def pytest_bdd_step_error( feature: Feature, scenario: Scenario, step: Step, - step_func: Callable[..., Any], - step_func_args: dict, + step_func: Callable[..., object], + step_func_args: dict[str, object], exception: Exception, ) -> None: reporting.step_error(request, feature, scenario, step, step_func, step_func_args, exception) @@ -112,7 +112,7 @@ def pytest_bdd_before_step( feature: Feature, scenario: Scenario, step: Step, - step_func: Callable[..., Any], + step_func: Callable[..., object], ) -> None: reporting.before_step(request, feature, scenario, step, step_func) @@ -123,8 +123,8 @@ def pytest_bdd_after_step( feature: Feature, scenario: Scenario, step: Step, - step_func: Callable[..., Any], - step_func_args: dict[str, Any], + step_func: Callable[..., object], + step_func_args: dict[str, object], ) -> None: reporting.after_step(request, feature, scenario, step, step_func, step_func_args) diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index 4958b4da7..2706a28e4 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -39,7 +39,7 @@ def __init__(self, step: Step) -> None: self.step = step self.started = time.perf_counter() - def serialize(self) -> dict[str, Any]: + def serialize(self) -> dict[str, object]: """Serialize the step execution report. :return: Serialized step execution report. @@ -103,7 +103,7 @@ def add_step_report(self, step_report: StepReport) -> None: """ self.step_reports.append(step_report) - def serialize(self) -> dict[str, Any]: + def serialize(self) -> dict[str, object]: """Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode. :return: Serialized report. @@ -178,8 +178,8 @@ def step_error( feature: Feature, scenario: Scenario, step: Step, - step_func: Callable[..., Any], - step_func_args: dict, + step_func: Callable[..., object], + step_func_args: dict[str, object], exception: Exception, ) -> None: """Finalize the step report as failed.""" diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 79c2baad7..11ed967b3 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -44,10 +44,10 @@ PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") -scenario_wrapper_template_registry: WeakKeyDictionary[Callable[..., Any], ScenarioTemplate] = WeakKeyDictionary() +scenario_wrapper_template_registry: WeakKeyDictionary[Callable[..., object], ScenarioTemplate] = WeakKeyDictionary() -def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: Node) -> Iterable[FixtureDef[Any]]: +def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: Node) -> Iterable[FixtureDef[object]]: """Find the fixture defs that can parse a step.""" # happens to be that _arg2fixturedefs is changed during the iteration so we use a copy fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items()) @@ -64,7 +64,7 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: if not match: continue - fixturedefs = cast(list[FixtureDef[Any]], getfixturedefs(fixturemanager, fixturename, node) or []) + fixturedefs = list(getfixturedefs(fixturemanager, fixturename, node) or []) if fixturedef not in fixturedefs: continue diff --git a/src/pytest_bdd/steps.py b/src/pytest_bdd/steps.py index 529e9f961..2fbfa0c3e 100644 --- a/src/pytest_bdd/steps.py +++ b/src/pytest_bdd/steps.py @@ -41,7 +41,7 @@ def _(article): from collections.abc import Iterable from dataclasses import dataclass, field from itertools import count -from typing import Any, Callable, Literal, TypeVar +from typing import Callable, Literal, TypeVar from weakref import WeakKeyDictionary import pytest @@ -54,7 +54,7 @@ def _(article): P = ParamSpec("P") T = TypeVar("T") -step_function_context_registry: WeakKeyDictionary[Callable[..., Any], StepFunctionContext] = WeakKeyDictionary() +step_function_context_registry: WeakKeyDictionary[Callable[..., object], StepFunctionContext] = WeakKeyDictionary() @enum.unique @@ -66,9 +66,9 @@ class StepNamePrefix(enum.Enum): @dataclass class StepFunctionContext: type: Literal["given", "when", "then"] | None - step_func: Callable[..., Any] + step_func: Callable[..., object] parser: StepParser - converters: dict[str, Callable[[str], Any]] = field(default_factory=dict) + converters: dict[str, Callable[[str], object]] = field(default_factory=dict) target_fixture: str | None = None @@ -79,7 +79,7 @@ def get_step_fixture_name(step: Step) -> str: def given( name: str | StepParser, - converters: dict[str, Callable[[str], Any]] | None = None, + converters: dict[str, Callable[[str], object]] | None = None, target_fixture: str | None = None, stacklevel: int = 1, ) -> Callable[[Callable[P, T]], Callable[P, T]]: @@ -98,7 +98,7 @@ def given( def when( name: str | StepParser, - converters: dict[str, Callable[[str], Any]] | None = None, + converters: dict[str, Callable[[str], object]] | None = None, target_fixture: str | None = None, stacklevel: int = 1, ) -> Callable[[Callable[P, T]], Callable[P, T]]: @@ -117,7 +117,7 @@ def when( def then( name: str | StepParser, - converters: dict[str, Callable[[str], Any]] | None = None, + converters: dict[str, Callable[[str], object]] | None = None, target_fixture: str | None = None, stacklevel: int = 1, ) -> Callable[[Callable[P, T]], Callable[P, T]]: @@ -137,7 +137,7 @@ def then( def step( name: str | StepParser, type_: Literal["given", "when", "then"] | None = None, - converters: dict[str, Callable[[str], Any]] | None = None, + converters: dict[str, Callable[[str], object]] | None = None, target_fixture: str | None = None, stacklevel: int = 1, ) -> Callable[[Callable[P, T]], Callable[P, T]]: diff --git a/src/pytest_bdd/utils.py b/src/pytest_bdd/utils.py index e4f55dbd0..14d07ec6c 100644 --- a/src/pytest_bdd/utils.py +++ b/src/pytest_bdd/utils.py @@ -7,12 +7,10 @@ import re from inspect import getframeinfo, signature from sys import _getframe -from typing import TYPE_CHECKING, TypeVar, cast +from typing import TYPE_CHECKING, Callable, TypeVar, cast, overload from weakref import WeakKeyDictionary if TYPE_CHECKING: - from typing import Any, Callable - from _pytest.config import Config from _pytest.pytester import RunResult @@ -23,7 +21,7 @@ CONFIG_STACK: list[Config] = [] -def get_args(func: Callable[..., Any]) -> list[str]: +def get_args(func: Callable[..., object]) -> list[str]: """Get a list of argument names for a function. :param func: The function to inspect. @@ -37,7 +35,7 @@ def get_args(func: Callable[..., Any]) -> list[str]: ] -def get_caller_module_locals(stacklevel: int = 1) -> dict[str, Any]: +def get_caller_module_locals(stacklevel: int = 1) -> dict[str, object]: """Get the caller module locals dictionary. We use sys._getframe instead of inspect.stack(0) because the latter is way slower, since it iterates over @@ -60,7 +58,7 @@ def get_caller_module_path(depth: int = 2) -> str: _DUMP_END = "<<<_pytest_bdd_" -def dump_obj(*objects: Any) -> None: +def dump_obj(*objects: object) -> None: """Dump objects to stdout so that they can be inspected by the test suite.""" for obj in objects: dump = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) @@ -88,11 +86,17 @@ def setdefault(obj: object, name: str, default: T) -> T: return default -def registry_get_safe(registry: WeakKeyDictionary[K, V], key: K, default: V | None = None) -> V | None: +@overload +def registry_get_safe(registry: WeakKeyDictionary[K, V], key: object, default: T) -> V | T: ... +@overload +def registry_get_safe(registry: WeakKeyDictionary[K, V], key: object, default: None = None) -> V | None: ... + + +def registry_get_safe(registry: WeakKeyDictionary[K, V], key: object, default: T | None = None) -> T | V | None: """Get a value from a registry, or None if the key is not in the registry. It ensures that this works even if the key cannot be weak-referenced (normally this would raise a TypeError). """ try: - return registry.get(key, default) + return registry.get(key, default) # type: ignore[arg-type] except TypeError: return None From ad221becd19c34c58eee9b15ea3247d1c76b520f Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 1 Dec 2024 16:34:23 +0100 Subject: [PATCH 22/25] Fix typing in `reporting.py` and `cucumber_json.py` I managed to remove all occurrences of `Any`, and use proper typed dicts instead --- src/pytest_bdd/cucumber_json.py | 70 +++++++++++++++++---- src/pytest_bdd/gherkin_terminal_reporter.py | 4 +- src/pytest_bdd/parser.py | 4 +- src/pytest_bdd/reporting.py | 61 ++++++++++++++---- 4 files changed, 110 insertions(+), 29 deletions(-) diff --git a/src/pytest_bdd/cucumber_json.py b/src/pytest_bdd/cucumber_json.py index 5d016ec21..a8a4cf49b 100644 --- a/src/pytest_bdd/cucumber_json.py +++ b/src/pytest_bdd/cucumber_json.py @@ -6,11 +6,11 @@ import math import os import time -from typing import TYPE_CHECKING, Any, Literal, TypedDict +from typing import TYPE_CHECKING, Literal, TypedDict from typing_extensions import NotRequired -from .reporting import test_report_context_registry +from .reporting import FeatureDict, ScenarioReportDict, StepReportDict, test_report_context_registry if TYPE_CHECKING: from _pytest.config import Config @@ -19,6 +19,56 @@ from _pytest.terminal import TerminalReporter +class ResultElementDict(TypedDict): + status: Literal["passed", "failed", "skipped"] + duration: int # in nanoseconds + error_message: NotRequired[str] + + +class TagElementDict(TypedDict): + name: str + line: int + + +class MatchElementDict(TypedDict): + location: str + + +class StepElementDict(TypedDict): + keyword: str + name: str + line: int + match: MatchElementDict + result: ResultElementDict + + +class ScenarioElementDict(TypedDict): + keyword: str + id: str + name: str + line: int + description: str + tags: list[TagElementDict] + type: Literal["scenario"] + steps: list[StepElementDict] + + +class FeatureElementDict(TypedDict): + keyword: str + uri: str + name: str + id: str + line: int + description: str + language: str + tags: list[TagElementDict] + elements: list[ScenarioElementDict] + + +class FeaturesDict(TypedDict): + features: dict[str, FeatureElementDict] + + def add_options(parser: Parser) -> None: """Add pytest-bdd options.""" group = parser.getgroup("bdd", "Cucumber JSON") @@ -48,21 +98,15 @@ def unconfigure(config: Config) -> None: config.pluginmanager.unregister(xml) -class Result(TypedDict): - status: Literal["passed", "failed", "skipped"] - duration: int # in nanoseconds - error_message: NotRequired[str] - - class LogBDDCucumberJSON: """Logging plugin for cucumber like json output.""" def __init__(self, logfile: str) -> None: logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) - self.features: dict[str, dict] = {} + self.features: dict[str, FeatureElementDict] = {} - def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> Result: + def _get_result(self, step: StepReportDict, report: TestReport, error_message: bool = False) -> ResultElementDict: """Get scenario test run result. :param step: `Step` step we get result for @@ -80,12 +124,12 @@ def _get_result(self, step: dict[str, Any], report: TestReport, error_message: b status = "skipped" else: raise ValueError(f"Unknown test outcome {report.outcome}") - res: Result = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec + res: ResultElementDict = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec if res_message is not None: res["error_message"] = res_message return res - def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]: + def _serialize_tags(self, item: FeatureDict | ScenarioReportDict) -> list[TagElementDict]: """Serialize item's tags. :param item: json-serialized `Scenario` or `Feature`. @@ -110,7 +154,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: # skip if there isn't a result or scenario has no steps return - def stepmap(step: dict[str, Any]) -> dict[str, Any]: + def stepmap(step: StepReportDict) -> StepElementDict: error_message = False if step["failed"] and not scenario.setdefault("failed", False): scenario["failed"] = True diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index e92808cde..264aea2d9 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -43,10 +43,10 @@ def configure(config: Config) -> None: raise Exception("gherkin-terminal-reporter is not compatible with 'xdist' plugin.") -class GherkinTerminalReporter(TerminalReporter): # type: ignore +class GherkinTerminalReporter(TerminalReporter): # type: ignore[misc] def __init__(self, config: Config) -> None: super().__init__(config) - self.current_rule = None + self.current_rule: str | None = None def pytest_runtest_logreport(self, report: TestReport) -> None: rep = report diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index 4da0d9c16..6bb15e478 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -64,7 +64,7 @@ class Feature: scenarios (OrderedDict[str, ScenarioTemplate]): A dictionary of scenarios in the feature. filename (str): The absolute path of the feature file. rel_filename (str): The relative path of the feature file. - name (Optional[str]): The name of the feature. + name (str): The name of the feature. tags (set[str]): A set of tags associated with the feature. background (Optional[Background]): The background steps for the feature, if any. line_number (int): The line number where the feature starts in the file. @@ -76,7 +76,7 @@ class Feature: rel_filename: str language: str keyword: str - name: str | None + name: str tags: set[str] background: Background | None line_number: int diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index 2706a28e4..fe047b4f9 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -8,12 +8,12 @@ import time from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, TypedDict from weakref import WeakKeyDictionary -if TYPE_CHECKING: - from typing import Any, Callable +from typing_extensions import NotRequired +if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest from _pytest.nodes import Item from _pytest.reports import TestReport @@ -25,6 +25,44 @@ test_report_context_registry: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary() +class FeatureDict(TypedDict): + keyword: str + name: str + filename: str + rel_filename: str + language: str + line_number: int + description: str + tags: list[str] + + +class RuleDict(TypedDict): + keyword: str + name: str + description: str + tags: list[str] + + +class StepReportDict(TypedDict): + name: str + type: str + keyword: str + line_number: int + failed: bool + duration: float + + +class ScenarioReportDict(TypedDict): + steps: list[StepReportDict] + keyword: str + name: str + line_number: int + tags: list[str] + feature: FeatureDict + rule: NotRequired[RuleDict] + failed: NotRequired[bool] + + class StepReport: """Step execution report.""" @@ -39,11 +77,10 @@ def __init__(self, step: Step) -> None: self.step = step self.started = time.perf_counter() - def serialize(self) -> dict[str, object]: + def serialize(self) -> StepReportDict: """Serialize the step execution report. :return: Serialized step execution report. - :rtype: dict """ return { "name": self.step.name, @@ -103,16 +140,15 @@ def add_step_report(self, step_report: StepReport) -> None: """ self.step_reports.append(step_report) - def serialize(self) -> dict[str, object]: + def serialize(self) -> ScenarioReportDict: """Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode. :return: Serialized report. - :rtype: dict """ scenario = self.scenario feature = scenario.feature - serialized = { + serialized: ScenarioReportDict = { "steps": [step_report.serialize() for step_report in self.step_reports], "keyword": scenario.keyword, "name": scenario.name, @@ -131,12 +167,13 @@ def serialize(self) -> dict[str, object]: } if scenario.rule: - serialized["rule"] = { + rule_dict: RuleDict = { "keyword": scenario.rule.keyword, "name": scenario.rule.name, "description": scenario.rule.description, - "tags": scenario.rule.tags, + "tags": sorted(scenario.rule.tags), } + serialized["rule"] = rule_dict return serialized @@ -154,7 +191,7 @@ def fail(self) -> None: @dataclass class ReportContext: - scenario: dict[str, Any] + scenario: ScenarioReportDict name: str @@ -191,7 +228,7 @@ def before_step( feature: Feature, scenario: Scenario, step: Step, - step_func: Callable[..., Any], + step_func: Callable[..., object], ) -> None: """Store step start time.""" scenario_reports_registry[request.node].add_step_report(StepReport(step=step)) From 5e5c60f3b5a4a884aada9e8b548eaacfaea5aedc Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 1 Dec 2024 16:38:30 +0100 Subject: [PATCH 23/25] Remove usages of `Any` from `scenario.py` --- src/pytest_bdd/scenario.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index 7b0113734..e8e842777 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -19,7 +19,7 @@ import re from collections.abc import Iterable, Iterator from inspect import signature -from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast +from typing import TYPE_CHECKING, Callable, TypeVar, cast from weakref import WeakKeyDictionary import pytest @@ -444,15 +444,17 @@ def get_name() -> str: suffix = f"_{index}" -def scenarios(*feature_paths: str, **kwargs: Any) -> None: +def scenarios(*feature_paths: str, encoding: str = "utf-8", features_base_dir: str | None = None) -> None: + caller_locals = get_caller_module_locals() """Parse features from the paths and put all found scenarios in the caller module. :param *feature_paths: feature file paths to use for scenarios + :param str encoding: Feature file encoding. + :param features_base_dir: Optional base dir location for locating feature files. If not set, it will try and + resolve using property set in .ini file, otherwise it is assumed to be relative from the caller path location. """ - 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(caller_path) @@ -474,7 +476,7 @@ def scenarios(*feature_paths: str, **kwargs: Any) -> None: # skip already bound scenarios if (scenario_object.feature.filename, scenario_name) not in module_scenarios: - @scenario(feature.filename, scenario_name, **kwargs) + @scenario(feature.filename, scenario_name, encoding=encoding, features_base_dir=features_base_dir) def _scenario() -> None: pass # pragma: no cover From 0bb8b0c1cbe0d49115d54c55897f02122767766e Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:13:51 +0100 Subject: [PATCH 24/25] Fix `description` typing --- src/pytest_bdd/parser.py | 4 ++-- src/pytest_bdd/reporting.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index 6bb15e478..a1b6eb264 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -166,7 +166,7 @@ class ScenarioTemplate: name: str line_number: int templated: bool - description: str | None = None + description: str tags: set[str] = field(default_factory=set) _steps: list[Step] = field(init=False, default_factory=list) examples: list[Examples] = field(default_factory=list[Examples]) @@ -254,7 +254,7 @@ class Scenario: name: str line_number: int steps: list[Step] - description: str | None = None + description: str tags: set[str] = field(default_factory=set) rule: Rule | None = None diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index f93ce22a7..4d5c626b6 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -59,6 +59,7 @@ class ScenarioReportDict(TypedDict): line_number: int tags: list[str] feature: FeatureDict + description: str rule: NotRequired[RuleDict] failed: NotRequired[bool] From f4413e586ddac614e63bafffd5f4cdde46029418 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:19:26 +0100 Subject: [PATCH 25/25] Update/fix changelog --- CHANGES.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 163428575..8ec90a688 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,9 +20,15 @@ Deprecated Removed +++++++ +* The following private attributes are not available anymore (`#658 `_): + * ``_pytest.reports.TestReport.scenario``; replaced by ``pytest_bdd.reporting.test_report_context`` WeakKeyDictionary (internal use) + * ``__scenario__`` attribute of test functions generated by the ``@scenario`` (and ``@scenarios``) decorator; replaced by ``pytest_bdd.scenario.scenario_wrapper_template_registry`` WeakKeyDictionary (internal use) + * ``_pytest.nodes.Item.__scenario_report__``; replaced by ``pytest_bdd.reporting.scenario_reports_registry`` WeakKeyDictionary (internal use) + * ``_pytest_bdd_step_context`` attribute of internal test function markers; replaced by ``pytest_bdd.steps.step_function_context_registry`` WeakKeyDictionary (internal use) Fixed +++++ +* Made type annotations stronger and removed most of the ``typing.Any`` usages and ``# type: ignore`` annotations. `#658 `_ Security ++++++++ @@ -48,11 +54,6 @@ Fixed +++++ * Fixed an issue with the upcoming pytest release related to the use of ``@pytest.mark.usefixtures`` with an empty list. * Render template variables in docstrings and datatable cells with example table entries, as we already do for steps definitions. -* Address many ``mypy`` warnings. The following private attributes are not available anymore (`#658 `_): - * ``_pytest.reports.TestReport.scenario`` (replaced by ``pytest_bdd.reporting.test_report_context`` WeakKeyDictionary) - * ``__scenario__`` attribute of test functions generated by the ``@scenario`` (and ``@scenarios``) decorator (replaced by ``pytest_bdd.scenario.scenario_wrapper_template_registry`` WeakKeyDictionary) - * ``_pytest.nodes.Item.__scenario_report__`` (replaced by ``pytest_bdd.reporting.scenario_reports_registry`` WeakKeyDictionary) - * ``_pytest_bdd_step_context`` attribute of internal test function markers (replaced by ``pytest_bdd.steps.step_function_context_registry`` WeakKeyDictionary) Security ++++++++