6
6
import textwrap
7
7
from collections import OrderedDict
8
8
from dataclasses import dataclass , field
9
- from typing import Any , Iterable , List , Mapping , Optional , Sequence
9
+ from typing import Any , Dict , Iterable , List , Mapping , Optional , Sequence
10
10
11
11
from gherkin .errors import CompositeParserException
12
12
from gherkin .parser import Parser
@@ -33,36 +33,6 @@ def strip_comments(line: str) -> str:
33
33
return line .strip ()
34
34
35
35
36
- def parse_feature (basedir : str , filename : str , encoding : str = "utf-8" ) -> Feature :
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
- """
50
- abs_filename = os .path .abspath (os .path .join (basedir , filename ))
51
- rel_filename = os .path .join (os .path .basename (basedir ), filename )
52
- with open (abs_filename , encoding = encoding ) as f :
53
- file_contents = f .read ()
54
- try :
55
- gherkin_document = Parser ().parse (TokenScanner (file_contents ))
56
- except CompositeParserException as e :
57
- raise FeatureError (
58
- e .args [0 ],
59
- e .errors [0 ].location ["line" ],
60
- linecache .getline (abs_filename , e .errors [0 ].location ["line" ]).rstrip ("\n " ),
61
- abs_filename ,
62
- ) from e
63
- return dict_to_feature (abs_filename , rel_filename , gherkin_document )
64
-
65
-
66
36
@dataclass (eq = False )
67
37
class Feature :
68
38
"""Represents a feature parsed from a feature file.
@@ -293,18 +263,19 @@ def params(self) -> tuple[str, ...]:
293
263
return tuple (frozenset (STEP_PARAM_RE .findall (self .name )))
294
264
295
265
def render (self , context : Mapping [str , Any ]) -> str :
296
- """Render the step name with the given context.
266
+ """Render the step name with the given context, but avoid replacing text inside angle brackets if context is missing .
297
267
298
268
Args:
299
269
context (Mapping[str, Any]): The context for rendering the step name.
300
270
301
271
Returns:
302
- str: The rendered step name with parameters replaced by their values from the context.
272
+ str: The rendered step name with parameters replaced only if they exist in the context.
303
273
"""
304
274
305
275
def replacer (m : re .Match ) -> str :
306
276
varname = m .group (1 )
307
- return str (context .get (varname , f"<missing:{ varname } >" ))
277
+ # If the context contains the variable, replace it. Otherwise, leave it unchanged.
278
+ return str (context .get (varname , f"<{ varname } >" ))
308
279
309
280
return STEP_PARAM_RE .sub (replacer , self .name )
310
281
@@ -333,18 +304,21 @@ def add_step(self, step: Step) -> None:
333
304
self .steps .append (step )
334
305
335
306
336
- 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.
307
+ class FeatureParser :
308
+ """Converts a feature file into a Feature object.
338
309
339
310
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.
311
+ basedir (str): The basedir for locating feature files.
312
+ filename (str): The filename of the feature file.
313
+ encoding (str): File encoding of the feature file to parse.
346
314
"""
347
315
316
+ def __init__ (self , basedir : str , filename : str , encoding : str = "utf-8" ):
317
+ self .abs_filename = os .path .abspath (os .path .join (basedir , filename ))
318
+ self .rel_filename = os .path .join (os .path .basename (basedir ), filename )
319
+ self .encoding = encoding
320
+
321
+ @staticmethod
348
322
def get_tag_names (tag_data : list [dict ]) -> set [str ]:
349
323
"""Extract tag names from tag data.
350
324
@@ -356,6 +330,7 @@ def get_tag_names(tag_data: list[dict]) -> set[str]:
356
330
"""
357
331
return {tag ["name" ].lstrip ("@" ) for tag in tag_data }
358
332
333
+ @staticmethod
359
334
def get_step_type (keyword : str ) -> str | None :
360
335
"""Map a step keyword to its corresponding type.
361
336
@@ -371,7 +346,7 @@ def get_step_type(keyword: str) -> str | None:
371
346
"then" : THEN ,
372
347
}.get (keyword )
373
348
374
- def parse_steps (steps_data : list [dict ]) -> list [Step ]:
349
+ def parse_steps (self , steps_data : list [dict ]) -> list [Step ]:
375
350
"""Parse a list of step data into Step objects.
376
351
377
352
Args:
@@ -384,7 +359,7 @@ def parse_steps(steps_data: list[dict]) -> list[Step]:
384
359
current_step_type = None
385
360
for step_data in steps_data :
386
361
keyword = step_data ["keyword" ].strip ().lower ()
387
- current_step_type = get_step_type (keyword ) or current_step_type
362
+ current_step_type = self . get_step_type (keyword ) or current_step_type
388
363
name = strip_comments (step_data ["text" ])
389
364
if "docString" in step_data :
390
365
doc_string = textwrap .dedent (step_data ["docString" ]["content" ])
@@ -400,7 +375,7 @@ def parse_steps(steps_data: list[dict]) -> list[Step]:
400
375
)
401
376
return steps
402
377
403
- def parse_scenario (scenario_data : dict , feature : Feature ) -> ScenarioTemplate :
378
+ def parse_scenario (self , scenario_data : dict , feature : Feature ) -> ScenarioTemplate :
404
379
"""Parse a scenario data dictionary into a ScenarioTemplate object.
405
380
406
381
Args:
@@ -415,10 +390,10 @@ def parse_scenario(scenario_data: dict, feature: Feature) -> ScenarioTemplate:
415
390
name = strip_comments (scenario_data ["name" ]),
416
391
line_number = scenario_data ["location" ]["line" ],
417
392
templated = False ,
418
- tags = get_tag_names (scenario_data ["tags" ]),
393
+ tags = self . get_tag_names (scenario_data ["tags" ]),
419
394
description = textwrap .dedent (scenario_data .get ("description" , "" )),
420
395
)
421
- for step in parse_steps (scenario_data ["steps" ]):
396
+ for step in self . parse_steps (scenario_data ["steps" ]):
422
397
scenario .add_step (step )
423
398
424
399
if "examples" in scenario_data :
@@ -436,31 +411,54 @@ def parse_scenario(scenario_data: dict, feature: Feature) -> ScenarioTemplate:
436
411
437
412
return scenario
438
413
439
- def parse_background (background_data : dict , feature : Feature ) -> Background :
414
+ def parse_background (self , background_data : dict , feature : Feature ) -> Background :
440
415
background = Background (
441
416
feature = feature ,
442
417
line_number = background_data ["location" ]["line" ],
443
418
)
444
- background .steps = parse_steps (background_data ["steps" ])
419
+ background .steps = self . parse_steps (background_data ["steps" ])
445
420
return background
446
421
447
- feature_data = data ["feature" ]
448
- feature = Feature (
449
- scenarios = OrderedDict (),
450
- filename = abs_filename ,
451
- rel_filename = rel_filename ,
452
- name = strip_comments (feature_data ["name" ]),
453
- tags = get_tag_names (feature_data ["tags" ]),
454
- background = None ,
455
- line_number = feature_data ["location" ]["line" ],
456
- description = textwrap .dedent (feature_data .get ("description" , "" )),
457
- )
458
-
459
- for child in feature_data ["children" ]:
460
- if "background" in child :
461
- feature .background = parse_background (child ["background" ], feature )
462
- elif "scenario" in child :
463
- scenario = parse_scenario (child ["scenario" ], feature )
464
- feature .scenarios [scenario .name ] = scenario
465
-
466
- return feature
422
+ def _parse_feature_file (self ) -> dict :
423
+ """Parse a feature file into a Feature object.
424
+
425
+ Returns:
426
+ Dict: A Gherkin document representation of the feature file.
427
+
428
+ Raises:
429
+ FeatureError: If there is an error parsing the feature file.
430
+ """
431
+ with open (self .abs_filename , encoding = self .encoding ) as f :
432
+ file_contents = f .read ()
433
+ try :
434
+ return Parser ().parse (TokenScanner (file_contents ))
435
+ except CompositeParserException as e :
436
+ raise FeatureError (
437
+ e .args [0 ],
438
+ e .errors [0 ].location ["line" ],
439
+ linecache .getline (self .abs_filename , e .errors [0 ].location ["line" ]).rstrip ("\n " ),
440
+ self .abs_filename ,
441
+ ) from e
442
+
443
+ def parse (self ):
444
+ data = self ._parse_feature_file ()
445
+ feature_data = data ["feature" ]
446
+ feature = Feature (
447
+ scenarios = OrderedDict (),
448
+ filename = self .abs_filename ,
449
+ rel_filename = self .rel_filename ,
450
+ name = strip_comments (feature_data ["name" ]),
451
+ tags = self .get_tag_names (feature_data ["tags" ]),
452
+ background = None ,
453
+ line_number = feature_data ["location" ]["line" ],
454
+ description = textwrap .dedent (feature_data .get ("description" , "" )),
455
+ )
456
+
457
+ for child in feature_data ["children" ]:
458
+ if "background" in child :
459
+ feature .background = self .parse_background (child ["background" ], feature )
460
+ elif "scenario" in child :
461
+ scenario = self .parse_scenario (child ["scenario" ], feature )
462
+ feature .scenarios [scenario .name ] = scenario
463
+
464
+ return feature
0 commit comments