Skip to content

Commit 0d42885

Browse files
authored
Merge pull request #698 from jsa34/gherkin-official-parser
Implement the official gherkin parser
2 parents cec07df + 572934c commit 0d42885

35 files changed

+2232
-794
lines changed

.github/workflows/main.yml

+9-9
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,27 @@ jobs:
1313
include:
1414
- python-version: "3.8"
1515
toxfactor: py3.8
16-
ignore-typecheck-outcome: true
16+
ignore-typecheck-outcome: false
1717
ignore-test-outcome: false
1818
- python-version: "3.9"
1919
toxfactor: py3.9
20-
ignore-typecheck-outcome: true
20+
ignore-typecheck-outcome: false
2121
ignore-test-outcome: false
2222
- python-version: "3.10"
2323
toxfactor: py3.10
24-
ignore-typecheck-outcome: true
24+
ignore-typecheck-outcome: false
2525
ignore-test-outcome: false
2626
- python-version: "3.11"
2727
toxfactor: py3.11
28-
ignore-typecheck-outcome: true
28+
ignore-typecheck-outcome: false
2929
ignore-test-outcome: false
3030
- python-version: "3.12"
3131
toxfactor: py3.12
32-
ignore-typecheck-outcome: true
32+
ignore-typecheck-outcome: false
3333
ignore-test-outcome: false
3434
- python-version: "3.13-dev"
3535
toxfactor: py3.13
36-
ignore-typecheck-outcome: true
36+
ignore-typecheck-outcome: false
3737
ignore-test-outcome: false
3838

3939
steps:
@@ -47,7 +47,7 @@ jobs:
4747

4848
- name: Install poetry
4949
run: |
50-
python -m pip install poetry==1.8.2
50+
python -m pip install poetry==1.8.3
5151
5252
- name: Configure poetry
5353
run: |
@@ -81,10 +81,10 @@ jobs:
8181
coverage combine
8282
coverage xml
8383
84-
- uses: codecov/codecov-action@v3
84+
- uses: codecov/codecov-action@v4
8585
with:
8686
# Explicitly using the token to avoid Codecov rate limit errors
8787
# See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954
8888
token: ${{ secrets.CODECOV_TOKEN }}
89-
fail_ci_if_error: true
89+
fail_ci_if_error: false
9090
verbose: true # optional (default = false)

CHANGES.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ Changelog
33

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

811
7.3.0
912
----------

poetry.lock

+199-168
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ parse-type = "*"
4242
pytest = ">=6.2.0"
4343
typing-extensions = "*"
4444
packaging = "*"
45+
gherkin-official = "^29.0.0"
4546

4647
[tool.poetry.group.dev.dependencies]
4748
tox = ">=4.11.3"
@@ -93,6 +94,7 @@ python_version = "3.8"
9394
warn_return_any = true
9495
warn_unused_configs = true
9596
files = "src/pytest_bdd/**/*.py"
97+
disallow_untyped_defs = true
9698

9799
[[tool.mypy.overrides]]
98100
module = ["parse", "parse_type"]

pytest.ini

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[pytest]
22
testpaths = tests
33
filterwarnings =
4-
error
4+
# only ignore errors from the pytest_bdd package
5+
error:::(src)?\.pytest_bdd.*

src/pytest_bdd/compat.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
3434
# if there was already one registered, so we need to force its value
3535
# to the one we want to inject.
3636
fixture_def = request._get_active_fixturedef(arg)
37-
fixture_def.cached_result = (value, None, None)
37+
fixture_def.cached_result = (value, None, None) # type: ignore
3838

3939
else:
4040

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

4444
def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
4545
"""Inject fixture into pytest fixture request.
@@ -49,7 +49,7 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
4949
:param value: argument value
5050
"""
5151
fd = FixtureDef(
52-
fixturemanager=request._fixturemanager,
52+
fixturemanager=request._fixturemanager, # type: ignore
5353
baseid=None,
5454
argname=arg,
5555
func=lambda: value,

src/pytest_bdd/cucumber_json.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ def configure(config: Config) -> None:
3535
cucumber_json_path = config.option.cucumber_json_path
3636
# prevent opening json log on worker nodes (xdist)
3737
if cucumber_json_path and not hasattr(config, "workerinput"):
38-
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
39-
config.pluginmanager.register(config._bddcucumberjson)
38+
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path) # type: ignore[attr-defined]
39+
config.pluginmanager.register(config._bddcucumberjson) # type: ignore[attr-defined]
4040

4141

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

4848

src/pytest_bdd/exceptions.py

+33-9
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ class ScenarioNotFound(ScenarioValidationError):
1515
"""Scenario Not Found."""
1616

1717

18-
class ExamplesNotValidError(ScenarioValidationError):
19-
"""Example table is not valid."""
20-
21-
2218
class StepDefinitionNotFoundError(Exception):
2319
"""Step definition not found."""
2420

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

2925

30-
class FeatureError(Exception):
31-
"""Feature parse error."""
26+
class GherkinParseError(Exception):
27+
"""Base class for all Gherkin parsing errors."""
3228

33-
message = "{0}.\nLine number: {1}.\nLine: {2}.\nFile: {3}"
29+
def __init__(self, message: str, line: int, line_content: str, filename: str) -> None:
30+
super().__init__(message)
31+
self.message = message
32+
self.line = line
33+
self.line_content = line_content
34+
self.filename = filename
3435

3536
def __str__(self) -> str:
36-
"""String representation."""
37-
return self.message.format(*self.args)
37+
return f"{self.message}\nLine number: {self.line}\nLine: {self.line_content}\nFile: {self.filename}"
38+
39+
40+
class FeatureError(GherkinParseError):
41+
pass
42+
43+
44+
class BackgroundError(GherkinParseError):
45+
pass
46+
47+
48+
class ScenarioError(GherkinParseError):
49+
pass
50+
51+
52+
class StepError(GherkinParseError):
53+
pass
54+
55+
56+
class RuleError(GherkinParseError):
57+
pass
58+
59+
60+
class TokenError(GherkinParseError):
61+
pass

src/pytest_bdd/feature.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import glob
3030
import os.path
3131

32-
from .parser import Feature, parse_feature
32+
from .parser import Feature, FeatureParser
3333

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

5959

60-
def get_features(paths: list[str], **kwargs) -> list[Feature]:
60+
def get_features(paths: list[str], encoding: str = "utf-8") -> list[Feature]:
6161
"""Get features for given paths.
6262
6363
:param list paths: `list` of paths (file or dirs)
6464
6565
:return: `list` of `Feature` objects.
6666
"""
6767
seen_names = set()
68-
features = []
68+
_features = []
6969
for path in paths:
7070
if path not in seen_names:
7171
seen_names.add(path)
7272
if os.path.isdir(path):
73-
features.extend(
74-
get_features(glob.iglob(os.path.join(path, "**", "*.feature"), recursive=True), **kwargs)
75-
)
73+
file_paths = list(glob.iglob(os.path.join(path, "**", "*.feature"), recursive=True))
74+
_features.extend(get_features(file_paths, encoding=encoding))
7675
else:
7776
base, name = os.path.split(path)
78-
feature = get_feature(base, name, **kwargs)
79-
features.append(feature)
80-
features.sort(key=lambda feature: feature.name or feature.filename)
81-
return features
77+
feature = get_feature(base, name, encoding=encoding)
78+
_features.append(feature)
79+
_features.sort(key=lambda _feature: _feature.name or _feature.filename)
80+
return _features

src/pytest_bdd/generation.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import TYPE_CHECKING, cast
88

99
from _pytest._io import TerminalWriter
10-
from mako.lookup import TemplateLookup
10+
from mako.lookup import TemplateLookup # type: ignore
1111

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

183183
for item in session.items:
184-
if scenario := getattr(item.obj, "__scenario__", None):
184+
if scenario := getattr(item.obj, "__scenario__", None): # type: ignore
185185
if scenario in scenarios:
186186
scenarios.remove(scenario)
187187
for step in scenario.steps:
188-
if _find_step_fixturedef(fm, item, step=step):
188+
if _find_step_fixturedef(fm, item, step=step): # type: ignore
189189
try:
190190
steps.remove(step)
191191
except ValueError:

0 commit comments

Comments
 (0)