4
4
import re
5
5
import textwrap
6
6
from collections import OrderedDict
7
- from collections .abc import Iterable , Mapping , Sequence
7
+ from collections .abc import Generator , Iterable , Mapping , Sequence
8
8
from dataclasses import dataclass , field
9
9
from typing import Any
10
10
13
13
from .gherkin_parser import DataTable
14
14
from .gherkin_parser import Feature as GherkinFeature
15
15
from .gherkin_parser import GherkinDocument
16
+ from .gherkin_parser import Rule as GherkinRule
16
17
from .gherkin_parser import Scenario as GherkinScenario
17
18
from .gherkin_parser import Step as GherkinStep
18
19
from .gherkin_parser import Tag as GherkinTag
@@ -113,6 +114,15 @@ def __bool__(self) -> bool:
113
114
return bool (self .examples )
114
115
115
116
117
+ @dataclass (eq = False )
118
+ class Rule :
119
+ keyword : str
120
+ name : str
121
+ description : str
122
+ tags : set [str ]
123
+ background : Background | None = None
124
+
125
+
116
126
@dataclass (eq = False )
117
127
class ScenarioTemplate :
118
128
"""Represents a scenario template within a feature.
@@ -127,6 +137,7 @@ class ScenarioTemplate:
127
137
tags (set[str]): A set of tags associated with the scenario.
128
138
_steps (List[Step]): The list of steps in the scenario (internal use only).
129
139
examples (Optional[Examples]): The examples used for parameterization in the scenario.
140
+ rule (Optional[Rule]): The rule to which the scenario may belong (None = no rule).
130
141
"""
131
142
132
143
feature : Feature
@@ -138,6 +149,7 @@ class ScenarioTemplate:
138
149
tags : set [str ] = field (default_factory = set )
139
150
_steps : list [Step ] = field (init = False , default_factory = list )
140
151
examples : list [Examples ] = field (default_factory = list [Examples ])
152
+ rule : Rule | None = None
141
153
142
154
def add_step (self , step : Step ) -> None :
143
155
"""Add a step to the scenario.
@@ -148,14 +160,25 @@ def add_step(self, step: Step) -> None:
148
160
step .scenario = self
149
161
self ._steps .append (step )
150
162
163
+ @property
164
+ def all_background_steps (self ) -> list [Step ]:
165
+ steps = []
166
+ # Add background steps from the feature
167
+ if self .feature .background :
168
+ steps .extend (self .feature .background .steps )
169
+ if self .rule is not None and self .rule .background is not None :
170
+ # Add background steps from the rule
171
+ steps .extend (self .rule .background .steps )
172
+ return steps
173
+
151
174
@property
152
175
def steps (self ) -> list [Step ]:
153
176
"""Get all steps for the scenario, including background steps.
154
177
155
178
Returns:
156
179
List[Step]: A list of steps, including any background steps from the feature.
157
180
"""
158
- return ( self .feature . background . steps if self . feature . background else []) + self ._steps
181
+ return self .all_background_steps + self ._steps
159
182
160
183
def render (self , context : Mapping [str , Any ]) -> Scenario :
161
184
"""Render the scenario with the given context.
@@ -166,7 +189,6 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
166
189
Returns:
167
190
Scenario: A Scenario object with steps rendered based on the context.
168
191
"""
169
- background_steps = self .feature .background .steps if self .feature .background else []
170
192
scenario_steps = [
171
193
Step (
172
194
name = step .render (context ),
@@ -179,7 +201,7 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
179
201
)
180
202
for step in self ._steps
181
203
]
182
- steps = background_steps + scenario_steps
204
+ steps = self . all_background_steps + scenario_steps
183
205
return Scenario (
184
206
feature = self .feature ,
185
207
keyword = self .keyword ,
@@ -188,6 +210,7 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
188
210
steps = steps ,
189
211
tags = self .tags ,
190
212
description = self .description ,
213
+ rule = self .rule ,
191
214
)
192
215
193
216
@@ -212,6 +235,7 @@ class Scenario:
212
235
steps : list [Step ]
213
236
description : str | None = None
214
237
tags : set [str ] = field (default_factory = set )
238
+ rule : Rule | None = None
215
239
216
240
217
241
@dataclass (eq = False )
@@ -307,12 +331,10 @@ class Background:
307
331
"""Represents the background steps for a feature.
308
332
309
333
Attributes:
310
- feature (Feature): The feature to which this background belongs.
311
334
line_number (int): The line number where the background starts in the file.
312
335
steps (List[Step]): The list of steps in the background.
313
336
"""
314
337
315
- feature : Feature
316
338
line_number : int
317
339
steps : list [Step ] = field (init = False , default_factory = list )
318
340
@@ -379,12 +401,15 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
379
401
)
380
402
return steps
381
403
382
- def parse_scenario (self , scenario_data : GherkinScenario , feature : Feature ) -> ScenarioTemplate :
404
+ def parse_scenario (
405
+ self , scenario_data : GherkinScenario , feature : Feature , rule : Rule | None = None
406
+ ) -> ScenarioTemplate :
383
407
"""Parse a scenario data dictionary into a ScenarioTemplate object.
384
408
385
409
Args:
386
410
scenario_data (dict): The dictionary containing scenario data.
387
411
feature (Feature): The feature to which this scenario belongs.
412
+ rule (Optional[Rule]): The rule to which this scenario may belong. (None = no rule)
388
413
389
414
Returns:
390
415
ScenarioTemplate: A ScenarioTemplate object representing the parsed scenario.
@@ -398,6 +423,7 @@ def parse_scenario(self, scenario_data: GherkinScenario, feature: Feature) -> Sc
398
423
templated = templated ,
399
424
tags = get_tag_names (scenario_data .tags ),
400
425
description = textwrap .dedent (scenario_data .description ),
426
+ rule = rule ,
401
427
)
402
428
for step in self .parse_steps (scenario_data .steps ):
403
429
scenario .add_step (step )
@@ -420,9 +446,8 @@ def parse_scenario(self, scenario_data: GherkinScenario, feature: Feature) -> Sc
420
446
421
447
return scenario
422
448
423
- def parse_background (self , background_data : GherkinBackground , feature : Feature ) -> Background :
449
+ def parse_background (self , background_data : GherkinBackground ) -> Background :
424
450
background = Background (
425
- feature = feature ,
426
451
line_number = background_data .location .line ,
427
452
)
428
453
background .steps = self .parse_steps (background_data .steps )
@@ -439,6 +464,7 @@ def _parse_feature_file(self) -> GherkinDocument:
439
464
return get_gherkin_document (self .abs_filename , self .encoding )
440
465
441
466
def parse (self ) -> Feature :
467
+ """Parse the feature file and return a Feature object with its backgrounds, rules, and scenarios."""
442
468
gherkin_doc : GherkinDocument = self ._parse_feature_file ()
443
469
feature_data : GherkinFeature = gherkin_doc .feature
444
470
feature = Feature (
@@ -456,9 +482,47 @@ def parse(self) -> Feature:
456
482
457
483
for child in feature_data .children :
458
484
if child .background :
459
- feature .background = self .parse_background (child .background , feature )
485
+ feature .background = self .parse_background (child .background )
486
+ elif child .rule :
487
+ self ._parse_and_add_rule (child .rule , feature )
460
488
elif child .scenario :
461
- scenario = self .parse_scenario (child .scenario , feature )
462
- feature .scenarios [scenario .name ] = scenario
489
+ self ._parse_and_add_scenario (child .scenario , feature )
463
490
464
491
return feature
492
+
493
+ def _parse_and_add_rule (self , rule_data : GherkinRule , feature : Feature ) -> None :
494
+ """Parse a rule, including its background and scenarios, and add to the feature."""
495
+ background = self ._extract_rule_background (rule_data )
496
+
497
+ rule = Rule (
498
+ keyword = rule_data .keyword ,
499
+ name = rule_data .name ,
500
+ description = rule_data .description ,
501
+ tags = get_tag_names (rule_data .tags ),
502
+ background = background ,
503
+ )
504
+
505
+ for scenario in self ._extract_rule_scenarios (rule_data , feature , rule ):
506
+ feature .scenarios [scenario .name ] = scenario
507
+
508
+ def _extract_rule_background (self , rule_data : GherkinRule ) -> Background | None :
509
+ """Extract the first background from rule children if it exists."""
510
+ for child in rule_data .children :
511
+ if child .background :
512
+ return self .parse_background (child .background )
513
+ return None
514
+
515
+ def _extract_rule_scenarios (
516
+ self , rule_data : GherkinRule , feature : Feature , rule : Rule
517
+ ) -> Generator [ScenarioTemplate ]:
518
+ """Yield each parsed scenario under a rule."""
519
+ for child in rule_data .children :
520
+ if child .scenario :
521
+ yield self .parse_scenario (child .scenario , feature , rule )
522
+
523
+ def _parse_and_add_scenario (
524
+ self , scenario_data : GherkinScenario , feature : Feature , rule : Rule | None = None
525
+ ) -> None :
526
+ """Parse an individual scenario and add it to the feature's scenarios."""
527
+ scenario = self .parse_scenario (scenario_data , feature , rule )
528
+ feature .scenarios [scenario .name ] = scenario
0 commit comments