Skip to content

Commit ec28d7c

Browse files
committed
Add async_ decorators.
1 parent 6a301ce commit ec28d7c

File tree

4 files changed

+106
-19
lines changed

4 files changed

+106
-19
lines changed

src/pytest_bdd/asyncio.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from pytest_bdd.steps import async_given, async_then, async_when
2+
3+
__all__ = ["async_given", "async_when", "async_then"]

src/pytest_bdd/steps.py

+77-7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def given(
8383
converters: dict[str, Callable] | None = None,
8484
target_fixture: str | None = None,
8585
stacklevel: int = 1,
86+
is_async: bool = False,
8687
) -> Callable:
8788
"""Given step decorator.
8889
@@ -95,14 +96,36 @@ def given(
9596
9697
:return: Decorator function for the step.
9798
"""
98-
return step(name, GIVEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
99+
return step(
100+
name, GIVEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel, is_async=is_async
101+
)
102+
103+
104+
def async_given(
105+
name: str | StepParser,
106+
converters: dict[str, Callable] | None = None,
107+
target_fixture: str | None = None,
108+
stacklevel: int = 1,
109+
) -> Callable:
110+
"""Async Given step decorator.
111+
112+
:param name: Step name or a parser object.
113+
:param converters: Optional `dict` of the argument or parameter converters in form
114+
{<param_name>: <converter function>}.
115+
:param target_fixture: Target fixture name to replace by steps definition function.
116+
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
117+
118+
:return: Decorator function for the step.
119+
"""
120+
return given(name, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel, is_async=True)
99121

100122

101123
def when(
102124
name: str | StepParser,
103125
converters: dict[str, Callable] | None = None,
104126
target_fixture: str | None = None,
105127
stacklevel: int = 1,
128+
is_async: bool = False,
106129
) -> Callable:
107130
"""When step decorator.
108131
@@ -115,14 +138,59 @@ def when(
115138
116139
:return: Decorator function for the step.
117140
"""
118-
return step(name, WHEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
141+
return step(
142+
name, WHEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel, is_async=is_async
143+
)
144+
145+
146+
def async_when(
147+
name: str | StepParser,
148+
converters: dict[str, Callable] | None = None,
149+
target_fixture: str | None = None,
150+
stacklevel: int = 1,
151+
) -> Callable:
152+
"""When step decorator.
153+
154+
:param name: Step name or a parser object.
155+
:param converters: Optional `dict` of the argument or parameter converters in form
156+
{<param_name>: <converter function>}.
157+
:param target_fixture: Target fixture name to replace by steps definition function.
158+
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
159+
:param is_async: True if the step is asynchronous. (Default: False)
160+
161+
:return: Decorator function for the step.
162+
"""
163+
return when(name, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel, is_async=True)
119164

120165

121166
def then(
122167
name: str | StepParser,
123168
converters: dict[str, Callable] | None = None,
124169
target_fixture: str | None = None,
125170
stacklevel: int = 1,
171+
is_async: bool = False,
172+
) -> Callable:
173+
"""Then step decorator.
174+
175+
:param name: Step name or a parser object.
176+
:param converters: Optional `dict` of the argument or parameter converters in form
177+
{<param_name>: <converter function>}.
178+
:param target_fixture: Target fixture name to replace by steps definition function.
179+
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.
180+
:param is_async: True if the step is asynchronous. (Default: False)
181+
182+
:return: Decorator function for the step.
183+
"""
184+
return step(
185+
name, THEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel, is_async=is_async
186+
)
187+
188+
189+
def async_then(
190+
name: str | StepParser,
191+
converters: dict[str, Callable] | None = None,
192+
target_fixture: str | None = None,
193+
stacklevel: int = 1,
126194
) -> Callable:
127195
"""Then step decorator.
128196
@@ -135,7 +203,7 @@ def then(
135203
136204
:return: Decorator function for the step.
137205
"""
138-
return step(name, THEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
206+
return step(name, THEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel, is_async=True)
139207

140208

141209
def step(
@@ -144,6 +212,7 @@ def step(
144212
converters: dict[str, Callable] | None = None,
145213
target_fixture: str | None = None,
146214
stacklevel: int = 1,
215+
is_async: bool = False,
147216
) -> Callable[[TCallable], TCallable]:
148217
"""Generic step decorator.
149218
@@ -168,10 +237,11 @@ def step(
168237
def decorator(func: TCallable) -> TCallable:
169238
parser = get_parser(name)
170239

171-
if inspect.isasyncgenfunction(func):
172-
func = wrap_asyncgen(func)
173-
elif inspect.iscoroutinefunction(func):
174-
func = wrap_coroutine(func)
240+
if is_async:
241+
if inspect.isasyncgenfunction(func):
242+
func = wrap_asyncgen(func)
243+
elif inspect.iscoroutinefunction(func):
244+
func = wrap_coroutine(func)
175245

176246
context = StepFunctionContext(
177247
type=type_,

tests/feature/test_async_steps.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,44 @@ def test_steps(pytester):
2525
pytester.makepyfile(
2626
textwrap.dedent(
2727
"""\
28-
from pytest_bdd import given, when, then, scenario
28+
from pytest_bdd import scenario
29+
from pytest_bdd.asyncio import async_given, async_when, async_then
2930
3031
@scenario("steps.feature", "Executed step by step")
3132
def test_steps():
3233
pass
3334
34-
@given('I have a foo fixture with value "foo"', target_fixture="foo")
35+
@async_given('I have a foo fixture with value "foo"', target_fixture="foo")
3536
async def _():
3637
return "foo"
3738
3839
39-
@given("there is a list", target_fixture="results")
40+
@async_given("there is a list", target_fixture="results")
4041
async def _():
4142
yield []
4243
4344
44-
@when("I append 1 to the list")
45+
@async_when("I append 1 to the list")
4546
async def _(results):
4647
results.append(1)
4748
4849
49-
@when("I append 2 to the list")
50+
@async_when("I append 2 to the list")
5051
async def _(results):
5152
results.append(2)
5253
5354
54-
@when("I append 3 to the list")
55+
@async_when("I append 3 to the list")
5556
async def _(results):
5657
results.append(3)
5758
5859
59-
@then('foo should have value "foo"')
60+
@async_then('foo should have value "foo"')
6061
async def _(foo):
6162
assert foo == "foo"
6263
6364
64-
@then("the list should be [1, 2, 3]")
65+
@async_then("the list should be [1, 2, 3]")
6566
async def _(results):
6667
assert results == [1, 2, 3]
6768

tests/steps/test_common.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@
55
import pytest
66

77
from pytest_bdd import given, parsers, then, when
8+
from pytest_bdd.asyncio import async_given, async_then, async_when
89
from pytest_bdd.utils import collect_dumped_objects
910

1011

11-
@pytest.mark.parametrize("step_fn, step_type", [(given, "given"), (when, "when"), (then, "then")])
12-
def test_given_when_then_delegate_to_step(step_fn: Callable[..., Any], step_type: str) -> None:
12+
@pytest.mark.parametrize(
13+
"step_fn, step_type, is_async",
14+
[
15+
(given, "given", False),
16+
(when, "when", False),
17+
(then, "then", False),
18+
(async_given, "given", True),
19+
(async_when, "when", True),
20+
(async_then, "then", True),
21+
],
22+
)
23+
def test_given_when_then_delegate_to_step(step_fn: Callable[..., Any], step_type: str, is_async: bool) -> None:
1324
"""Test that @given, @when, @then just delegate the work to @step(...).
1425
This way we don't have to repeat integration tests for each step decorator.
1526
"""
@@ -18,15 +29,17 @@ def test_given_when_then_delegate_to_step(step_fn: Callable[..., Any], step_type
1829
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
1930
step_fn("foo")
2031

21-
step_mock.assert_called_once_with("foo", type_=step_type, converters=None, target_fixture=None, stacklevel=1)
32+
step_mock.assert_called_once_with(
33+
"foo", type_=step_type, converters=None, target_fixture=None, stacklevel=1, is_async=is_async
34+
)
2235

2336
# Advanced usage: step parser, converters, target_fixture, ...
2437
with mock.patch("pytest_bdd.steps.step", autospec=True) as step_mock:
2538
parser = parsers.re(r"foo (?P<n>\d+)")
2639
step_fn(parser, converters={"n": int}, target_fixture="foo_n", stacklevel=3)
2740

2841
step_mock.assert_called_once_with(
29-
name=parser, type_=step_type, converters={"n": int}, target_fixture="foo_n", stacklevel=3
42+
name=parser, type_=step_type, converters={"n": int}, target_fixture="foo_n", stacklevel=3, is_async=is_async
3043
)
3144

3245

0 commit comments

Comments
 (0)