Source code for sphinx_gherkindoc.parsers.pytest_bdd

"""Helper functions for writing rST files."""
from collections import namedtuple
from typing import List, Optional, Union
import pathlib

import pytest_bdd.feature

from .base import BaseModel

BlankString = str
InlineTable = namedtuple("InlineTable", ["headings", "rows"])


[docs]class PytestModel(BaseModel): """Base Model for Pytest-Bdd objects.""" @property def description(self) -> Optional[List[str]]: """Return description as a list of lines.""" description = getattr(self._data, "description", None) if description: lines = list(description.split("\n")) lines.append("") return lines return description @property def keyword(self) -> str: """Return the keyword for a given item.""" keyword = getattr( self._data, "keyword", self._data.__class__.__name__.rsplit(".", 1)[-1] ) # Match Scenario or ScenarioTemplate if keyword.startswith("Scenario"): if self._data.examples.examples: return "Scenario Outline" return "Scenario" return keyword @property def name(self) -> str: """Return the name for a given item, if available.""" return getattr(self._data, "name", None) or ""
[docs]class Step(PytestModel): """Step model for Pytest-Bdd.""" @property def filename(self) -> str: """Return the source file path for the step.""" parent = self._data.scenario or self._data.background return parent.feature.filename @property def line(self) -> int: """Return the line number from the source file.""" return self._data.line_number @property def step_type(self) -> str: """Return the step type/keyword.""" return self.keyword @property def table(self) -> Union[InlineTable, BlankString]: """Return the step table, if present.""" lines = self._data.lines if lines and all("|" in x for x in lines): rows = [x.strip().split("|") for x in lines] rows = [ list(filter(None, (entry.strip() for entry in row))) for row in rows ] return InlineTable(headings=rows[0], rows=rows[1:]) return "" @property def text(self) -> Union[List[str], BlankString]: """Return the (non-table) multi-line text from a step.""" if self.table: # pytest-bdd doesn't distinguish between table and text # in the same way as behave, # so we determine whether the lines are a table, # and return only non-table lines. return "" return [ x.strip() for x in self._data.lines if not set(x).issubset({"'", '"', " "}) ] @property def name(self) -> str: """Return text after keyword.""" return self._data.name.splitlines()[0]
[docs]class Background(PytestModel): """Background model for Pytest-Bdd.""" @property def steps(self) -> List[Step]: """Return the steps from the background.""" return [Step(s) for s in self._data.steps]
[docs]class Example(PytestModel): """Example model for Pytest-Bdd.""" @property def tags(self) -> list: """Return an empty list of tags, as Pytest-Bdd does not support example tags.""" return [] @property def table(self) -> InlineTable: """Return the Example table.""" return InlineTable(headings=self._data.example_params, rows=self._data.examples)
[docs]class Scenario(PytestModel): """Scenario model for Pytest-Bdd.""" @property def steps(self) -> List[Step]: """Return (non-background) steps for the scenario.""" return [Step(s) for s in self._data.steps if not s.background] @property def examples(self) -> List[Optional[Example]]: """Return examples from the scenario, if any exist.""" if self._data.examples.examples: return [Example(self._data.examples)] return []
[docs]class Feature(PytestModel): """Feature model for Pytest-Bdd.""" def __init__(self, root_path: str, source_path: str): relative_file_path = pathlib.Path(source_path).resolve().relative_to(root_path) # If the version of pytest-bdd implements the parse_feature method then use it pytest_bdd_parse_feature = getattr(pytest_bdd.feature, "parse_feature", None) try: if callable(pytest_bdd_parse_feature): self._data = pytest_bdd.feature.parse_feature( root_path, relative_file_path ) else: self._data = pytest_bdd.feature.Feature(root_path, relative_file_path) except Exception as e: raise ValueError( f"Failed to parse feature file: {relative_file_path}" ) from e @property def scenarios(self) -> List[Scenario]: """Return all scenarios for the feature.""" return [Scenario(s) for s in self._data.scenarios.values()] @property def background(self) -> Optional[Background]: """Return the background for the feature.""" background_entry = self._data.background return Background(background_entry) if background_entry else None @property def examples(self) -> List[Optional[Example]]: """Return feature-level examples, if any exist.""" if hasattr(self._data, "examples") and self._data.examples.examples: return [Example(self._data.examples)] return []