Skip to content

Commit 65c06e4

Browse files
committed
Improve docstrings in parser.py
1 parent 1a2baff commit 65c06e4

File tree

1 file changed

+204
-2
lines changed

1 file changed

+204
-2
lines changed

src/pytest_bdd/parser.py

Lines changed: 204 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,33 @@
2020

2121

2222
def strip_comments(line: str) -> str:
23-
"""Remove comments from a line of text."""
23+
"""Remove comments from a line of text.
24+
25+
Args:
26+
line (str): The line of text from which to remove comments.
27+
28+
Returns:
29+
str: The line of text without comments, with leading and trailing whitespace removed.
30+
"""
2431
if res := COMMENT_RE.search(line):
2532
line = line[: res.start()]
2633
return line.strip()
2734

2835

2936
def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Feature:
30-
"""Parse a feature file into a Feature object."""
37+
"""Parse a feature file into a Feature object.
38+
39+
Args:
40+
basedir (str): The base directory of the feature file.
41+
filename (str): The name of the feature file.
42+
encoding (str): The encoding of the feature file (default is "utf-8").
43+
44+
Returns:
45+
Feature: A Feature object representing the parsed feature file.
46+
47+
Raises:
48+
FeatureError: If there is an error parsing the feature file.
49+
"""
3150
abs_filename = os.path.abspath(os.path.join(basedir, filename))
3251
rel_filename = os.path.join(os.path.basename(basedir), filename)
3352
with open(abs_filename, encoding=encoding) as f:
@@ -46,6 +65,19 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
4665

4766
@dataclass(eq=False)
4867
class Feature:
68+
"""Represents a feature parsed from a feature file.
69+
70+
Attributes:
71+
scenarios (OrderedDict[str, ScenarioTemplate]): A dictionary of scenarios in the feature.
72+
filename (str): The absolute path of the feature file.
73+
rel_filename (str): The relative path of the feature file.
74+
name (Optional[str]): The name of the feature.
75+
tags (set[str]): A set of tags associated with the feature.
76+
background (Optional[Background]): The background steps for the feature, if any.
77+
line_number (int): The line number where the feature starts in the file.
78+
description (str): The description of the feature.
79+
"""
80+
4981
scenarios: OrderedDict[str, ScenarioTemplate]
5082
filename: str
5183
rel_filename: str
@@ -58,28 +90,70 @@ class Feature:
5890

5991
@dataclass(eq=False)
6092
class Examples:
93+
"""Represents examples used in scenarios for parameterization.
94+
95+
Attributes:
96+
line_number (Optional[int]): The line number where the examples start.
97+
name (Optional[str]): The name of the examples.
98+
example_params (List[str]): The names of the parameters for the examples.
99+
examples (List[Sequence[str]]): The list of example rows.
100+
"""
101+
61102
line_number: int | None = None
62103
name: str | None = None
63104
example_params: list[str] = field(default_factory=list)
64105
examples: list[Sequence[str]] = field(default_factory=list)
65106

66107
def set_param_names(self, keys: Iterable[str]) -> None:
108+
"""Set the parameter names for the examples.
109+
110+
Args:
111+
keys (Iterable[str]): The parameter names to set.
112+
"""
67113
self.example_params = [str(key) for key in keys]
68114

69115
def add_example(self, values: Sequence[str]) -> None:
116+
"""Add a new example row.
117+
118+
Args:
119+
values (Sequence[str]): The values for the example row.
120+
"""
70121
self.examples.append([str(value) if value is not None else "" for value in values])
71122

72123
def as_contexts(self) -> Iterable[dict[str, Any]]:
124+
"""Generate contexts for the examples.
125+
126+
Yields:
127+
Dict[str, Any]: A dictionary mapping parameter names to their values for each example row.
128+
"""
73129
for row in self.examples:
74130
assert len(self.example_params) == len(row)
75131
yield dict(zip(self.example_params, row))
76132

77133
def __bool__(self) -> bool:
134+
"""Check if there are any examples.
135+
136+
Returns:
137+
bool: True if there are examples, False otherwise.
138+
"""
78139
return bool(self.examples)
79140

80141

81142
@dataclass(eq=False)
82143
class ScenarioTemplate:
144+
"""Represents a scenario template within a feature.
145+
146+
Attributes:
147+
feature (Feature): The feature to which this scenario belongs.
148+
name (str): The name of the scenario.
149+
line_number (int): The line number where the scenario starts in the file.
150+
templated (bool): Whether the scenario is templated.
151+
description (Optional[str]): The description of the scenario.
152+
tags (set[str]): A set of tags associated with the scenario.
153+
_steps (List[Step]): The list of steps in the scenario (internal use only).
154+
examples (Optional[Examples]): The examples used for parameterization in the scenario.
155+
"""
156+
83157
feature: Feature
84158
name: str
85159
line_number: int
@@ -90,14 +164,32 @@ class ScenarioTemplate:
90164
examples: Examples | None = field(default_factory=Examples)
91165

92166
def add_step(self, step: Step) -> None:
167+
"""Add a step to the scenario.
168+
169+
Args:
170+
step (Step): The step to add.
171+
"""
93172
step.scenario = self
94173
self._steps.append(step)
95174

96175
@property
97176
def steps(self) -> list[Step]:
177+
"""Get all steps for the scenario, including background steps.
178+
179+
Returns:
180+
List[Step]: A list of steps, including any background steps from the feature.
181+
"""
98182
return (self.feature.background.steps if self.feature.background else []) + self._steps
99183

100184
def render(self, context: Mapping[str, Any]) -> Scenario:
185+
"""Render the scenario with the given context.
186+
187+
Args:
188+
context (Mapping[str, Any]): The context for rendering steps.
189+
190+
Returns:
191+
Scenario: A Scenario object with steps rendered based on the context.
192+
"""
101193
background_steps = self.feature.background.steps if self.feature.background else []
102194
scenario_steps = [
103195
Step(
@@ -122,6 +214,17 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
122214

123215
@dataclass(eq=False)
124216
class Scenario:
217+
"""Represents a scenario with steps.
218+
219+
Attributes:
220+
feature (Feature): The feature to which this scenario belongs.
221+
name (str): The name of the scenario.
222+
line_number (int): The line number where the scenario starts in the file.
223+
steps (List[Step]): The list of steps in the scenario.
224+
description (Optional[str]): The description of the scenario.
225+
tags (set[str]): A set of tags associated with the scenario.
226+
"""
227+
125228
feature: Feature
126229
name: str
127230
line_number: int
@@ -132,6 +235,20 @@ class Scenario:
132235

133236
@dataclass(eq=False)
134237
class Step:
238+
"""Represents a step within a scenario or background.
239+
240+
Attributes:
241+
type (str): The type of step (e.g., 'given', 'when', 'then').
242+
_name (str): The name of the step.
243+
line_number (int): The line number where the step starts in the file.
244+
indent (int): The indentation level of the step.
245+
keyword (str): The keyword used for the step (e.g., 'Given', 'When', 'Then').
246+
failed (bool): Whether the step has failed (internal use only).
247+
scenario (Optional[ScenarioTemplate]): The scenario to which this step belongs (internal use only).
248+
background (Optional[Background]): The background to which this step belongs (internal use only).
249+
lines (List[str]): Additional lines for the step (internal use only).
250+
"""
251+
135252
type: str
136253
_name: str
137254
line_number: int
@@ -143,20 +260,48 @@ class Step:
143260
lines: list[str] = field(init=False, default_factory=list)
144261

145262
def __init__(self, name: str, type: str, indent: int, line_number: int, keyword: str) -> None:
263+
"""Initialize a step.
264+
265+
Args:
266+
name (str): The name of the step.
267+
type (str): The type of the step (e.g., 'given', 'when', 'then').
268+
indent (int): The indentation level of the step.
269+
line_number (int): The line number where the step starts in the file.
270+
keyword (str): The keyword used for the step (e.g., 'Given', 'When', 'Then').
271+
"""
146272
self.name = name
147273
self.type = type
148274
self.indent = indent
149275
self.line_number = line_number
150276
self.keyword = keyword
151277

152278
def __str__(self) -> str:
279+
"""Return a string representation of the step.
280+
281+
Returns:
282+
str: A string representation of the step.
283+
"""
153284
return f'{self.type.capitalize()} "{self.name}"'
154285

155286
@property
156287
def params(self) -> tuple[str, ...]:
288+
"""Get the parameters in the step name.
289+
290+
Returns:
291+
Tuple[str, ...]: A tuple of parameter names found in the step name.
292+
"""
157293
return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
158294

159295
def render(self, context: Mapping[str, Any]) -> str:
296+
"""Render the step name with the given context.
297+
298+
Args:
299+
context (Mapping[str, Any]): The context for rendering the step name.
300+
301+
Returns:
302+
str: The rendered step name with parameters replaced by their values from the context.
303+
"""
304+
160305
def replacer(m: re.Match) -> str:
161306
varname = m.group(1)
162307
return str(context.get(varname, f"<missing:{varname}>"))
@@ -166,27 +311,75 @@ def replacer(m: re.Match) -> str:
166311

167312
@dataclass(eq=False)
168313
class Background:
314+
"""Represents the background steps for a feature.
315+
316+
Attributes:
317+
feature (Feature): The feature to which this background belongs.
318+
line_number (int): The line number where the background starts in the file.
319+
steps (List[Step]): The list of steps in the background.
320+
"""
321+
169322
feature: Feature
170323
line_number: int
171324
steps: list[Step] = field(init=False, default_factory=list)
172325

173326
def add_step(self, step: Step) -> None:
327+
"""Add a step to the background.
328+
329+
Args:
330+
step (Step): The step to add.
331+
"""
174332
step.background = self
175333
self.steps.append(step)
176334

177335

178336
def dict_to_feature(abs_filename: str, rel_filename: str, data: dict) -> Feature:
337+
"""Convert a dictionary representation of a feature into a Feature object.
338+
339+
Args:
340+
abs_filename (str): The absolute path of the feature file.
341+
rel_filename (str): The relative path of the feature file.
342+
data (dict): The dictionary containing the feature data.
343+
344+
Returns:
345+
Feature: A Feature object representing the parsed feature data.
346+
"""
347+
179348
def get_tag_names(tag_data: list[dict]) -> set[str]:
349+
"""Extract tag names from tag data.
350+
351+
Args:
352+
tag_data (List[dict]): The tag data to extract names from.
353+
354+
Returns:
355+
set[str]: A set of tag names.
356+
"""
180357
return {tag["name"].lstrip("@") for tag in tag_data}
181358

182359
def get_step_type(keyword: str) -> str | None:
360+
"""Map a step keyword to its corresponding type.
361+
362+
Args:
363+
keyword (str): The keyword for the step (e.g., 'given', 'when', 'then').
364+
365+
Returns:
366+
str | None: The type of the step, or None if the keyword is unknown.
367+
"""
183368
return {
184369
"given": GIVEN,
185370
"when": WHEN,
186371
"then": THEN,
187372
}.get(keyword)
188373

189374
def parse_steps(steps_data: list[dict]) -> list[Step]:
375+
"""Parse a list of step data into Step objects.
376+
377+
Args:
378+
steps_data (List[dict]): The list of step data.
379+
380+
Returns:
381+
List[Step]: A list of Step objects.
382+
"""
190383
steps = []
191384
current_step_type = None
192385
for step_data in steps_data:
@@ -208,6 +401,15 @@ def parse_steps(steps_data: list[dict]) -> list[Step]:
208401
return steps
209402

210403
def parse_scenario(scenario_data: dict, feature: Feature) -> ScenarioTemplate:
404+
"""Parse a scenario data dictionary into a ScenarioTemplate object.
405+
406+
Args:
407+
scenario_data (dict): The dictionary containing scenario data.
408+
feature (Feature): The feature to which this scenario belongs.
409+
410+
Returns:
411+
ScenarioTemplate: A ScenarioTemplate object representing the parsed scenario.
412+
"""
211413
scenario = ScenarioTemplate(
212414
feature=feature,
213415
name=strip_comments(scenario_data["name"]),

0 commit comments

Comments
 (0)