Skip to content

Commit ad221be

Browse files
committed
Fix typing in reporting.py and cucumber_json.py
I managed to remove all occurrences of `Any`, and use proper typed dicts instead
1 parent 4ccb683 commit ad221be

File tree

4 files changed

+110
-29
lines changed

4 files changed

+110
-29
lines changed

src/pytest_bdd/cucumber_json.py

+57-13
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import math
77
import os
88
import time
9-
from typing import TYPE_CHECKING, Any, Literal, TypedDict
9+
from typing import TYPE_CHECKING, Literal, TypedDict
1010

1111
from typing_extensions import NotRequired
1212

13-
from .reporting import test_report_context_registry
13+
from .reporting import FeatureDict, ScenarioReportDict, StepReportDict, test_report_context_registry
1414

1515
if TYPE_CHECKING:
1616
from _pytest.config import Config
@@ -19,6 +19,56 @@
1919
from _pytest.terminal import TerminalReporter
2020

2121

22+
class ResultElementDict(TypedDict):
23+
status: Literal["passed", "failed", "skipped"]
24+
duration: int # in nanoseconds
25+
error_message: NotRequired[str]
26+
27+
28+
class TagElementDict(TypedDict):
29+
name: str
30+
line: int
31+
32+
33+
class MatchElementDict(TypedDict):
34+
location: str
35+
36+
37+
class StepElementDict(TypedDict):
38+
keyword: str
39+
name: str
40+
line: int
41+
match: MatchElementDict
42+
result: ResultElementDict
43+
44+
45+
class ScenarioElementDict(TypedDict):
46+
keyword: str
47+
id: str
48+
name: str
49+
line: int
50+
description: str
51+
tags: list[TagElementDict]
52+
type: Literal["scenario"]
53+
steps: list[StepElementDict]
54+
55+
56+
class FeatureElementDict(TypedDict):
57+
keyword: str
58+
uri: str
59+
name: str
60+
id: str
61+
line: int
62+
description: str
63+
language: str
64+
tags: list[TagElementDict]
65+
elements: list[ScenarioElementDict]
66+
67+
68+
class FeaturesDict(TypedDict):
69+
features: dict[str, FeatureElementDict]
70+
71+
2272
def add_options(parser: Parser) -> None:
2373
"""Add pytest-bdd options."""
2474
group = parser.getgroup("bdd", "Cucumber JSON")
@@ -48,21 +98,15 @@ def unconfigure(config: Config) -> None:
4898
config.pluginmanager.unregister(xml)
4999

50100

51-
class Result(TypedDict):
52-
status: Literal["passed", "failed", "skipped"]
53-
duration: int # in nanoseconds
54-
error_message: NotRequired[str]
55-
56-
57101
class LogBDDCucumberJSON:
58102
"""Logging plugin for cucumber like json output."""
59103

60104
def __init__(self, logfile: str) -> None:
61105
logfile = os.path.expanduser(os.path.expandvars(logfile))
62106
self.logfile = os.path.normpath(os.path.abspath(logfile))
63-
self.features: dict[str, dict] = {}
107+
self.features: dict[str, FeatureElementDict] = {}
64108

65-
def _get_result(self, step: dict[str, Any], report: TestReport, error_message: bool = False) -> Result:
109+
def _get_result(self, step: StepReportDict, report: TestReport, error_message: bool = False) -> ResultElementDict:
66110
"""Get scenario test run result.
67111
68112
: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
80124
status = "skipped"
81125
else:
82126
raise ValueError(f"Unknown test outcome {report.outcome}")
83-
res: Result = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec
127+
res: ResultElementDict = {"status": status, "duration": int(math.floor((10**9) * step["duration"]))} # nanosec
84128
if res_message is not None:
85129
res["error_message"] = res_message
86130
return res
87131

88-
def _serialize_tags(self, item: dict[str, Any]) -> list[dict[str, Any]]:
132+
def _serialize_tags(self, item: FeatureDict | ScenarioReportDict) -> list[TagElementDict]:
89133
"""Serialize item's tags.
90134
91135
:param item: json-serialized `Scenario` or `Feature`.
@@ -110,7 +154,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
110154
# skip if there isn't a result or scenario has no steps
111155
return
112156

113-
def stepmap(step: dict[str, Any]) -> dict[str, Any]:
157+
def stepmap(step: StepReportDict) -> StepElementDict:
114158
error_message = False
115159
if step["failed"] and not scenario.setdefault("failed", False):
116160
scenario["failed"] = True

src/pytest_bdd/gherkin_terminal_reporter.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ def configure(config: Config) -> None:
4343
raise Exception("gherkin-terminal-reporter is not compatible with 'xdist' plugin.")
4444

4545

46-
class GherkinTerminalReporter(TerminalReporter): # type: ignore
46+
class GherkinTerminalReporter(TerminalReporter): # type: ignore[misc]
4747
def __init__(self, config: Config) -> None:
4848
super().__init__(config)
49-
self.current_rule = None
49+
self.current_rule: str | None = None
5050

5151
def pytest_runtest_logreport(self, report: TestReport) -> None:
5252
rep = report

src/pytest_bdd/parser.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class Feature:
6464
scenarios (OrderedDict[str, ScenarioTemplate]): A dictionary of scenarios in the feature.
6565
filename (str): The absolute path of the feature file.
6666
rel_filename (str): The relative path of the feature file.
67-
name (Optional[str]): The name of the feature.
67+
name (str): The name of the feature.
6868
tags (set[str]): A set of tags associated with the feature.
6969
background (Optional[Background]): The background steps for the feature, if any.
7070
line_number (int): The line number where the feature starts in the file.
@@ -76,7 +76,7 @@ class Feature:
7676
rel_filename: str
7777
language: str
7878
keyword: str
79-
name: str | None
79+
name: str
8080
tags: set[str]
8181
background: Background | None
8282
line_number: int

src/pytest_bdd/reporting.py

+49-12
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
import time
1010
from dataclasses import dataclass
11-
from typing import TYPE_CHECKING
11+
from typing import TYPE_CHECKING, Callable, TypedDict
1212
from weakref import WeakKeyDictionary
1313

14-
if TYPE_CHECKING:
15-
from typing import Any, Callable
14+
from typing_extensions import NotRequired
1615

16+
if TYPE_CHECKING:
1717
from _pytest.fixtures import FixtureRequest
1818
from _pytest.nodes import Item
1919
from _pytest.reports import TestReport
@@ -25,6 +25,44 @@
2525
test_report_context_registry: WeakKeyDictionary[TestReport, ReportContext] = WeakKeyDictionary()
2626

2727

28+
class FeatureDict(TypedDict):
29+
keyword: str
30+
name: str
31+
filename: str
32+
rel_filename: str
33+
language: str
34+
line_number: int
35+
description: str
36+
tags: list[str]
37+
38+
39+
class RuleDict(TypedDict):
40+
keyword: str
41+
name: str
42+
description: str
43+
tags: list[str]
44+
45+
46+
class StepReportDict(TypedDict):
47+
name: str
48+
type: str
49+
keyword: str
50+
line_number: int
51+
failed: bool
52+
duration: float
53+
54+
55+
class ScenarioReportDict(TypedDict):
56+
steps: list[StepReportDict]
57+
keyword: str
58+
name: str
59+
line_number: int
60+
tags: list[str]
61+
feature: FeatureDict
62+
rule: NotRequired[RuleDict]
63+
failed: NotRequired[bool]
64+
65+
2866
class StepReport:
2967
"""Step execution report."""
3068

@@ -39,11 +77,10 @@ def __init__(self, step: Step) -> None:
3977
self.step = step
4078
self.started = time.perf_counter()
4179

42-
def serialize(self) -> dict[str, object]:
80+
def serialize(self) -> StepReportDict:
4381
"""Serialize the step execution report.
4482
4583
:return: Serialized step execution report.
46-
:rtype: dict
4784
"""
4885
return {
4986
"name": self.step.name,
@@ -103,16 +140,15 @@ def add_step_report(self, step_report: StepReport) -> None:
103140
"""
104141
self.step_reports.append(step_report)
105142

106-
def serialize(self) -> dict[str, object]:
143+
def serialize(self) -> ScenarioReportDict:
107144
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
108145
109146
:return: Serialized report.
110-
:rtype: dict
111147
"""
112148
scenario = self.scenario
113149
feature = scenario.feature
114150

115-
serialized = {
151+
serialized: ScenarioReportDict = {
116152
"steps": [step_report.serialize() for step_report in self.step_reports],
117153
"keyword": scenario.keyword,
118154
"name": scenario.name,
@@ -131,12 +167,13 @@ def serialize(self) -> dict[str, object]:
131167
}
132168

133169
if scenario.rule:
134-
serialized["rule"] = {
170+
rule_dict: RuleDict = {
135171
"keyword": scenario.rule.keyword,
136172
"name": scenario.rule.name,
137173
"description": scenario.rule.description,
138-
"tags": scenario.rule.tags,
174+
"tags": sorted(scenario.rule.tags),
139175
}
176+
serialized["rule"] = rule_dict
140177

141178
return serialized
142179

@@ -154,7 +191,7 @@ def fail(self) -> None:
154191

155192
@dataclass
156193
class ReportContext:
157-
scenario: dict[str, Any]
194+
scenario: ScenarioReportDict
158195
name: str
159196

160197

@@ -191,7 +228,7 @@ def before_step(
191228
feature: Feature,
192229
scenario: Scenario,
193230
step: Step,
194-
step_func: Callable[..., Any],
231+
step_func: Callable[..., object],
195232
) -> None:
196233
"""Store step start time."""
197234
scenario_reports_registry[request.node].add_step_report(StepReport(step=step))

0 commit comments

Comments
 (0)