First commit for setting default parser

This commit is contained in:
Jason Allen 2024-11-28 09:55:04 +00:00
parent c4f44dc3b3
commit e41d8b9a6f
4 changed files with 101 additions and 23 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)