Skip to content

Commit e0713d6

Browse files
committed
Render docstrings and datatable cells with example table entries, just like step names currently are.
1 parent 93e446e commit e0713d6

File tree

2 files changed

+86
-16
lines changed

2 files changed

+86
-16
lines changed

src/pytest_bdd/parser.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@
2323
STEP_PARAM_RE = re.compile(r"<(.+?)>")
2424

2525

26+
def render_string(input_string: str, render_context: Mapping[str, Any]) -> str:
27+
"""
28+
Render the string with the given context,
29+
but avoid replacing text inside angle brackets if context is missing.
30+
31+
Args:
32+
input_string (str): The string for which to render/replace params.
33+
render_context (Mapping[str, Any]): The context for rendering the string.
34+
35+
Returns:
36+
str: The rendered string with parameters replaced only if they exist in the context.
37+
"""
38+
39+
def replacer(m: re.Match) -> str:
40+
varname = m.group(1)
41+
# If the context contains the variable, replace it. Otherwise, leave it unchanged.
42+
return str(render_context.get(varname, f"<{varname}>"))
43+
44+
return STEP_PARAM_RE.sub(replacer, input_string)
45+
46+
2647
def get_tag_names(tag_data: list[GherkinTag]) -> set[str]:
2748
"""Extract tag names from tag data.
2849
@@ -191,13 +212,13 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
191212
"""
192213
scenario_steps = [
193214
Step(
194-
name=step.render(context),
215+
name=step.render_step_name(context),
195216
type=step.type,
196217
indent=step.indent,
197218
line_number=step.line_number,
198219
keyword=step.keyword,
199-
datatable=step.datatable,
200-
docstring=step.docstring,
220+
datatable=step.render_datatable(context),
221+
docstring=step.render_docstring(context),
201222
)
202223
for step in self._steps
203224
]
@@ -308,22 +329,50 @@ def params(self) -> tuple[str, ...]:
308329
"""
309330
return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
310331

311-
def render(self, context: Mapping[str, Any]) -> str:
312-
"""Render the step name with the given context, but avoid replacing text inside angle brackets if context is missing.
332+
def render_step_name(self, context: Mapping[str, Any]) -> str:
333+
"""
334+
Render the step name with the given context,
335+
but avoid replacing text inside angle brackets if context is missing.
313336
314337
Args:
315338
context (Mapping[str, Any]): The context for rendering the step name.
316339
317340
Returns:
318341
str: The rendered step name with parameters replaced only if they exist in the context.
319342
"""
343+
return render_string(self.name, context)
344+
345+
def render_datatable(self, context: Mapping[str, Any]) -> datatable | None:
346+
"""
347+
Render the datatable with the given context,
348+
but avoid replacing text inside angle brackets if context is missing.
349+
350+
Args:
351+
context (Mapping[str, Any]): The context for rendering the datatable.
352+
353+
Returns:
354+
datatable: The rendered datatable with parameters replaced only if they exist in the context.
355+
"""
356+
if self.datatable:
357+
rendered_datatable = self.datatable
358+
for row in rendered_datatable.rows:
359+
for cell in row.cells:
360+
cell.value = render_string(cell.value, context)
361+
return rendered_datatable
362+
return None
363+
364+
def render_docstring(self, context: Mapping[str, Any]) -> str | None:
365+
"""
366+
Render the docstring with the given context,
367+
but avoid replacing text inside angle brackets if context is missing.
320368
321-
def replacer(m: re.Match) -> str:
322-
varname = m.group(1)
323-
# If the context contains the variable, replace it. Otherwise, leave it unchanged.
324-
return str(context.get(varname, f"<{varname}>"))
369+
Args:
370+
context (Mapping[str, Any]): The context for rendering the docstring.
325371
326-
return STEP_PARAM_RE.sub(replacer, self.name)
372+
Returns:
373+
str: The rendered docstring with parameters replaced only if they exist in the context.
374+
"""
375+
return render_string(self.docstring, context) if self.docstring else None
327376

328377

329378
@dataclass(eq=False)

tests/feature/test_scenario.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,24 +161,32 @@ def test_angular_brackets_are_not_parsed(pytester):
161161
"""
162162
pytester.makefile(
163163
".feature",
164-
simple="""
164+
simple='''
165165
Feature: Simple feature
166166
Scenario: Simple scenario
167167
Given I have a <tag>
168168
Then pass
169169
170170
Scenario Outline: Outlined scenario
171171
Given I have a templated <foo>
172+
When I have a templated datatable
173+
| <data> |
174+
| example |
175+
And I have a templated docstring
176+
"""
177+
This is a <doc>
178+
"""
172179
Then pass
173180
174181
Examples:
175-
| foo |
176-
| bar |
177-
""",
182+
| foo | data | doc |
183+
| bar | table | string |
184+
''',
178185
)
179186
pytester.makepyfile(
180187
"""
181-
from pytest_bdd import scenarios, given, then, parsers
188+
from pytest_bdd import scenarios, given, when, then, parsers
189+
from pytest_bdd.utils import dump_obj
182190
183191
scenarios("simple.feature")
184192
@@ -190,14 +198,27 @@ def _():
190198
def _(foo):
191199
return "foo"
192200
201+
@when("I have a templated datatable")
202+
def _(datatable):
203+
return dump_obj(("datatable", datatable))
204+
205+
@when("I have a templated docstring")
206+
def _(docstring):
207+
return dump_obj(("docstring", docstring))
208+
193209
@then("pass")
194210
def _():
195211
pass
196212
"""
197213
)
198-
result = pytester.runpytest()
214+
result = pytester.runpytest("-s")
199215
result.assert_outcomes(passed=2)
200216

217+
assert collect_dumped_objects(result) == [
218+
("datatable", [["table"], ["example"]]),
219+
("docstring", "This is a string"),
220+
]
221+
201222

202223
def test_multilanguage_support(pytester):
203224
"""Test multilanguage support."""

0 commit comments

Comments
 (0)