Skip to content

Commit

Permalink
Merge pull request #29 from bradsbrown/pytest_bdd
Browse files Browse the repository at this point in the history
Enhancement: add pytest-bdd parser support
  • Loading branch information
brolewis authored Apr 17, 2020
2 parents 14b77ef + 07578be commit 91414e2
Show file tree
Hide file tree
Showing 18 changed files with 998 additions and 73 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ per-file-ignores =
tests/*:D103
# example rst output might have long lines
tests/rst_output.py:E501
tests/rst_output_pytest.py:E501
169 changes: 111 additions & 58 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sphinx_gherkindoc"
version = "3.3.7"
version = "3.4.0"
description = "A tool to convert Gherkin into Sphinx documentation"
authors = ["Lewis Franklin <[email protected]>", "Doug Philips <[email protected]>"]
readme = "README.rst"
Expand All @@ -22,6 +22,7 @@ ghp-import = "^0.5.5"
tomlkit = "^0.5.3"
sphinx-autodoc-typehints = "^1.6"
pytest = "^4.6"
pytest-bdd = {git = "https://github.com/rbcasperson/pytest-bdd.git", rev = "scenario-descriptions"}
pytest-cov = "^2.7"

[tool.poetry.scripts]
Expand Down
8 changes: 8 additions & 0 deletions sphinx_gherkindoc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .files import is_feature_file, is_rst_file, scan_tree
from .glossary import make_steps_glossary
from .parsers import parsers
from .utils import make_flat_name, set_dry_run, set_verbose, verbose
from .writer import feature_to_rst, toctree

Expand Down Expand Up @@ -119,6 +120,7 @@ def process_args(
feature_rst_file = feature_to_rst(
source_path,
root_path,
feature_parser=args.parser,
get_url_from_tag=get_url_from_tag,
get_url_from_step=get_url_from_step,
integrate_background=args.integrate_background,
Expand Down Expand Up @@ -215,6 +217,12 @@ def main() -> None:
"is also included."
),
)
parser.add_argument(
"--parser",
default="behave",
choices=list(parsers.keys()),
help=f"Specify an alternate parser to use.",
)
parser.add_argument(
"-v",
"--verbose",
Expand Down
19 changes: 19 additions & 0 deletions sphinx_gherkindoc/parsers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Sphinx-Gherkindoc Parsers."""
import importlib
from pathlib import Path


parsers = {}

for file in Path(__file__).parent.glob("*.py"):
name = file.stem
if name.startswith("_"):
continue
try:
module = importlib.import_module(f"sphinx_gherkindoc.parsers.{name}")
except ImportError:
continue
feature = getattr(module, "Feature", None)
if not feature:
continue
parsers[name] = feature
36 changes: 36 additions & 0 deletions sphinx_gherkindoc/parsers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Base classes for parsing."""


class BaseModel:
"""Base model for parsers."""

def __init__(self, data):
self._data = data

def __getattr__(self, key):
"""Grab attribute from wrapped class, if present.
When inheriting this model,
properties may need to be added to the subclass
in cases where a specific ``behave`` attribute
does not exist on the underlying class,
or where the format returned from the underlying attribute
does not match the ``behave`` format.
"""
if key == "description":
# Workaround for current pytest-bdd release (3.2.1),
# which does not have a scenario.description attribute.
return getattr(self._data, key, None)
return getattr(self._data, key)


class BaseFeature(BaseModel):
"""Feature base for parsers."""

def __init__(self, root_path, source_path):
self._data = None

@property
def examples(self):
"""Supports feature-level examples in some parsers."""
return []
11 changes: 11 additions & 0 deletions sphinx_gherkindoc/parsers/behave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Helper functions for writing rST files with behave parser."""
import behave.parser

from .base import BaseFeature


class Feature(BaseFeature):
"""Feature model for Behave."""

def __init__(self, root_path, source_path):
self._data = behave.parser.parse_file(source_path)
143 changes: 143 additions & 0 deletions sphinx_gherkindoc/parsers/pytest_bdd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Helper functions for writing rST files."""
from collections import namedtuple
import pathlib

import pytest_bdd.feature

from .base import BaseModel

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


class PytestModel(BaseModel):
"""Base Model for Pytest-Bdd objects."""

@property
def keyword(self):
"""Return the keyword for a given item."""
keyword = getattr(
self._data, "keyword", self._data.__class__.__name__.rsplit(".", 1)[-1]
)
if keyword == "Scenario" and self._data.examples.examples:
return "Scenario Outline"
return keyword

@property
def name(self):
"""Return the name for a given item, if available."""
return getattr(self._data, "name", None) or ""


class Step(PytestModel):
"""Step model for Pytest-Bdd."""

@property
def filename(self):
"""Return the source file path for the step."""
parent = self._data.scenario or self._data.background
return parent.feature.filename

@property
def line(self):
"""Return the line number from the source file."""
return self._data.line_number

@property
def step_type(self):
"""Return the step type/keyword."""
return self.keyword

@property
def table(self):
"""Return the step table, if present."""
lines = self._data.lines
if lines and all("|" in x for x in lines):
rows = [l.strip().split("|") for l 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):
"""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 [
l.strip() for l in self._data.lines if not set(l).issubset({"'", '"', " "})
]

@property
def name(self):
"""Return text after keyword."""
return self._data.name.splitlines()[0]


class Background(PytestModel):
"""Background model for Pytest-Bdd."""

@property
def steps(self):
"""Return the steps from the background."""
return [Step(s) for s in self._data.steps]


class Example(PytestModel):
"""Example model for Pytest-Bdd."""

@property
def tags(self):
"""Return an empty list of tags, as Pytest-Bdd does not support example tags."""
return []

@property
def table(self):
"""Return the Example table."""
return InlineTable(headings=self._data.example_params, rows=self._data.examples)


class Scenario(PytestModel):
"""Scenario model for Pytest-Bdd."""

@property
def steps(self):
"""Return (non-background) steps for the scenario."""
return [Step(s) for s in self._data.steps if not s.background]

@property
def examples(self):
"""Return examples from the scenario, if any exist."""
if self._data.examples.examples:
return [Example(self._data.examples)]
return []


class Feature(PytestModel):
"""Feature model for Pytest-Bdd."""

def __init__(self, root_path, source_path):
self._data = pytest_bdd.feature.Feature(
root_path, pathlib.Path(source_path).resolve().relative_to(root_path)
)

@property
def scenarios(self):
"""Return all scenarios for the feature."""
return [Scenario(s) for s in self._data.scenarios.values()]

@property
def background(self):
"""Return the background for the feature."""
return Background(self._data.background)

@property
def examples(self):
"""Return feature-level examples, if any exist."""
if self._data.examples.examples:
return [Example(self._data.examples)]
return []
7 changes: 7 additions & 0 deletions sphinx_gherkindoc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
INDENT_DEPTH = 4


MAIN_STEP_KEYWORDS = ["Given", "When", "Then"]

# The csv-table parser for restructuredtext does not allow for escaping so use
# a unicode character that looks like a quote but will not be in any Gherkin
QUOTE = "\u201C"


# DRY_RUN and VERBOSE are global states for all the code.
# By making these into global variables, the code "admits that" they are global;
# rather than cluttering up method parameters passing these values around,
Expand Down
12 changes: 11 additions & 1 deletion sphinx_gherkindoc/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from .files import is_rst_file
from .glossary import step_glossary
from .parsers import parsers
from .utils import (
display_name,
make_flat_name,
Expand Down Expand Up @@ -145,6 +146,7 @@ def apply_role(role: str, content: str) -> str:
def feature_to_rst(
source_path: pathlib.Path,
root_path: pathlib.Path,
feature_parser: str = "behave",
get_url_from_tag: Optional[Callable] = None,
get_url_from_step: Optional[Callable] = None,
integrate_background: bool = False,
Expand Down Expand Up @@ -325,9 +327,17 @@ def table(table: behave.model.Table, inline: bool = False) -> None:
if AVAILABLE_ROLES:
output_file.blank_line()

feature = behave.parser.parse_file(source_path)
feature_class = parsers.get(feature_parser, None)
if not feature_class:
raise KeyError(
f'No parser found for "{feature_parser}",'
f" options are: {list(parsers.keys())}"
)
feature = feature_class(root_path, source_path)
section(1, feature)
description(feature)
if feature.examples:
examples(None, feature)
if feature.background and not integrate_background:
section(2, feature.background)
steps(feature.background.steps)
Expand Down
24 changes: 12 additions & 12 deletions tests/basic.feature
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Feature: Testing Sphinx Writer
Test the additional options for a scenario

Given step text
'''
Lorum ipsum solor sit amet.
'''
'''
Lorum ipsum solor sit amet.
'''
When the suite reaches a scenario table
| name | department |
| Barry | Beer Cans |
Expand All @@ -54,13 +54,13 @@ Feature: Testing Sphinx Writer
Scenario: Indentation is ignored when any step in the scenario has text or a table

Given a step with some text
'''
Here be that said text!
'''
'''
Here be that said text!
'''
And an And step with some text too
'''
Hello again!
'''
'''
Hello again!
'''
And how about a table in there too
| position | name |
| first | Who |
Expand All @@ -69,8 +69,8 @@ Feature: Testing Sphinx Writer
When the fantasy has ended
And all the children are gone
Then something deep inside me
'''
Helps me to carry on
'''
'''
Helps me to carry on
'''
And Encarnaciooooon
And doodle-doodle-doodle-doo
Loading

0 comments on commit 91414e2

Please sign in to comment.