Skip to content

Commit dcafea0

Browse files
authored
Merge pull request #725 from pytest-dev/implement-localisation
2 parents 2a402c6 + 06b4cca commit dcafea0

File tree

10 files changed

+107
-6
lines changed

10 files changed

+107
-6
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Unreleased
66
- Dropped support for python 3.8. Supported python versions: 3.9, 3.10, 3.11, 3.12, 3.13.
77
- Text after the `#` character is no longer stripped from the Scenario and Feature name.
88
- Gherkin keyword aliases can now be used and correctly reported in json and terminal output (see `Keywords <https://cucumber.io/docs/gherkin/reference/#keywords>` for permitted list).
9+
- Added localization support. The language of the feature file can be specified using the `# language: <language>` directive at the beginning of the file.
910

1011
8.0.0b2
1112
----------

src/pytest_bdd/cucumber_json.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def stepmap(step: dict[str, Any]) -> dict[str, Any]:
120120
"id": scenario["feature"]["rel_filename"].lower().replace(" ", "-"),
121121
"line": scenario["feature"]["line_number"],
122122
"description": scenario["feature"]["description"],
123+
"language": scenario["feature"]["language"],
123124
"tags": self._serialize_tags(scenario["feature"]),
124125
"elements": [],
125126
}

src/pytest_bdd/gherkin_parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ def from_dict(cls, data: dict[str, Any]) -> Self:
269269
@dataclass
270270
class Feature:
271271
location: Location
272+
language: str
272273
keyword: str
273274
tags: list[Tag]
274275
name: str
@@ -279,6 +280,7 @@ class Feature:
279280
def from_dict(cls, data: dict[str, Any]) -> Self:
280281
return cls(
281282
location=Location.from_dict(data["location"]),
283+
language=data["language"],
282284
keyword=data["keyword"],
283285
tags=[Tag.from_dict(tag) for tag in data["tags"]],
284286
name=data["name"],

src/pytest_bdd/parser.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .gherkin_parser import Step as GherkinStep
1818
from .gherkin_parser import Tag as GherkinTag
1919
from .gherkin_parser import get_gherkin_document
20-
from .types import STEP_TYPES
20+
from .types import STEP_TYPE_BY_PARSER_KEYWORD
2121

2222
STEP_PARAM_RE = re.compile(r"<(.+?)>")
2323

@@ -40,6 +40,7 @@ class Feature:
4040
scenarios: OrderedDict[str, ScenarioTemplate]
4141
filename: str
4242
rel_filename: str
43+
language: str
4344
keyword: str
4445
name: str | None
4546
tags: set[str]
@@ -352,7 +353,7 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
352353
return []
353354

354355
first_step = steps_data[0]
355-
if first_step.keyword.lower() not in STEP_TYPES:
356+
if first_step.keyword_type not in STEP_TYPE_BY_PARSER_KEYWORD:
356357
raise StepError(
357358
message=f"First step in a scenario or background must start with 'Given', 'When' or 'Then', but got {first_step.keyword}.",
358359
line=first_step.location.line,
@@ -361,11 +362,9 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
361362
)
362363

363364
steps = []
364-
current_type = first_step.keyword.lower()
365+
current_type = STEP_TYPE_BY_PARSER_KEYWORD[first_step.keyword_type]
365366
for step in steps_data:
366-
keyword = step.keyword.lower()
367-
if keyword in STEP_TYPES:
368-
current_type = keyword
367+
current_type = STEP_TYPE_BY_PARSER_KEYWORD.get(step.keyword_type, current_type)
369368
steps.append(
370369
Step(
371370
name=step.text,
@@ -449,6 +448,7 @@ def parse(self) -> Feature:
449448
background=None,
450449
line_number=feature_data.location.line,
451450
description=textwrap.dedent(feature_data.description),
451+
language=feature_data.language,
452452
)
453453

454454
for child in feature_data.children:

src/pytest_bdd/reporting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def serialize(self) -> dict[str, Any]:
119119
"name": feature.name,
120120
"filename": feature.filename,
121121
"rel_filename": feature.rel_filename,
122+
"language": feature.language,
122123
"line_number": feature.line_number,
123124
"description": feature.description,
124125
"tags": sorted(feature.tags),

src/pytest_bdd/types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@
1212
THEN: Literal["then"] = "then"
1313

1414
STEP_TYPES = (GIVEN, WHEN, THEN)
15+
16+
STEP_TYPE_BY_PARSER_KEYWORD = {
17+
"Context": GIVEN,
18+
"Action": WHEN,
19+
"Outcome": THEN,
20+
}

tests/feature/test_cucumber_json.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def test_passing_outline():
225225
],
226226
"id": os.path.join("test_step_trace0", "test.feature"),
227227
"keyword": "Feature",
228+
"language": "en",
228229
"line": 2,
229230
"name": "One passing scenario, one failing scenario",
230231
"tags": [{"name": "feature-tag", "line": 1}],

tests/feature/test_report.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def _(cucumbers, left):
107107
"feature": {
108108
"description": "",
109109
"keyword": "Feature",
110+
"language": "en",
110111
"filename": str(feature),
111112
"line_number": 2,
112113
"name": "One passing scenario, one failing scenario",
@@ -144,6 +145,7 @@ def _(cucumbers, left):
144145
"feature": {
145146
"description": "",
146147
"keyword": "Feature",
148+
"language": "en",
147149
"filename": str(feature),
148150
"line_number": 2,
149151
"name": "One passing scenario, one failing scenario",
@@ -180,6 +182,7 @@ def _(cucumbers, left):
180182
"feature": {
181183
"description": "",
182184
"keyword": "Feature",
185+
"language": "en",
183186
"filename": str(feature),
184187
"line_number": 2,
185188
"name": "One passing scenario, one failing scenario",
@@ -224,6 +227,7 @@ def _(cucumbers, left):
224227
"feature": {
225228
"description": "",
226229
"keyword": "Feature",
230+
"language": "en",
227231
"filename": str(feature),
228232
"line_number": 2,
229233
"name": "One passing scenario, one failing scenario",

tests/feature/test_scenario.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import textwrap
44

5+
from pytest_bdd.utils import collect_dumped_objects
6+
57

68
def test_scenario_not_found(pytester, pytest_params):
79
"""Test the situation when scenario is not found."""
@@ -195,3 +197,85 @@ def _():
195197
)
196198
result = pytester.runpytest()
197199
result.assert_outcomes(passed=2)
200+
201+
202+
def test_multilanguage_support(pytester):
203+
"""Test multilanguage support."""
204+
pytester.makefile(
205+
".feature",
206+
simple="""
207+
# language: it
208+
209+
Funzionalità: Funzionalità semplice
210+
211+
Contesto:
212+
Dato che uso uno step nel contesto
213+
Allora va tutto bene
214+
215+
Scenario: Scenario semplice
216+
Dato che uso uno step con "Dato"
217+
E che uso uno step con "E"
218+
Ma che uso uno step con "Ma"
219+
* che uso uno step con "*"
220+
Allora va tutto bene
221+
222+
Schema dello scenario: Scenario con schema
223+
Dato che uso uno step con "<nome esempio>"
224+
Allora va tutto bene
225+
226+
Esempi:
227+
| nome esempio |
228+
| esempio 1 |
229+
| esempio 2 |
230+
""",
231+
)
232+
pytester.makepyfile(
233+
"""
234+
from pytest_bdd import scenario, given, then, parsers
235+
from pytest_bdd.utils import dump_obj
236+
237+
@scenario("simple.feature", "Scenario semplice")
238+
def test_scenario_semplice():
239+
pass
240+
241+
@scenario("simple.feature", "Scenario con schema")
242+
def test_scenario_con_schema():
243+
pass
244+
245+
@given("che uso uno step nel contesto")
246+
def _():
247+
return dump_obj(("given", "che uso uno step nel contesto"))
248+
249+
@given(parsers.parse('che uso uno step con "{step_name}"'))
250+
def _(step_name):
251+
return dump_obj(("given", "che uso uno step con ", step_name))
252+
253+
@then("va tutto bene")
254+
def _():
255+
dump_obj(("then", "va tutto bene"))
256+
"""
257+
)
258+
result = pytester.runpytest("-s")
259+
result.assert_outcomes(passed=3)
260+
261+
assert collect_dumped_objects(result) == [
262+
# 1st scenario
263+
("given", "che uso uno step nel contesto"),
264+
("then", "va tutto bene"),
265+
("given", "che uso uno step con ", "Dato"),
266+
("given", "che uso uno step con ", "E"),
267+
("given", "che uso uno step con ", "Ma"),
268+
("given", "che uso uno step con ", "*"),
269+
("then", "va tutto bene"),
270+
# 2nd scenario
271+
# 1st example
272+
("given", "che uso uno step nel contesto"),
273+
("then", "va tutto bene"),
274+
("given", "che uso uno step con ", "esempio 1"),
275+
("then", "va tutto bene"),
276+
# 2nd example
277+
("given", "che uso uno step nel contesto"),
278+
("then", "va tutto bene"),
279+
("given", "che uso uno step con ", "esempio 2"),
280+
("then", "va tutto bene"),
281+
]

tests/parser/test_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test_parser():
3636
tags=[],
3737
name="User login",
3838
description=" As a registered user\n I want to be able to log in\n So that I can access my account",
39+
language="en",
3940
children=[
4041
Child(
4142
background=Background(

0 commit comments

Comments
 (0)