From e41d8b9a6fba6f5ca36df6d54a971096d905e6ad Mon Sep 17 00:00:00 2001 From: Jason Allen Date: Thu, 28 Nov 2024 09:55:04 +0000 Subject: [PATCH] First commit for setting default parser --- src/pytest_bdd/parsers.py | 34 +++++++++-------- src/pytest_bdd/scenario.py | 5 ++- src/pytest_bdd/steps.py | 22 ++++++++--- tests/args/test_default_parser.py | 63 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 tests/args/test_default_parser.py diff --git a/src/pytest_bdd/parsers.py b/src/pytest_bdd/parsers.py index 62f1677..828db26 100644 --- a/src/pytest_bdd/parsers.py +++ b/src/pytest_bdd/parsers.py @@ -17,12 +17,18 @@ if TYPE_CHECKING: def add_options(parser: PytestArgParser) -> None: """Add pytest-bdd options.""" group = parser.getgroup("bdd") - group.addoption( + group._addoption( "--bdd-default-parser", + dest="bdd_default_parser", action="store", default=None, help="Set the default step parser type (e.g. string, parse, re, cfparse).", ) + parser._addini( + "bdd_default_parser", + help="Default step parser type (e.g. string, parse, re, cfparse) for pytest-bdd.", + default=None, + ) def configure(config: Config): @@ -129,19 +135,17 @@ class string(StepParser): TStepParser = TypeVar("TStepParser", bound=StepParser) -def get_parser(step_name: str | StepParser, config: Config | None = None) -> StepParser: - """Get parser by given name.""" +def get_parser(step_name: str | StepParser, config: Config) -> StepParser: if isinstance(step_name, StepParser): return step_name - - default_parser = getattr(config, "_bdd_default_parser", "string") if config else "string" - - parser_classes = { - "string": string, - "parse": parse, - "re": re, - "cfparse": cfparse, - } - - parser_cls = parser_classes.get(default_parser, string) - return parser_cls(step_name) + if config: + default_parser = getattr(config, "_bdd_default_parser", "string") + parser_classes = { + "string": string, + "parse": parse, + "re": re, + "cfparse": cfparse, + } + parser_cls = parser_classes.get(default_parser, string) + return parser_cls(step_name) + return string(step_name) diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index ac8844c..74663f8 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -24,7 +24,7 @@ import pytest from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func from typing_extensions import ParamSpec -from . import exceptions +from . import exceptions, steps from .compat import getfixturedefs, inject_fixture from .feature import get_feature, get_features from .steps import StepFunctionContext, get_step_fixture_name @@ -52,13 +52,14 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items()) for fixturename, fixturedefs in fixture_def_by_name: for _, fixturedef in enumerate(fixturedefs): - step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None) + step_func_context: steps.StepFunctionContext = getattr(fixturedef.func, "_pytest_bdd_step_context", None) if step_func_context is None: continue if step_func_context.type is not None and step_func_context.type != step.type: continue + steps.register_step_context(step_func_context, node.config) match = step_func_context.parser.is_matching(step.name) if not match: continue diff --git a/src/pytest_bdd/steps.py b/src/pytest_bdd/steps.py index 0f3899a..dfc4006 100644 --- a/src/pytest_bdd/steps.py +++ b/src/pytest_bdd/steps.py @@ -41,7 +41,7 @@ import enum from collections.abc import Iterable from dataclasses import dataclass, field from itertools import count -from typing import Any, Callable, Literal, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar import pytest from typing_extensions import ParamSpec @@ -51,6 +51,10 @@ from .parsers import StepParser, get_parser from .types import GIVEN, THEN, WHEN from .utils import get_caller_module_locals +if TYPE_CHECKING: + from _pytest.config import Config + + P = ParamSpec("P") T = TypeVar("T") @@ -63,11 +67,12 @@ class StepNamePrefix(enum.Enum): @dataclass class StepFunctionContext: + name: str type: Literal["given", "when", "then"] | None step_func: Callable[..., Any] - parser: StepParser converters: dict[str, Callable[[str], Any]] = field(default_factory=dict) target_fixture: str | None = None + parser: StepParser | None = None def get_step_fixture_name(step: Step) -> str: @@ -159,12 +164,10 @@ def step( converters = {} def decorator(func: Callable[P, T]) -> Callable[P, T]: - parser = get_parser(name) - context = StepFunctionContext( + name=name, type=type_, step_func=func, - parser=parser, converters=converters, target_fixture=target_fixture, ) @@ -175,8 +178,9 @@ def step( step_function_marker._pytest_bdd_step_context = context # type: ignore caller_locals = get_caller_module_locals(stacklevel=stacklevel) + step_name = name.name if isinstance(name, StepParser) else name fixture_step_name = find_unique_name( - f"{StepNamePrefix.step_def.value}_{type_ or '*'}_{parser.name}", seen=caller_locals.keys() + f"{StepNamePrefix.step_def.value}_{type_ or '*'}_{step_name}", seen=caller_locals.keys() ) caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker) return func @@ -205,3 +209,9 @@ def find_unique_name(name: str, seen: Iterable[str]) -> str: # This line will never be reached, but it's here to satisfy mypy raise RuntimeError("Unable to find a unique name") + + +def register_step_context(step_context: StepFunctionContext, config: Config): + """Ensure step context has a parser set early in the lifecycle.""" + if step_context.parser is None: + step_context.parser = get_parser(step_context.name, config) diff --git a/tests/args/test_default_parser.py b/tests/args/test_default_parser.py new file mode 100644 index 0000000..587196e --- /dev/null +++ b/tests/args/test_default_parser.py @@ -0,0 +1,63 @@ +import textwrap + + +def test_tags_selector(pytester): + """Test tests selection by tags.""" + pytester.makefile( + ".ini", + pytest=textwrap.dedent( + """ + [pytest] + bdd_default_parser = string + """ + ), + ) + pytester.makefile( + ".feature", + parser=textwrap.dedent( + """\ + Feature: Step arguments + Scenario: Every step takes a parameter with the same name + Given I have 1 Euro + When I pay 2 Euro + And I pay 1 Euro + Then I should have 0 Euro + And I should have 999999 Euro + + """ + ), + ) + + pytester.makepyfile( + textwrap.dedent( + """\ + import pytest + from pytest_bdd import parsers, given, when, then, scenarios + + scenarios("parser.feature") + + + @pytest.fixture + def values(): + return [1, 2, 1, 0, 999999] + + + @given("I have {euro:d} Euro") + def _(euro, values): + assert euro == values.pop(0) + + + @when("I pay {euro:d} Euro") + def _(euro, values, request): + assert euro == values.pop(0) + + + @then("I should have {euro:d} Euro") + def _(euro, values): + assert euro == values.pop(0) + + """ + ) + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1)