Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the official gherkin parser #698

Merged
merged 40 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1a2baff
First commit for using the official gherkin parser (trying to maintai…
jsa34 Sep 5, 2024
65c06e4
Improve docstrings in parser.py
jsa34 Sep 5, 2024
abe5e79
Improve docstrings in parser.py
jsa34 Sep 5, 2024
240ac6d
Fix issues and create a FeatureParser class to consolidate parsing logic
jsa34 Sep 5, 2024
e7b5326
Forgot to go back and implement the templated bool
jsa34 Sep 6, 2024
2f3e029
Remove unused import
jsa34 Sep 6, 2024
cc9b37f
Move Gherkin parsing to pydantic models for easier future reference o…
jsa34 Sep 6, 2024
4e17ccb
Move the calculating of given/when/then to pydantic models, as well a…
jsa34 Sep 6, 2024
57b9e55
Fix silly mistakes
jsa34 Sep 6, 2024
ff1a926
Fix type hints for py3.8
jsa34 Sep 6, 2024
21afdb1
Response to feedback
jsa34 Sep 8, 2024
fec8270
Another grammar fix
jsa34 Sep 8, 2024
6676692
Use dataclasses and not attr
jsa34 Sep 8, 2024
becfed2
Response to feedback
jsa34 Sep 12, 2024
2c8455b
Response to feedback
jsa34 Sep 12, 2024
c3008c1
Couple of tidy ups
jsa34 Sep 12, 2024
9c12dbf
Forgot to fix background in steps and revert test that was skipped
jsa34 Sep 12, 2024
93a11ae
Fix import (Python < 3.11 compat)
youtux Sep 14, 2024
fee0eb9
Remove default to None
youtux Sep 14, 2024
7cbfc47
Revert string literals to their original form
youtux Sep 15, 2024
2f3acbd
Response to feedback and make mypy happy.
jsa34 Sep 15, 2024
9b45269
Merge remote-tracking branch 'origin/gherkin-official-parser' into gh…
jsa34 Sep 15, 2024
a3a5195
Do not fail the CI job if we can't upload to Codecov.
youtux Sep 16, 2024
656c7ce
Ignore py3.13 failures for now
youtux Sep 16, 2024
d11096f
Fix matching result in case there are warnings
youtux Sep 16, 2024
0a77782
Remove accidentally committed local file.
jsa34 Sep 21, 2024
2941d18
Merge remote-tracking branch 'origin/gherkin-official-parser' into gh…
jsa34 Sep 21, 2024
9801a28
Merge branch 'master' into gherkin-official-parser
jsa34 Sep 21, 2024
ca7508b
Merge branch 'master' into fork/jsa34/gherkin-official-parser
youtux Sep 21, 2024
093b6a8
Update release notes
youtux Sep 21, 2024
5f717c4
Require tests to pass on 3.13
youtux Sep 21, 2024
0fe4419
Only treat warnings from `pytest_bdd` as exceptions
youtux Sep 21, 2024
9b263f4
Add type annotations
youtux Sep 21, 2024
6fe8e40
remove redundant statements
youtux Sep 21, 2024
0573c6f
Fix typing issue
youtux Sep 21, 2024
8629438
Add missing type annotations, and require all files to be typed
youtux Sep 21, 2024
ace6ca5
mypy is passing, require typecheck outcome to be positive
youtux Sep 21, 2024
c12d8c1
Remove declaration for field that was never initialised
youtux Sep 21, 2024
8378a34
Remove unused function
youtux Sep 21, 2024
572934c
Update poetry
youtux Sep 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,27 @@ jobs:
include:
- python-version: "3.8"
toxfactor: py3.8
ignore-typecheck-outcome: true
ignore-typecheck-outcome: false
ignore-test-outcome: false
- python-version: "3.9"
toxfactor: py3.9
ignore-typecheck-outcome: true
ignore-typecheck-outcome: false
ignore-test-outcome: false
- python-version: "3.10"
toxfactor: py3.10
ignore-typecheck-outcome: true
ignore-typecheck-outcome: false
ignore-test-outcome: false
- python-version: "3.11"
toxfactor: py3.11
ignore-typecheck-outcome: true
ignore-typecheck-outcome: false
ignore-test-outcome: false
- python-version: "3.12"
toxfactor: py3.12
ignore-typecheck-outcome: true
ignore-typecheck-outcome: false
ignore-test-outcome: false
- python-version: "3.13-dev"
toxfactor: py3.13
ignore-typecheck-outcome: true
ignore-typecheck-outcome: false
ignore-test-outcome: false

steps:
Expand All @@ -47,7 +47,7 @@ jobs:

- name: Install poetry
run: |
python -m pip install poetry==1.8.2
python -m pip install poetry==1.8.3

- name: Configure poetry
run: |
Expand Down Expand Up @@ -81,10 +81,10 @@ jobs:
coverage combine
coverage xml

- uses: codecov/codecov-action@v3
- uses: codecov/codecov-action@v4
with:
# Explicitly using the token to avoid Codecov rate limit errors
# See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
fail_ci_if_error: false
verbose: true # optional (default = false)
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ Changelog

Unreleased
----------

- Use `gherkin-official` parser to replace custom parsing logic.
- Multiline steps must now always use triple-quotes for the additional lines.
- All feature files must now use the keyword `Feature:` to be considered valid.
- Tags can no longer have spaces (e.g. "@tag one" "@tag two" are no longer valid).

7.3.0
----------
Expand Down
367 changes: 199 additions & 168 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ parse-type = "*"
pytest = ">=6.2.0"
typing-extensions = "*"
packaging = "*"
gherkin-official = "^29.0.0"

[tool.poetry.group.dev.dependencies]
tox = ">=4.11.3"
Expand Down Expand Up @@ -93,6 +94,7 @@ python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
files = "src/pytest_bdd/**/*.py"
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = ["parse", "parse_type"]
Expand Down
3 changes: 2 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[pytest]
testpaths = tests
filterwarnings =
error
# only ignore errors from the pytest_bdd package
error:::(src)?\.pytest_bdd.*
6 changes: 3 additions & 3 deletions src/pytest_bdd/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
# if there was already one registered, so we need to force its value
# to the one we want to inject.
fixture_def = request._get_active_fixturedef(arg)
fixture_def.cached_result = (value, None, None)
fixture_def.cached_result = (value, None, None) # type: ignore

else:

def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None:
return fixturemanager.getfixturedefs(fixturename, node.nodeid)
return fixturemanager.getfixturedefs(fixturename, node.nodeid) # type: ignore

def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
"""Inject fixture into pytest fixture request.
Expand All @@ -49,7 +49,7 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
:param value: argument value
"""
fd = FixtureDef(
fixturemanager=request._fixturemanager,
fixturemanager=request._fixturemanager, # type: ignore
baseid=None,
argname=arg,
func=lambda: value,
Expand Down
8 changes: 4 additions & 4 deletions src/pytest_bdd/cucumber_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ def configure(config: Config) -> None:
cucumber_json_path = config.option.cucumber_json_path
# prevent opening json log on worker nodes (xdist)
if cucumber_json_path and not hasattr(config, "workerinput"):
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
config.pluginmanager.register(config._bddcucumberjson)
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path) # type: ignore[attr-defined]
config.pluginmanager.register(config._bddcucumberjson) # type: ignore[attr-defined]


def unconfigure(config: Config) -> None:
xml = getattr(config, "_bddcucumberjson", None)
xml = getattr(config, "_bddcucumberjson", None) # type: ignore[attr-defined]
if xml is not None:
del config._bddcucumberjson
del config._bddcucumberjson # type: ignore[attr-defined]
config.pluginmanager.unregister(xml)


Expand Down
42 changes: 33 additions & 9 deletions src/pytest_bdd/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ class ScenarioNotFound(ScenarioValidationError):
"""Scenario Not Found."""


class ExamplesNotValidError(ScenarioValidationError):
"""Example table is not valid."""


class StepDefinitionNotFoundError(Exception):
"""Step definition not found."""

Expand All @@ -27,11 +23,39 @@ class NoScenariosFound(Exception):
"""No scenarios found."""


class FeatureError(Exception):
"""Feature parse error."""
class GherkinParseError(Exception):
"""Base class for all Gherkin parsing errors."""

message = "{0}.\nLine number: {1}.\nLine: {2}.\nFile: {3}"
def __init__(self, message: str, line: int, line_content: str, filename: str) -> None:
super().__init__(message)
self.message = message
self.line = line
self.line_content = line_content
self.filename = filename

def __str__(self) -> str:
"""String representation."""
return self.message.format(*self.args)
return f"{self.message}\nLine number: {self.line}\nLine: {self.line_content}\nFile: {self.filename}"


class FeatureError(GherkinParseError):
pass


class BackgroundError(GherkinParseError):
pass


class ScenarioError(GherkinParseError):
pass


class StepError(GherkinParseError):
pass


class RuleError(GherkinParseError):
pass


class TokenError(GherkinParseError):
pass
21 changes: 10 additions & 11 deletions src/pytest_bdd/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import glob
import os.path

from .parser import Feature, parse_feature
from .parser import Feature, FeatureParser

# Global features dictionary
features: dict[str, Feature] = {}
Expand All @@ -52,30 +52,29 @@ def get_feature(base_path: str, filename: str, encoding: str = "utf-8") -> Featu
full_name = os.path.abspath(os.path.join(base_path, filename))
feature = features.get(full_name)
if not feature:
feature = parse_feature(base_path, filename, encoding=encoding)
feature = FeatureParser(base_path, filename, encoding).parse()
features[full_name] = feature
return feature


def get_features(paths: list[str], **kwargs) -> list[Feature]:
def get_features(paths: list[str], encoding: str = "utf-8") -> list[Feature]:
"""Get features for given paths.

:param list paths: `list` of paths (file or dirs)

:return: `list` of `Feature` objects.
"""
seen_names = set()
features = []
_features = []
for path in paths:
if path not in seen_names:
seen_names.add(path)
if os.path.isdir(path):
features.extend(
get_features(glob.iglob(os.path.join(path, "**", "*.feature"), recursive=True), **kwargs)
)
file_paths = list(glob.iglob(os.path.join(path, "**", "*.feature"), recursive=True))
_features.extend(get_features(file_paths, encoding=encoding))
else:
base, name = os.path.split(path)
feature = get_feature(base, name, **kwargs)
features.append(feature)
features.sort(key=lambda feature: feature.name or feature.filename)
return features
feature = get_feature(base, name, encoding=encoding)
_features.append(feature)
_features.sort(key=lambda _feature: _feature.name or _feature.filename)
return _features
6 changes: 3 additions & 3 deletions src/pytest_bdd/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import TYPE_CHECKING, cast

from _pytest._io import TerminalWriter
from mako.lookup import TemplateLookup
from mako.lookup import TemplateLookup # type: ignore

from .compat import getfixturedefs
from .feature import get_features
Expand Down Expand Up @@ -181,11 +181,11 @@ def _show_missing_code_main(config: Config, session: Session) -> None:
features, scenarios, steps = parse_feature_files(config.option.features)

for item in session.items:
if scenario := getattr(item.obj, "__scenario__", None):
if scenario := getattr(item.obj, "__scenario__", None): # type: ignore
if scenario in scenarios:
scenarios.remove(scenario)
for step in scenario.steps:
if _find_step_fixturedef(fm, item, step=step):
if _find_step_fixturedef(fm, item, step=step): # type: ignore
try:
steps.remove(step)
except ValueError:
Expand Down
Loading
Loading