Skip to content

Commit 3402450

Browse files
authored
Merge pull request #717 from jsa34/docstrings
Docstrings
2 parents 336f6e1 + 1278cd9 commit 3402450

File tree

8 files changed

+372
-135
lines changed

8 files changed

+372
-135
lines changed

CHANGES.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ Unreleased
1010
- Update documentation to clarify that `--gherkin-terminal-reporter` needs to be used with `-v` or `-vv`.
1111
- Drop compatibility with pytest < 7.0.0.
1212
- Continuation of steps using asterisks instead of And/But supported.
13-
- Added `datatable` argument for steps that contain a datatable.
13+
- Added `datatable` argument for steps that contain a datatable (see `Data Tables <https://cucumber.io/docs/gherkin/reference/#data-tables>`).
14+
- Added `docstring` argument for steps that contain a docstring (see `Doc Strings <https://cucumber.io/docs/gherkin/reference/#doc-strings>`).
15+
- Multiline strings no longer match name based on multiple lines - only on the actual step text on the step line.
1416

1517
8.0.0b1
1618
----------

README.rst

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -428,53 +428,6 @@ A common use case is when we have to assert the outcome of an HTTP request:
428428
Then the request should be successful
429429
430430
431-
Multiline steps
432-
---------------
433-
434-
As Gherkin, pytest-bdd supports multiline steps
435-
(a.k.a. `Doc Strings <https://cucumber.io/docs/gherkin/reference/#doc-strings>`_).
436-
437-
.. code-block:: gherkin
438-
439-
Feature: Multiline steps
440-
Scenario: Multiline step using sub indentation
441-
Given I have a step with:
442-
"""
443-
Some
444-
Extra
445-
Lines
446-
"""
447-
Then the text should be parsed with correct indentation
448-
449-
A step is considered as a multiline one, if the **next** line(s) after its first line is encapsulated by
450-
triple quotes. The step name is then simply extended by adding further lines inside the triple quotes.
451-
In the example above, the Given step name will be:
452-
453-
.. code-block:: python
454-
455-
'I have a step with:\nSome\nExtra\nLines'
456-
457-
You can of course register a step using the full name (including the newlines), but it seems more practical to use
458-
step arguments and capture lines after first line (or some subset of them) into the argument:
459-
460-
.. code-block:: python
461-
462-
from pytest_bdd import given, then, scenario, parsers
463-
464-
465-
scenarios("multiline.feature")
466-
467-
468-
@given(parsers.parse("I have a step with:\n{content}"), target_fixture="text")
469-
def given_text(content):
470-
return content
471-
472-
473-
@then("the text should be parsed with correct indentation")
474-
def text_should_be_correct(text):
475-
assert text == "Some\nExtra\nLines"
476-
477-
478431
Scenarios shortcut
479432
------------------
480433

@@ -561,8 +514,8 @@ Example:
561514
assert cucumbers["start"] - cucumbers["eat"] == left
562515
563516
564-
Step Definitions and Accessing the Datatable
565-
--------------------------------------------
517+
Datatables
518+
----------
566519

567520
The ``datatable`` argument allows you to utilise data tables defined in your Gherkin scenarios
568521
directly within your test functions. This is particularly useful for scenarios that require tabular data as input,
@@ -653,6 +606,89 @@ Full example:
653606
assert users_have_correct_permissions(users, expected_permissions)
654607
655608
609+
Docstrings
610+
----------
611+
612+
The `docstring` argument allows you to access the Gherkin docstring defined in your steps as a multiline string.
613+
The content of the docstring is passed as a single string, with each line separated by `\\n`.
614+
Leading indentation are stripped.
615+
616+
For example, the Gherkin docstring:
617+
618+
619+
.. code-block:: gherkin
620+
621+
"""
622+
This is a sample docstring.
623+
It spans multiple lines.
624+
"""
625+
626+
627+
Will be returned as:
628+
629+
.. code-block:: python
630+
631+
"This is a sample docstring.\nIt spans multiple lines."
632+
633+
634+
Full example:
635+
636+
.. code-block:: gherkin
637+
638+
Feature: Docstring
639+
640+
Scenario: Step with docstrings
641+
Given some steps will have docstrings
642+
643+
Then a step has a docstring
644+
"""
645+
This is a docstring
646+
on two lines
647+
"""
648+
649+
And a step provides a docstring with lower indentation
650+
"""
651+
This is a docstring
652+
"""
653+
654+
And this step has no docstring
655+
656+
And this step has a greater indentation
657+
"""
658+
This is a docstring
659+
"""
660+
661+
And this step has no docstring
662+
663+
.. code-block:: python
664+
665+
from pytest_bdd import given, then
666+
667+
668+
@given("some steps will have docstrings")
669+
def _():
670+
pass
671+
672+
@then("a step has a docstring")
673+
def _(docstring):
674+
assert docstring == "This is a docstring\non two lines"
675+
676+
@then("a step provides a docstring with lower indentation")
677+
def _(docstring):
678+
assert docstring == "This is a docstring"
679+
680+
@then("this step has a greater indentation")
681+
def _(docstring):
682+
assert docstring == "This is a docstring"
683+
684+
@then("this step has no docstring")
685+
def _():
686+
pass
687+
688+
689+
.. note:: The ``docstring`` argument can only be used for steps that have an associated docstring.
690+
Otherwise, an error will be thrown.
691+
656692
Organizing your scenarios
657693
-------------------------
658694

src/pytest_bdd/parser.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
173173
line_number=step.line_number,
174174
keyword=step.keyword,
175175
datatable=step.datatable,
176+
docstring=step.docstring,
176177
)
177178
for step in self._steps
178179
]
@@ -228,13 +229,21 @@ class Step:
228229
line_number: int
229230
indent: int
230231
keyword: str
232+
docstring: str | None = None
231233
datatable: DataTable | None = None
232234
failed: bool = field(init=False, default=False)
233235
scenario: ScenarioTemplate | None = field(init=False, default=None)
234236
background: Background | None = field(init=False, default=None)
235237

236238
def __init__(
237-
self, name: str, type: str, indent: int, line_number: int, keyword: str, datatable: DataTable | None = None
239+
self,
240+
name: str,
241+
type: str,
242+
indent: int,
243+
line_number: int,
244+
keyword: str,
245+
datatable: DataTable | None = None,
246+
docstring: str | None = None,
238247
) -> None:
239248
"""Initialize a step.
240249
@@ -251,6 +260,7 @@ def __init__(
251260
self.line_number = line_number
252261
self.keyword = keyword
253262
self.datatable = datatable
263+
self.docstring = docstring
254264

255265
def __str__(self) -> str:
256266
"""Return a string representation of the step.
@@ -347,12 +357,6 @@ def parse_steps(self, steps_data: list[GherkinStep]) -> list[Step]:
347357
List[Step]: A list of Step objects.
348358
"""
349359

350-
def get_step_content(_gherkin_step: GherkinStep) -> str:
351-
step_name = strip_comments(_gherkin_step.text)
352-
if _gherkin_step.docstring:
353-
step_name = f"{step_name}\n{_gherkin_step.docstring.content}"
354-
return step_name
355-
356360
if not steps_data:
357361
return []
358362

@@ -361,25 +365,25 @@ def get_step_content(_gherkin_step: GherkinStep) -> str:
361365
raise StepError(
362366
message=f"First step in a scenario or background must start with 'Given', 'When' or 'Then', but got {first_step.keyword}.",
363367
line=first_step.location.line,
364-
line_content=get_step_content(first_step),
368+
line_content=first_step.text,
365369
filename=self.abs_filename,
366370
)
367371

368372
steps = []
369373
current_type = first_step.keyword.lower()
370374
for step in steps_data:
371-
name = get_step_content(step)
372375
keyword = step.keyword.lower()
373376
if keyword in STEP_TYPES:
374377
current_type = keyword
375378
steps.append(
376379
Step(
377-
name=name,
380+
name=strip_comments(step.text),
378381
type=current_type,
379382
indent=step.location.column - 1,
380383
line_number=step.location.line,
381384
keyword=step.keyword.title(),
382385
datatable=step.datatable,
386+
docstring=step.docstring.content if step.docstring else None,
383387
)
384388
)
385389
return steps

src/pytest_bdd/scenario.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,19 @@ def _execute_step_function(
199199
f"Unexpected `NoneType` returned from " f"parse_arguments(...) in parser: {context.parser!r}"
200200
)
201201

202-
if step.datatable is not None:
203-
kwargs["datatable"] = step.datatable.raw()
204-
205202
for arg, value in parsed_args.items():
206203
if arg in converters:
207204
value = converters[arg](value)
208205
kwargs[arg] = value
209206

207+
if step.datatable is not None:
208+
kwargs["datatable"] = step.datatable.raw()
209+
210+
if step.docstring is not None:
211+
kwargs["docstring"] = step.docstring
212+
210213
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
214+
211215
kw["step_func_args"] = kwargs
212216

213217
request.config.hook.pytest_bdd_before_step_call(**kw)

tests/datatable/test_datatable.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ def test_datatable():
9797
]
9898

9999

100-
def test_steps_with_missing_datatables(pytester):
100+
def test_datatable_argument_in_step_impl_is_optional(pytester):
101101
pytester.makefile(
102102
".feature",
103-
missing_datatable=textwrap.dedent(
103+
optional_arg_datatable=textwrap.dedent(
104104
"""\
105105
Feature: Missing data table
106106
@@ -110,7 +110,7 @@ def test_steps_with_missing_datatables(pytester):
110110
| John | [email protected] | 30 |
111111
| Alice | [email protected] | 25 |
112112
113-
When this step has no data table but tries to use the datatable fixture
113+
When this step has no data table but tries to use the datatable argument
114114
Then an error is thrown
115115
"""
116116
),
@@ -126,7 +126,7 @@ def _(datatable):
126126
print(datatable)
127127
128128
129-
@when("this step has no data table but tries to use the datatable fixture")
129+
@when("this step has no data table but tries to use the datatable argument")
130130
def _(datatable):
131131
print(datatable)
132132
@@ -139,17 +139,74 @@ def _(datatable):
139139
)
140140
)
141141

142+
pytester.makepyfile(
143+
textwrap.dedent(
144+
"""\
145+
from pytest_bdd import scenarios
146+
147+
scenarios("optional_arg_datatable.feature")
148+
"""
149+
)
150+
)
151+
result = pytester.runpytest("-s")
152+
result.assert_outcomes(failed=1)
153+
result.stdout.fnmatch_lines(["*fixture 'datatable' not found*"])
154+
155+
156+
def test_steps_with_datatable_missing_argument_in_step(pytester):
157+
pytester.makefile(
158+
".feature",
159+
missing_datatable_arg=textwrap.dedent(
160+
"""\
161+
Feature: Missing datatable
162+
163+
Scenario: Datatable arg is missing for a step definition
164+
Given this step has a datatable
165+
| name | email | age |
166+
| John | [email protected] | 30 |
167+
168+
When this step has a datatable but no datatable argument
169+
| name | email | age |
170+
| John | [email protected] | 30 |
171+
172+
Then the test passes
173+
"""
174+
),
175+
)
176+
pytester.makeconftest(
177+
textwrap.dedent(
178+
"""\
179+
from pytest_bdd import given, when, then
180+
181+
182+
@given("this step has a datatable")
183+
def _(datatable):
184+
print(datatable)
185+
186+
187+
@when("this step has a datatable but no datatable argument")
188+
def _():
189+
pass
190+
191+
192+
@then("the test passes")
193+
def _():
194+
pass
195+
196+
"""
197+
)
198+
)
199+
142200
pytester.makepyfile(
143201
textwrap.dedent(
144202
"""\
145203
from pytest_bdd import scenario
146204
147-
@scenario("missing_datatable.feature", "Data table is missing for a step")
205+
@scenario("missing_datatable_arg.feature", "Datatable arg is missing for a step definition")
148206
def test_datatable():
149207
pass
150208
"""
151209
)
152210
)
153211
result = pytester.runpytest("-s")
154-
result.assert_outcomes(failed=1)
155-
result.stdout.fnmatch_lines(["*fixture 'datatable' not found*"])
212+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)