pytest_beehave.feature_parser

Gherkin feature file parser for pytest-beehave.

  1"""Gherkin feature file parser for pytest-beehave."""
  2
  3from __future__ import annotations
  4
  5import re
  6from dataclasses import dataclass
  7from pathlib import Path
  8from typing import Any, Protocol, cast
  9
 10from gherkin import Parser as _GherkinParserImpl
 11
 12from pytest_beehave.models import ExampleId, FeatureSlug, RuleSlug
 13
 14_ID_TAG_RE: re.Pattern[str] = re.compile(r"@id:([a-f0-9]{8})")
 15
 16
 17class GherkinParserProtocol(Protocol):
 18    """Protocol for a Gherkin file parser."""
 19
 20    def parse(self, text: str) -> dict[str, Any]:  # pragma: no cover
 21        """Parse Gherkin text into an AST dict."""
 22        ...
 23
 24
 25class GherkinParser:
 26    """Adapter wrapping the gherkin library Parser to match GherkinParserProtocol."""
 27
 28    def __init__(self) -> None:
 29        """Initialise the underlying gherkin parser."""
 30        self._impl = _GherkinParserImpl()
 31
 32    def parse(self, text: str) -> dict[str, Any]:
 33        """Parse Gherkin text into an AST dict.
 34
 35        Args:
 36            text: The Gherkin feature file content.
 37
 38        Returns:
 39            AST as a dict.
 40        """
 41        return cast(dict[str, Any], self._impl.parse(text))
 42
 43
 44@dataclass(frozen=True, slots=True)
 45class ParsedStep:
 46    """A single step line parsed from a Gherkin Example.
 47
 48    Attributes:
 49        keyword: The step keyword (Given, When, Then, And, But, *).
 50        text: The step text.
 51        doc_string: Optional attached doc string content.
 52        data_table: Optional rendered data table string.
 53    """
 54
 55    keyword: str
 56    text: str
 57    doc_string: str | None
 58    data_table: str | None
 59
 60
 61@dataclass(frozen=True, slots=True)
 62class ParsedExample:
 63    """A single Example parsed from a .feature file.
 64
 65    Attributes:
 66        example_id: The @id hex identifier.
 67        steps: Tuple of parsed steps.
 68        background_sections: Background step tuples (feature-level then rule-level).
 69        outline_examples: Rendered Examples table string, if Scenario Outline.
 70        is_deprecated: True if tagged @deprecated.
 71    """
 72
 73    example_id: ExampleId
 74    steps: tuple[ParsedStep, ...]
 75    background_sections: tuple[tuple[ParsedStep, ...], ...]
 76    outline_examples: str | None
 77    is_deprecated: bool
 78
 79
 80@dataclass(frozen=True, slots=True)
 81class ParsedRule:
 82    """A Rule block parsed from a feature file.
 83
 84    Attributes:
 85        title: The Rule: title text.
 86        rule_slug: Slugified rule title (underscore-separated).
 87        examples: Tuple of parsed examples in this rule.
 88        is_deprecated: True if the rule itself is deprecated.
 89    """
 90
 91    title: str
 92    rule_slug: RuleSlug
 93    examples: tuple[ParsedExample, ...]
 94    is_deprecated: bool
 95
 96
 97@dataclass(frozen=True, slots=True)
 98class ParsedFeature:
 99    """A fully parsed .feature file.
100
101    Attributes:
102        path: Path to the .feature file.
103        feature_slug: Slugified feature folder name (underscore-separated).
104        rules: Tuple of parsed rules (may be empty if no Rule blocks).
105        top_level_examples: Examples not inside any Rule block.
106        is_deprecated: True if the feature is deprecated.
107    """
108
109    path: Path
110    feature_slug: FeatureSlug
111    rules: tuple[ParsedRule, ...]
112    top_level_examples: tuple[ParsedExample, ...]
113    is_deprecated: bool
114
115    def all_example_ids(self) -> set[ExampleId]:
116        """Collect all example IDs from rules and top-level examples.
117
118        Returns:
119            Set of ExampleId objects.
120        """
121        ids: set[ExampleId] = {ex.example_id for ex in self.top_level_examples}
122        for rule in self.rules:
123            ids.update(ex.example_id for ex in rule.examples)
124        return ids
125
126
127def _compute_col_widths(all_cells: list[list[str]]) -> list[int]:
128    """Compute maximum column widths across all rows.
129
130    Args:
131        all_cells: List of rows, each row is a list of cell value strings.
132
133    Returns:
134        List of column widths.
135    """
136    col_count = max(len(row) for row in all_cells)
137    return [
138        max(len(row[col]) for row in all_cells if col < len(row))
139        for col in range(col_count)
140    ]
141
142
143def _render_padded_row(row_cells: list[str], col_widths: list[int]) -> str:
144    """Render a table row with padded cells.
145
146    Args:
147        row_cells: Cell values for this row.
148        col_widths: Maximum width for each column.
149
150    Returns:
151        Pipe-delimited row string.
152    """
153    padded = [
154        row_cells[col].ljust(col_widths[col])
155        if col < len(row_cells)
156        else " " * col_widths[col]
157        for col in range(len(col_widths))
158    ]
159    return "| " + " | ".join(padded) + " |"
160
161
162def _render_data_table(rows: list[dict[str, Any]]) -> str:
163    """Render a Gherkin data table as a multi-line string.
164
165    Args:
166        rows: List of row dicts with 'cells' lists.
167
168    Returns:
169        Rendered table string.
170    """
171    if not rows:
172        return ""
173    all_cells = [
174        [cell.get("value", "") for cell in row.get("cells", [])] for row in rows
175    ]
176    col_widths = _compute_col_widths(all_cells)
177    return "\n".join(_render_padded_row(row, col_widths) for row in all_cells)
178
179
180def _render_examples_table(examples: list[dict[str, Any]]) -> str:
181    """Render the Examples table from a Scenario Outline.
182
183    Args:
184        examples: List of examples dicts from the Gherkin AST.
185
186    Returns:
187        Rendered Examples table, or empty string if none.
188    """
189    if not examples:
190        return ""
191    header = examples[0].get("tableHeader")
192    body = examples[0].get("tableBody", [])
193    all_rows: list[list[str]] = []
194    if header:
195        all_rows.append([cell.get("value", "") for cell in header.get("cells", [])])
196    for row in body:
197        all_rows.append([cell.get("value", "") for cell in row.get("cells", [])])
198    if not all_rows:
199        return "Examples:"
200    col_widths = _compute_col_widths(all_rows)
201    lines = ["Examples:"] + [
202        "  " + _render_padded_row(row, col_widths) for row in all_rows
203    ]
204    return "\n".join(lines)
205
206
207def _build_step(raw: dict[str, Any]) -> ParsedStep:
208    """Build a ParsedStep from a Gherkin AST step dict.
209
210    Args:
211        raw: A step dict from the Gherkin AST.
212
213    Returns:
214        A ParsedStep.
215    """
216    doc_string: str | None = None
217    data_table: str | None = None
218    if "docString" in raw:
219        doc_string = raw["docString"].get("content", "")
220    if "dataTable" in raw:
221        data_table = _render_data_table(raw["dataTable"].get("rows", []))
222    return ParsedStep(
223        keyword=raw["keyword"].strip(),
224        text=raw.get("text", ""),
225        doc_string=doc_string,
226        data_table=data_table,
227    )
228
229
230def _build_steps(raw_steps: list[dict[str, Any]]) -> tuple[ParsedStep, ...]:
231    """Build a tuple of ParsedStep from AST step list.
232
233    Args:
234        raw_steps: List of step dicts.
235
236    Returns:
237        Tuple of ParsedStep.
238    """
239    return tuple(_build_step(s) for s in raw_steps)
240
241
242def _extract_background(
243    children: list[dict[str, Any]],
244) -> tuple[ParsedStep, ...] | None:
245    """Extract background steps from a list of AST children.
246
247    Args:
248        children: Child dicts from the Gherkin AST.
249
250    Returns:
251        Tuple of ParsedStep, or None if no Background.
252    """
253    for child in children:
254        background = child.get("background")
255        if background is not None:
256            return _build_steps(background.get("steps", []))
257    return None
258
259
260def _extract_id_from_tags(tags: list[dict[str, Any]]) -> str | None:
261    """Find the @id:<hex> value from Gherkin AST tags.
262
263    Args:
264        tags: List of tag dicts.
265
266    Returns:
267        8-char hex ID or None.
268    """
269    for tag in tags:
270        match = _ID_TAG_RE.search(tag.get("name", ""))
271        if match:
272            return match.group(1)
273    return None
274
275
276def _has_deprecated_tag(tags: list[dict[str, Any]]) -> bool:
277    """Check if @deprecated tag is present.
278
279    Args:
280        tags: List of tag dicts.
281
282    Returns:
283        True if @deprecated is found.
284    """
285    return any(t["name"] == "@deprecated" for t in tags)
286
287
288def _collect_background_sections(
289    feature_bg: tuple[ParsedStep, ...] | None,
290    rule_bg: tuple[ParsedStep, ...] | None,
291) -> tuple[tuple[ParsedStep, ...], ...]:
292    """Collect non-None background step tuples in order.
293
294    Args:
295        feature_bg: Feature-level background steps.
296        rule_bg: Rule-level background steps.
297
298    Returns:
299        Tuple of background step tuples.
300    """
301    sections = [bg for bg in (feature_bg, rule_bg) if bg is not None]
302    return tuple(sections)
303
304
305def _build_example(
306    scenario: dict[str, Any],
307    feature_bg: tuple[ParsedStep, ...] | None,
308    rule_bg: tuple[ParsedStep, ...] | None,
309    parent_deprecated: bool = False,
310) -> ParsedExample | None:
311    """Build a ParsedExample from a scenario dict.
312
313    Args:
314        scenario: Gherkin AST scenario dict.
315        feature_bg: Feature-level background steps.
316        rule_bg: Rule-level background steps.
317        parent_deprecated: True if a parent (rule or feature) is deprecated.
318
319    Returns:
320        ParsedExample or None if no @id tag.
321    """
322    tags = scenario.get("tags", [])
323    id_str = _extract_id_from_tags(tags)
324    if id_str is None:
325        return None
326    outline_examples = scenario.get("examples", [])
327    return ParsedExample(
328        example_id=ExampleId(id_str),
329        steps=_build_steps(scenario.get("steps", [])),
330        background_sections=_collect_background_sections(feature_bg, rule_bg),
331        outline_examples=(
332            _render_examples_table(outline_examples) if outline_examples else None
333        ),
334        is_deprecated=parent_deprecated or _has_deprecated_tag(tags),
335    )
336
337
338def _example_from_child(
339    child: dict[str, Any],
340    feature_bg: tuple[ParsedStep, ...] | None,
341    rule_bg: tuple[ParsedStep, ...] | None,
342    rule_deprecated: bool,
343) -> ParsedExample | None:
344    """Return a ParsedExample from a rule child dict, or None if not a scenario.
345
346    Args:
347        child: A child dict from the rule's Gherkin AST.
348        feature_bg: Feature-level background steps.
349        rule_bg: Rule-level background steps.
350        rule_deprecated: True if the rule is deprecated.
351
352    Returns:
353        ParsedExample or None.
354    """
355    scenario = child.get("scenario")
356    if scenario is None:
357        return None
358    return _build_example(scenario, feature_bg, rule_bg, rule_deprecated)
359
360
361def _parse_rule_examples(
362    rule_children: list[dict[str, Any]],
363    feature_bg: tuple[ParsedStep, ...] | None,
364    rule_bg: tuple[ParsedStep, ...] | None,
365    rule_deprecated: bool,
366) -> tuple[ParsedExample, ...]:
367    """Parse all examples from rule children.
368
369    Args:
370        rule_children: Child dicts from the rule's Gherkin AST.
371        feature_bg: Feature-level background steps.
372        rule_bg: Rule-level background steps.
373        rule_deprecated: True if the rule is deprecated.
374
375    Returns:
376        Tuple of ParsedExample.
377    """
378    candidates = (
379        _example_from_child(child, feature_bg, rule_bg, rule_deprecated)
380        for child in rule_children
381    )
382    return tuple(ex for ex in candidates if ex is not None)
383
384
385def _parse_rule(
386    rule: dict[str, Any],
387    feature_bg: tuple[ParsedStep, ...] | None,
388    feature_deprecated: bool = False,
389) -> ParsedRule:
390    """Parse a Rule block into a ParsedRule.
391
392    Args:
393        rule: Rule dict from the Gherkin AST.
394        feature_bg: Feature-level background steps.
395        feature_deprecated: True if the parent feature is deprecated.
396
397    Returns:
398        A ParsedRule.
399    """
400    title = rule.get("name", "")
401    rule_children = rule.get("children", [])
402    rule_deprecated = feature_deprecated or _has_deprecated_tag(rule.get("tags", []))
403    rule_bg = _extract_background(rule_children)
404    examples = _parse_rule_examples(rule_children, feature_bg, rule_bg, rule_deprecated)
405    return ParsedRule(
406        title=title,
407        rule_slug=RuleSlug.from_rule_title(title),
408        examples=examples,
409        is_deprecated=rule_deprecated,
410    )
411
412
413def _empty_feature(path: Path, feature_slug: FeatureSlug) -> ParsedFeature:
414    """Return an empty ParsedFeature for a file with no feature block.
415
416    Args:
417        path: Path to the .feature file.
418        feature_slug: The feature slug.
419
420    Returns:
421        ParsedFeature with no rules or examples.
422    """
423    return ParsedFeature(
424        path=path,
425        feature_slug=feature_slug,
426        rules=(),
427        top_level_examples=(),
428        is_deprecated=False,
429    )
430
431
432def _parse_child(
433    child: dict[str, Any],
434    feature_bg: tuple[ParsedStep, ...] | None,
435    feature_deprecated: bool,
436    rules: list[ParsedRule],
437    top_level: list[ParsedExample],
438) -> None:
439    """Parse one feature child into rules or top-level examples.
440
441    Args:
442        child: A child dict from the Gherkin AST.
443        feature_bg: Feature-level background steps.
444        feature_deprecated: True if the feature is deprecated.
445        rules: List to append ParsedRule to.
446        top_level: List to append ParsedExample to.
447    """
448    rule_node = child.get("rule")
449    if rule_node is not None:
450        rules.append(_parse_rule(rule_node, feature_bg, feature_deprecated))
451        return
452    scenario = child.get("scenario")
453    if scenario is None:
454        return
455    ex = _build_example(scenario, feature_bg, None, feature_deprecated)
456    if ex is not None:
457        top_level.append(ex)
458
459
460def _parse_children(
461    children: list[dict[str, Any]],
462    feature_bg: tuple[ParsedStep, ...] | None,
463    feature_deprecated: bool,
464) -> tuple[tuple[ParsedRule, ...], tuple[ParsedExample, ...]]:
465    """Parse the children of a feature block into rules and top-level examples.
466
467    Args:
468        children: Child dicts from the Gherkin AST.
469        feature_bg: Feature-level background steps.
470        feature_deprecated: True if the feature is deprecated.
471
472    Returns:
473        Tuple of (rules, top_level_examples).
474    """
475    rules: list[ParsedRule] = []
476    top_level: list[ParsedExample] = []
477    for child in children:
478        _parse_child(child, feature_bg, feature_deprecated, rules, top_level)
479    return tuple(rules), tuple(top_level)
480
481
482def parse_feature(
483    path: Path,
484    folder_name: str | None = None,
485    parser: GherkinParserProtocol | None = None,
486) -> ParsedFeature:
487    """Parse a .feature file into a ParsedFeature.
488
489    Args:
490        path: Path to the .feature file.
491        folder_name: The feature folder name. Defaults to path.parent.name.
492        parser: Optional Gherkin parser instance. Defaults to GherkinParser().
493
494    Returns:
495        A ParsedFeature with all examples.
496    """
497    if folder_name is None:
498        parent = path.parent
499        folder_name = parent.name
500    if parser is None:
501        parser = GherkinParser()
502    doc = parser.parse(path.read_text(encoding="utf-8"))
503    feature = cast(dict[str, Any] | None, doc.get("feature"))
504    feature_slug = FeatureSlug.from_folder_name(folder_name)
505    if not feature:
506        return _empty_feature(path, feature_slug)
507    children = feature.get("children", [])
508    feature_deprecated = _has_deprecated_tag(feature.get("tags", []))
509    feature_bg = _extract_background(children)
510    rules, top_level = _parse_children(children, feature_bg, feature_deprecated)
511    return ParsedFeature(
512        path=path,
513        feature_slug=feature_slug,
514        rules=rules,
515        top_level_examples=top_level,
516        is_deprecated=feature_deprecated,
517    )
518
519
520def collect_all_example_ids(feature: ParsedFeature) -> set[ExampleId]:
521    """Collect all example IDs from a parsed feature.
522
523    Args:
524        feature: A ParsedFeature.
525
526    Returns:
527        Set of ExampleId objects.
528    """
529    return feature.all_example_ids()
class GherkinParserProtocol(typing.Protocol):
18class GherkinParserProtocol(Protocol):
19    """Protocol for a Gherkin file parser."""
20
21    def parse(self, text: str) -> dict[str, Any]:  # pragma: no cover
22        """Parse Gherkin text into an AST dict."""
23        ...

Protocol for a Gherkin file parser.

GherkinParserProtocol(*args, **kwargs)
1960def _no_init_or_replace_init(self, *args, **kwargs):
1961    cls = type(self)
1962
1963    if cls._is_protocol:
1964        raise TypeError('Protocols cannot be instantiated')
1965
1966    # Already using a custom `__init__`. No need to calculate correct
1967    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1968    if cls.__init__ is not _no_init_or_replace_init:
1969        return
1970
1971    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1972    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1973    # searches for a proper new `__init__` in the MRO. The new `__init__`
1974    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1975    # instantiation of the protocol subclass will thus use the new
1976    # `__init__` and no longer call `_no_init_or_replace_init`.
1977    for base in cls.__mro__:
1978        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1979        if init is not _no_init_or_replace_init:
1980            cls.__init__ = init
1981            break
1982    else:
1983        # should not happen
1984        cls.__init__ = object.__init__
1985
1986    cls.__init__(self, *args, **kwargs)
def parse(self, text: str) -> dict[str, typing.Any]:
21    def parse(self, text: str) -> dict[str, Any]:  # pragma: no cover
22        """Parse Gherkin text into an AST dict."""
23        ...

Parse Gherkin text into an AST dict.

class GherkinParser:
26class GherkinParser:
27    """Adapter wrapping the gherkin library Parser to match GherkinParserProtocol."""
28
29    def __init__(self) -> None:
30        """Initialise the underlying gherkin parser."""
31        self._impl = _GherkinParserImpl()
32
33    def parse(self, text: str) -> dict[str, Any]:
34        """Parse Gherkin text into an AST dict.
35
36        Args:
37            text: The Gherkin feature file content.
38
39        Returns:
40            AST as a dict.
41        """
42        return cast(dict[str, Any], self._impl.parse(text))

Adapter wrapping the gherkin library Parser to match GherkinParserProtocol.

GherkinParser()
29    def __init__(self) -> None:
30        """Initialise the underlying gherkin parser."""
31        self._impl = _GherkinParserImpl()

Initialise the underlying gherkin parser.

def parse(self, text: str) -> dict[str, typing.Any]:
33    def parse(self, text: str) -> dict[str, Any]:
34        """Parse Gherkin text into an AST dict.
35
36        Args:
37            text: The Gherkin feature file content.
38
39        Returns:
40            AST as a dict.
41        """
42        return cast(dict[str, Any], self._impl.parse(text))

Parse Gherkin text into an AST dict.

Args: text: The Gherkin feature file content.

Returns: AST as a dict.

@dataclass(frozen=True, slots=True)
class ParsedStep:
45@dataclass(frozen=True, slots=True)
46class ParsedStep:
47    """A single step line parsed from a Gherkin Example.
48
49    Attributes:
50        keyword: The step keyword (Given, When, Then, And, But, *).
51        text: The step text.
52        doc_string: Optional attached doc string content.
53        data_table: Optional rendered data table string.
54    """
55
56    keyword: str
57    text: str
58    doc_string: str | None
59    data_table: str | None

A single step line parsed from a Gherkin Example.

Attributes: keyword: The step keyword (Given, When, Then, And, But, *). text: The step text. doc_string: Optional attached doc string content. data_table: Optional rendered data table string.

ParsedStep( keyword: str, text: str, doc_string: str | None, data_table: str | None)
keyword: str
text: str
doc_string: str | None
data_table: str | None
@dataclass(frozen=True, slots=True)
class ParsedExample:
62@dataclass(frozen=True, slots=True)
63class ParsedExample:
64    """A single Example parsed from a .feature file.
65
66    Attributes:
67        example_id: The @id hex identifier.
68        steps: Tuple of parsed steps.
69        background_sections: Background step tuples (feature-level then rule-level).
70        outline_examples: Rendered Examples table string, if Scenario Outline.
71        is_deprecated: True if tagged @deprecated.
72    """
73
74    example_id: ExampleId
75    steps: tuple[ParsedStep, ...]
76    background_sections: tuple[tuple[ParsedStep, ...], ...]
77    outline_examples: str | None
78    is_deprecated: bool

A single Example parsed from a .feature file.

Attributes: example_id: The @id hex identifier. steps: Tuple of parsed steps. background_sections: Background step tuples (feature-level then rule-level). outline_examples: Rendered Examples table string, if Scenario Outline. is_deprecated: True if tagged @deprecated.

ParsedExample( example_id: pytest_beehave.models.ExampleId, steps: tuple[ParsedStep, ...], background_sections: tuple[tuple[ParsedStep, ...], ...], outline_examples: str | None, is_deprecated: bool)
steps: tuple[ParsedStep, ...]
background_sections: tuple[tuple[ParsedStep, ...], ...]
outline_examples: str | None
is_deprecated: bool
@dataclass(frozen=True, slots=True)
class ParsedRule:
81@dataclass(frozen=True, slots=True)
82class ParsedRule:
83    """A Rule block parsed from a feature file.
84
85    Attributes:
86        title: The Rule: title text.
87        rule_slug: Slugified rule title (underscore-separated).
88        examples: Tuple of parsed examples in this rule.
89        is_deprecated: True if the rule itself is deprecated.
90    """
91
92    title: str
93    rule_slug: RuleSlug
94    examples: tuple[ParsedExample, ...]
95    is_deprecated: bool

A Rule block parsed from a feature file.

Attributes: title: The Rule: title text. rule_slug: Slugified rule title (underscore-separated). examples: Tuple of parsed examples in this rule. is_deprecated: True if the rule itself is deprecated.

ParsedRule( title: str, rule_slug: pytest_beehave.models.RuleSlug, examples: tuple[ParsedExample, ...], is_deprecated: bool)
title: str
examples: tuple[ParsedExample, ...]
is_deprecated: bool
@dataclass(frozen=True, slots=True)
class ParsedFeature:
 98@dataclass(frozen=True, slots=True)
 99class ParsedFeature:
100    """A fully parsed .feature file.
101
102    Attributes:
103        path: Path to the .feature file.
104        feature_slug: Slugified feature folder name (underscore-separated).
105        rules: Tuple of parsed rules (may be empty if no Rule blocks).
106        top_level_examples: Examples not inside any Rule block.
107        is_deprecated: True if the feature is deprecated.
108    """
109
110    path: Path
111    feature_slug: FeatureSlug
112    rules: tuple[ParsedRule, ...]
113    top_level_examples: tuple[ParsedExample, ...]
114    is_deprecated: bool
115
116    def all_example_ids(self) -> set[ExampleId]:
117        """Collect all example IDs from rules and top-level examples.
118
119        Returns:
120            Set of ExampleId objects.
121        """
122        ids: set[ExampleId] = {ex.example_id for ex in self.top_level_examples}
123        for rule in self.rules:
124            ids.update(ex.example_id for ex in rule.examples)
125        return ids

A fully parsed .feature file.

Attributes: path: Path to the .feature file. feature_slug: Slugified feature folder name (underscore-separated). rules: Tuple of parsed rules (may be empty if no Rule blocks). top_level_examples: Examples not inside any Rule block. is_deprecated: True if the feature is deprecated.

ParsedFeature( path: pathlib._local.Path, feature_slug: pytest_beehave.models.FeatureSlug, rules: tuple[ParsedRule, ...], top_level_examples: tuple[ParsedExample, ...], is_deprecated: bool)
path: pathlib._local.Path
rules: tuple[ParsedRule, ...]
top_level_examples: tuple[ParsedExample, ...]
is_deprecated: bool
def all_example_ids(self) -> set[pytest_beehave.models.ExampleId]:
116    def all_example_ids(self) -> set[ExampleId]:
117        """Collect all example IDs from rules and top-level examples.
118
119        Returns:
120            Set of ExampleId objects.
121        """
122        ids: set[ExampleId] = {ex.example_id for ex in self.top_level_examples}
123        for rule in self.rules:
124            ids.update(ex.example_id for ex in rule.examples)
125        return ids

Collect all example IDs from rules and top-level examples.

Returns: Set of ExampleId objects.

def parse_feature( path: pathlib._local.Path, folder_name: str | None = None, parser: GherkinParserProtocol | None = None) -> ParsedFeature:
483def parse_feature(
484    path: Path,
485    folder_name: str | None = None,
486    parser: GherkinParserProtocol | None = None,
487) -> ParsedFeature:
488    """Parse a .feature file into a ParsedFeature.
489
490    Args:
491        path: Path to the .feature file.
492        folder_name: The feature folder name. Defaults to path.parent.name.
493        parser: Optional Gherkin parser instance. Defaults to GherkinParser().
494
495    Returns:
496        A ParsedFeature with all examples.
497    """
498    if folder_name is None:
499        parent = path.parent
500        folder_name = parent.name
501    if parser is None:
502        parser = GherkinParser()
503    doc = parser.parse(path.read_text(encoding="utf-8"))
504    feature = cast(dict[str, Any] | None, doc.get("feature"))
505    feature_slug = FeatureSlug.from_folder_name(folder_name)
506    if not feature:
507        return _empty_feature(path, feature_slug)
508    children = feature.get("children", [])
509    feature_deprecated = _has_deprecated_tag(feature.get("tags", []))
510    feature_bg = _extract_background(children)
511    rules, top_level = _parse_children(children, feature_bg, feature_deprecated)
512    return ParsedFeature(
513        path=path,
514        feature_slug=feature_slug,
515        rules=rules,
516        top_level_examples=top_level,
517        is_deprecated=feature_deprecated,
518    )

Parse a .feature file into a ParsedFeature.

Args: path: Path to the .feature file. folder_name: The feature folder name. Defaults to path.parent.name. parser: Optional Gherkin parser instance. Defaults to GherkinParser().

Returns: A ParsedFeature with all examples.

def collect_all_example_ids( feature: ParsedFeature) -> set[pytest_beehave.models.ExampleId]:
521def collect_all_example_ids(feature: ParsedFeature) -> set[ExampleId]:
522    """Collect all example IDs from a parsed feature.
523
524    Args:
525        feature: A ParsedFeature.
526
527    Returns:
528        Set of ExampleId objects.
529    """
530    return feature.all_example_ids()

Collect all example IDs from a parsed feature.

Args: feature: A ParsedFeature.

Returns: Set of ExampleId objects.