Skip to content

Commit 2f3acbd

Browse files
committed
Response to feedback and make mypy happy.
1 parent 9c12dbf commit 2f3acbd

19 files changed

+1147
-127
lines changed

src/pytest_bdd/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
3434
else:
3535

3636
def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None:
37-
return fixturemanager.getfixturedefs(fixturename, node.nodeid)
37+
return fixturemanager.getfixturedefs(fixturename, node.nodeid) # type: ignore
3838

3939
def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
4040
"""Inject fixture into pytest fixture request.
@@ -44,7 +44,7 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
4444
:param value: argument value
4545
"""
4646
fd = FixtureDef(
47-
fixturemanager=request._fixturemanager,
47+
fixturemanager=request._fixturemanager, # type: ignore
4848
baseid=None,
4949
argname=arg,
5050
func=lambda: value,

src/pytest_bdd/cucumber_json.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ def configure(config: Config) -> None:
3535
cucumber_json_path = config.option.cucumber_json_path
3636
# prevent opening json log on worker nodes (xdist)
3737
if cucumber_json_path and not hasattr(config, "workerinput"):
38-
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
39-
config.pluginmanager.register(config._bddcucumberjson)
38+
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path) # type: ignore[attr-defined]
39+
config.pluginmanager.register(config._bddcucumberjson) # type: ignore[attr-defined]
4040

4141

4242
def unconfigure(config: Config) -> None:
43-
xml = getattr(config, "_bddcucumberjson", None)
43+
xml = getattr(config, "_bddcucumberjson", None) # type: ignore[attr-defined]
4444
if xml is not None:
45-
del config._bddcucumberjson
45+
del config._bddcucumberjson # type: ignore[attr-defined]
4646
config.pluginmanager.unregister(xml)
4747

4848

src/pytest_bdd/exceptions.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ class ScenarioNotFound(ScenarioValidationError):
1515
"""Scenario Not Found."""
1616

1717

18-
class ExamplesNotValidError(ScenarioValidationError):
19-
"""Example table is not valid."""
20-
21-
2218
class StepDefinitionNotFoundError(Exception):
2319
"""Step definition not found."""
2420

@@ -41,7 +37,7 @@ def __init__(self, message, line, line_content, filename):
4137
self.filename = filename
4238

4339
def __str__(self):
44-
return f"{self.__class__.__name__}: {self.message}\nLine number: {self.line}\nLine: {self.line_content}\nFile: {self.filename}"
40+
return f"{self.message}\nLine number: {self.line}\nLine: {self.line_content}\nFile: {self.filename}"
4541

4642

4743
class FeatureError(GherkinParseError):
@@ -52,33 +48,17 @@ class BackgroundError(GherkinParseError):
5248
pass
5349

5450

55-
class ScenarioOutlineError(GherkinParseError):
56-
pass
57-
58-
5951
class ScenarioError(GherkinParseError):
6052
pass
6153

6254

63-
class ExamplesError(GherkinParseError):
64-
pass
65-
66-
6755
class StepError(GherkinParseError):
6856
pass
6957

7058

71-
class TagError(GherkinParseError):
72-
pass
73-
74-
7559
class RuleError(GherkinParseError):
7660
pass
7761

7862

79-
class DocStringError(GherkinParseError):
80-
pass
81-
82-
8363
class TokenError(GherkinParseError):
8464
pass

src/pytest_bdd/feature.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,16 @@ def get_features(paths: list[str], **kwargs) -> list[Feature]:
6565
:return: `list` of `Feature` objects.
6666
"""
6767
seen_names = set()
68-
features = []
68+
_features = []
6969
for path in paths:
7070
if path not in seen_names:
7171
seen_names.add(path)
7272
if os.path.isdir(path):
73-
features.extend(
74-
get_features(glob.iglob(os.path.join(path, "**", "*.feature"), recursive=True), **kwargs)
75-
)
73+
file_paths = list(glob.iglob(os.path.join(path, "**", "*.feature"), recursive=True))
74+
_features.extend(get_features(file_paths, **kwargs))
7675
else:
7776
base, name = os.path.split(path)
7877
feature = get_feature(base, name, **kwargs)
79-
features.append(feature)
80-
features.sort(key=lambda feature: feature.name or feature.filename)
81-
return features
78+
_features.append(feature)
79+
_features.sort(key=lambda _feature: _feature.name or _feature.filename)
80+
return _features

src/pytest_bdd/generation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import TYPE_CHECKING, cast
88

99
from _pytest._io import TerminalWriter
10-
from mako.lookup import TemplateLookup
10+
from mako.lookup import TemplateLookup # type: ignore
1111

1212
from .compat import getfixturedefs
1313
from .feature import get_features
@@ -181,11 +181,11 @@ def _show_missing_code_main(config: Config, session: Session) -> None:
181181
features, scenarios, steps = parse_feature_files(config.option.features)
182182

183183
for item in session.items:
184-
if scenario := getattr(item.obj, "__scenario__", None):
184+
if scenario := getattr(item.obj, "__scenario__", None): # type: ignore
185185
if scenario in scenarios:
186186
scenarios.remove(scenario)
187187
for step in scenario.steps:
188-
if _find_step_fixturedef(fm, item, step=step):
188+
if _find_step_fixturedef(fm, item, step=step): # type: ignore
189189
try:
190190
steps.remove(step)
191191
except ValueError:

src/pytest_bdd/gherkin_parser.py

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55
import textwrap
66
import typing
77
from dataclasses import dataclass, field
8-
from typing import Any, Dict, List, Optional
8+
from typing import Any
99

10-
from gherkin.errors import CompositeParserException
11-
from gherkin.parser import Parser
12-
from gherkin.token_scanner import TokenScanner
10+
from gherkin.errors import CompositeParserException # type: ignore
11+
from gherkin.parser import Parser # type: ignore
1312

1413
from . import exceptions
1514

1615
if typing.TYPE_CHECKING:
17-
from typing import Self
16+
from typing_extensions import Self
1817

1918

2019
ERROR_PATTERNS = [
@@ -33,41 +32,21 @@
3332
exceptions.BackgroundError,
3433
"Multiple 'Background' sections detected. Only one 'Background' is allowed per feature.",
3534
),
36-
(
37-
re.compile(r"expected:.*got 'Scenario Outline.*'"),
38-
exceptions.ScenarioOutlineError,
39-
"'Scenario Outline' requires steps before 'Examples'.",
40-
),
4135
(
4236
re.compile(r"expected:.*got 'Scenario.*'"),
4337
exceptions.ScenarioError,
44-
"Misplaced or incorrect 'Scenario' keyword. Ensure it's correctly placed.",
45-
),
46-
(
47-
re.compile(r"expected:.*got 'Examples.*'"),
48-
exceptions.ExamplesError,
49-
"'Examples' must follow a valid 'Scenario Outline' and contain table rows.",
38+
"Misplaced or incorrect 'Scenario' keyword. Ensure it's correctly placed. There might be a missing Feature section.",
5039
),
5140
(
5241
re.compile(r"expected:.*got 'Given.*'"),
5342
exceptions.StepError,
5443
"Improper step keyword detected. Ensure correct order and indentation for steps (Given, When, Then, etc.).",
5544
),
56-
(
57-
re.compile(r"expected:.*got 'TagLine.*'"),
58-
exceptions.TagError,
59-
"Tags are misplaced. They should be directly above features, scenarios, or outlines.",
60-
),
6145
(
6246
re.compile(r"expected:.*got 'Rule.*'"),
6347
exceptions.RuleError,
6448
"Misplaced or incorrectly formatted 'Rule'. Ensure it follows the feature structure.",
6549
),
66-
(
67-
re.compile(r"expected:.*got 'DocString.*'"),
68-
exceptions.DocStringError,
69-
'DocString must be enclosed in triple quotes ("""). Ensure proper formatting.',
70-
),
7150
(
7251
re.compile(r"expected:.*got '.*'"),
7352
exceptions.TokenError,
@@ -196,7 +175,7 @@ class Scenario:
196175
description: str
197176
steps: list[Step]
198177
tags: list[Tag]
199-
examples: list[DataTable] | None = field(default_factory=list)
178+
examples: list[DataTable] = field(default_factory=list)
200179

201180
@classmethod
202181
def from_dict(cls, data: dict[str, Any]) -> Self:
@@ -208,7 +187,7 @@ def from_dict(cls, data: dict[str, Any]) -> Self:
208187
description=data["description"],
209188
steps=[Step.from_dict(step) for step in data["steps"]],
210189
tags=[Tag.from_dict(tag) for tag in data["tags"]],
211-
examples=[DataTable.from_dict(example) for example in data.get("examples", [])],
190+
examples=[DataTable.from_dict(example) for example in data["examples"]],
212191
)
213192

214193

@@ -309,37 +288,38 @@ def _to_raw_string(normal_string: str) -> str:
309288
return normal_string.replace("\\", "\\\\")
310289

311290

312-
def get_gherkin_document(abs_filename: str = None, encoding: str = "utf-8") -> GherkinDocument:
291+
def get_gherkin_document(abs_filename: str, encoding: str = "utf-8") -> GherkinDocument:
313292
with open(abs_filename, encoding=encoding) as f:
314293
feature_file_text = f.read()
315294

316295
try:
317-
gherkin_data = Parser().parse(TokenScanner(feature_file_text))
296+
gherkin_data = Parser().parse(feature_file_text)
318297
except CompositeParserException as e:
319298
message = e.args[0]
320299
line = e.errors[0].location["line"]
321300
line_content = linecache.getline(abs_filename, e.errors[0].location["line"]).rstrip("\n")
322301
filename = abs_filename
323-
gherkin_error_handler = GherkinParserErrorHandler()
324-
gherkin_error_handler(message, line, line_content, filename)
302+
handle_gherkin_parser_error(message, line, line_content, filename, e)
325303
# If no patterns matched, raise a generic GherkinParserError
326-
raise exceptions.GherkinParseError(f"Unknown parsing error: {message}", line, line_content, filename)
304+
raise exceptions.GherkinParseError(f"Unknown parsing error: {message}", line, line_content, filename) from e
327305

328306
# At this point, the `gherkin_data` should be valid if no exception was raised
329307
return GherkinDocument.from_dict(gherkin_data)
330308

331309

332-
class GherkinParserErrorHandler:
333-
"""Parses raw Gherkin parser errors and converts them to human-readable exceptions."""
334-
335-
def __call__(self, raw_error: str, line: int, line_content: str, filename: str):
336-
"""Map the error message to a specific exception type and raise it."""
337-
# Split the raw_error into individual lines
338-
error_lines = raw_error.splitlines()
339-
340-
# Check each line against all error patterns
341-
for error_line in error_lines:
342-
for pattern, exception_class, message in ERROR_PATTERNS:
343-
if pattern.search(error_line):
344-
# If a match is found, raise the corresponding exception with the formatted message
310+
def handle_gherkin_parser_error(
311+
raw_error: str, line: int, line_content: str, filename: str, original_exception: Exception | None = None
312+
):
313+
"""Map the error message to a specific exception type and raise it."""
314+
# Split the raw_error into individual lines
315+
error_lines = raw_error.splitlines()
316+
317+
# Check each line against all error patterns
318+
for error_line in error_lines:
319+
for pattern, exception_class, message in ERROR_PATTERNS:
320+
if pattern.search(error_line):
321+
# If a match is found, raise the corresponding exception with the formatted message
322+
if original_exception:
323+
raise exception_class(message, line, line_content, filename) from original_exception
324+
else:
345325
raise exception_class(message, line, line_content, filename)

src/pytest_bdd/gherkin_terminal_reporter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def configure(config: Config) -> None:
4343
raise Exception("gherkin-terminal-reporter is not compatible with 'xdist' plugin.")
4444

4545

46-
class GherkinTerminalReporter(TerminalReporter):
46+
class GherkinTerminalReporter(TerminalReporter): # type: ignore
4747
def __init__(self, config: Config) -> None:
4848
super().__init__(config)
4949

src/pytest_bdd/parser.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass, field
88
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
99

10+
from .exceptions import StepError
1011
from .gherkin_parser import Background as GherkinBackground
1112
from .gherkin_parser import Feature as GherkinFeature
1213
from .gherkin_parser import GherkinDocument
@@ -347,8 +348,7 @@ def get_step_type(keyword: str) -> str | None:
347348
"then": THEN,
348349
}.get(keyword)
349350

350-
@staticmethod
351-
def parse_steps(steps_data: list[GherkinStep]) -> list[Step]:
351+
def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
352352
"""Parse a list of step data into Step objects.
353353
354354
Args:
@@ -357,22 +357,39 @@ def parse_steps(steps_data: list[GherkinStep]) -> list[Step]:
357357
Returns:
358358
List[Step]: A list of Step objects.
359359
"""
360+
361+
def get_step_content(_gherkin_step):
362+
step_name = strip_comments(_gherkin_step.text)
363+
if _gherkin_step.docString:
364+
step_name = f"{step_name}\n{_gherkin_step.docString.content}"
365+
return step_name
366+
367+
if not steps_data:
368+
return []
369+
370+
first_step = steps_data[0]
371+
if first_step.keyword.lower() not in STEP_TYPES:
372+
raise StepError(
373+
message=f"First step in a scenario or background must start with 'Given', 'When' or 'Then', but got {first_step.keyword}.",
374+
line=first_step.location.line,
375+
line_content=get_step_content(first_step),
376+
filename=self.abs_filename,
377+
)
378+
360379
steps = []
361-
current_type = None
362-
for step_data in steps_data:
363-
name = strip_comments(step_data.text)
364-
if step_data.docString:
365-
name = f"{name}\n{step_data.docString.content}"
366-
keyword = step_data.keyword.lower()
380+
current_type = first_step.keyword.lower()
381+
for step in steps_data:
382+
name = get_step_content(step)
383+
keyword = step.keyword.lower()
367384
if keyword in STEP_TYPES:
368385
current_type = keyword
369386
steps.append(
370387
Step(
371388
name=name,
372389
type=current_type,
373-
indent=step_data.location.column - 1,
374-
line_number=step_data.location.line,
375-
keyword=step_data.keyword.title(),
390+
indent=step.location.column - 1,
391+
line_number=step.location.line,
392+
keyword=step.keyword.title(),
376393
)
377394
)
378395
return steps
@@ -404,12 +421,14 @@ def parse_scenario(self, scenario_data: GherkinScenario, feature: Feature) -> Sc
404421
line_number=example_data.location.line,
405422
name=example_data.name,
406423
)
407-
param_names = [cell.value for cell in example_data.tableHeader.cells]
408-
examples.set_param_names(param_names)
409-
for row in example_data.tableBody:
410-
values = [cell.value or "" for cell in row.cells]
411-
examples.add_example(values)
412-
scenario.examples = examples
424+
if example_data.tableHeader is not None:
425+
param_names = [cell.value for cell in example_data.tableHeader.cells]
426+
examples.set_param_names(param_names)
427+
if example_data.tableBody is not None:
428+
for row in example_data.tableBody:
429+
values = [cell.value or "" for cell in row.cells]
430+
examples.add_example(values)
431+
scenario.examples = examples
413432

414433
return scenario
415434

@@ -431,7 +450,7 @@ def _parse_feature_file(self) -> GherkinDocument:
431450
"""
432451
return get_gherkin_document(self.abs_filename, self.encoding)
433452

434-
def parse(self):
453+
def parse(self) -> Feature:
435454
gherkin_doc: GherkinDocument = self._parse_feature_file()
436455
feature_data: GherkinFeature = gherkin_doc.feature
437456
feature = Feature(

0 commit comments

Comments
 (0)