Skip to content

Commit 7f6c20b

Browse files
committed
remove unasync
1 parent 72be7f2 commit 7f6c20b

File tree

5 files changed

+348
-39
lines changed

5 files changed

+348
-39
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["setuptools", "wheel", "unasync~=0.5.0"]
2+
requires = ["setuptools", "wheel"]
33
build-backend = "setuptools.build_meta"
44

55
[tool.black]

pytest_bdd/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""pytest-bdd public API."""
22

33
from pytest_bdd.steps import given, when, then
4-
from pytest_bdd.scenario import scenario, scenarios, async_scenario, async_scenarios
4+
from pytest_bdd.scenario import scenario, scenarios
55

66
__version__ = "4.0.2"
77

8-
__all__ = ["given", "when", "then", "scenario", "scenarios", "async_scenario", "async_scenarios"]
8+
__all__ = ["given", "when", "then", "scenario", "scenarios"]

pytest_bdd/scenario.py

+339-21
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,340 @@
1-
__all__ = [
2-
"async_scenario",
3-
"async_scenarios",
4-
"scenario",
5-
"scenarios",
6-
"find_argumented_step_fixture_name",
7-
"make_python_docstring",
8-
"make_python_name",
9-
"make_string_literal",
10-
"get_python_name_generator",
11-
]
12-
13-
from ._async.scenario import scenario as async_scenario, scenarios as async_scenarios
14-
from ._sync.scenario import (
15-
scenario,
16-
scenarios,
17-
find_argumented_step_fixture_name,
18-
make_python_docstring,
19-
make_python_name,
20-
make_string_literal,
21-
get_python_name_generator,
1+
"""Scenario implementation.
2+
3+
The pytest will collect the test case and the steps will be executed
4+
line by line.
5+
6+
Example:
7+
8+
test_publish_article = scenario(
9+
feature_name="publish_article.feature",
10+
scenario_name="Publishing the article",
2211
)
12+
"""
13+
import contextlib
14+
import collections
15+
import os
16+
import re
17+
18+
import pytest
19+
from _pytest.fixtures import FixtureLookupError
20+
21+
from . import exceptions
22+
from .feature import get_feature, get_features
23+
from .steps import get_step_fixture_name, inject_fixture
24+
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
25+
26+
PYTHON_REPLACE_REGEX = re.compile(r"\W")
27+
ALPHA_REGEX = re.compile(r"^\d+_*")
28+
29+
30+
def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None):
31+
"""Find argumented step fixture name."""
32+
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
33+
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
34+
for fixturedef in fixturedefs:
35+
parser = getattr(fixturedef.func, "parser", None)
36+
if parser is None:
37+
continue
38+
match = parser.is_matching(name)
39+
if not match:
40+
continue
41+
42+
converters = getattr(fixturedef.func, "converters", {})
43+
for arg, value in parser.parse_arguments(name).items():
44+
if arg in converters:
45+
value = converters[arg](value)
46+
if request:
47+
inject_fixture(request, arg, value)
48+
parser_name = get_step_fixture_name(parser.name, type_)
49+
if request:
50+
try:
51+
request.getfixturevalue(parser_name)
52+
except FixtureLookupError:
53+
continue
54+
return parser_name
55+
56+
57+
def _find_step_function(request, step, scenario):
58+
"""Match the step defined by the regular expression pattern.
59+
60+
:param request: PyTest request object.
61+
:param step: Step.
62+
:param scenario: Scenario.
63+
64+
:return: Function of the step.
65+
:rtype: function
66+
"""
67+
name = step.name
68+
try:
69+
# Simple case where no parser is used for the step
70+
return request.getfixturevalue(get_step_fixture_name(name, step.type))
71+
except FixtureLookupError:
72+
try:
73+
# Could not find a fixture with the same name, let's see if there is a parser involved
74+
name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request)
75+
if name:
76+
return request.getfixturevalue(name)
77+
raise
78+
except FixtureLookupError:
79+
raise exceptions.StepDefinitionNotFoundError(
80+
f"Step definition is not found: {step}. "
81+
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
82+
)
83+
84+
85+
async def _execute_step_function(request, scenario, step, step_func, sync):
86+
"""Execute step function.
87+
88+
:param request: PyTest request.
89+
:param scenario: Scenario.
90+
:param step: Step.
91+
:param function step_func: Step function.
92+
:param example: Example table.
93+
"""
94+
kw = dict(request=request, feature=scenario.feature, scenario=scenario, step=step, step_func=step_func)
95+
96+
request.config.hook.pytest_bdd_before_step(**kw)
97+
98+
kw["step_func_args"] = {}
99+
try:
100+
# Get the step argument values.
101+
kwargs = {arg: request.getfixturevalue(arg) for arg in get_args(step_func)}
102+
kw["step_func_args"] = kwargs
103+
104+
request.config.hook.pytest_bdd_before_step_call(**kw)
105+
target_fixture = getattr(step_func, "target_fixture", None)
106+
# Execute the step.
107+
if sync:
108+
return_value = step_func(**kwargs)
109+
else:
110+
return_value = await step_func(**kwargs)
111+
if target_fixture:
112+
inject_fixture(request, target_fixture, return_value)
113+
114+
request.config.hook.pytest_bdd_after_step(**kw)
115+
except Exception as exception:
116+
request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
117+
raise
118+
119+
120+
async def _execute_scenario(feature, scenario, request, sync):
121+
"""Execute the scenario.
122+
123+
:param feature: Feature.
124+
:param scenario: Scenario.
125+
:param request: request.
126+
:param encoding: Encoding.
127+
"""
128+
request.config.hook.pytest_bdd_before_scenario(request=request, feature=feature, scenario=scenario)
129+
130+
try:
131+
# Execute scenario steps
132+
for step in scenario.steps:
133+
try:
134+
step_func = _find_step_function(request, step, scenario)
135+
except exceptions.StepDefinitionNotFoundError as exception:
136+
request.config.hook.pytest_bdd_step_func_lookup_error(
137+
request=request, feature=feature, scenario=scenario, step=step, exception=exception
138+
)
139+
raise
140+
await _execute_step_function(request, scenario, step, step_func, sync)
141+
finally:
142+
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
143+
144+
145+
FakeRequest = collections.namedtuple("FakeRequest", ["module"])
146+
147+
148+
def await_(fn, *args):
149+
v = fn(*args)
150+
with contextlib.closing(v.__await__()) as gen:
151+
try:
152+
gen.send(None)
153+
except StopIteration as e:
154+
return e.value
155+
else:
156+
raise RuntimeError("coro did not stop")
157+
158+
159+
def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, *, sync):
160+
# HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
161+
# when the decorator is misused.
162+
# Pytest inspect the signature to determine the required fixtures, and in that case it would look
163+
# for a fixture called "fn" that doesn't exist (if it exists then it's even worse).
164+
# It will error with a "fixture 'fn' not found" message instead.
165+
# We can avoid this hack by using a pytest hook and check for misuse instead.
166+
def decorator(*args):
167+
if not args:
168+
raise exceptions.ScenarioIsDecoratorOnly(
169+
"scenario function can only be used as a decorator. Refer to the documentation."
170+
)
171+
[fn] = args
172+
args = get_args(fn)
173+
function_args = list(args)
174+
for arg in scenario.get_example_params():
175+
if arg not in function_args:
176+
function_args.append(arg)
177+
178+
if sync:
179+
180+
@pytest.mark.usefixtures(*function_args)
181+
def scenario_wrapper(request):
182+
await_(_execute_scenario, feature, scenario, request, sync)
183+
return fn(*[request.getfixturevalue(arg) for arg in args])
184+
185+
else:
186+
187+
@pytest.mark.usefixtures(*function_args)
188+
async def scenario_wrapper(request):
189+
await _execute_scenario(feature, scenario, request, sync)
190+
return await fn(*[request.getfixturevalue(arg) for arg in args])
191+
192+
for param_set in scenario.get_params():
193+
if param_set:
194+
scenario_wrapper = pytest.mark.parametrize(*param_set)(scenario_wrapper)
195+
for tag in scenario.tags.union(feature.tags):
196+
config = CONFIG_STACK[-1]
197+
config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper)
198+
199+
scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}"
200+
scenario_wrapper.__scenario__ = scenario
201+
scenario.test_function = scenario_wrapper
202+
return scenario_wrapper
203+
204+
return decorator
205+
206+
207+
def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None, sync=True):
208+
"""Scenario decorator.
209+
210+
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
211+
:param str scenario_name: Scenario name.
212+
:param str encoding: Feature file encoding.
213+
:param dict example_converters: optional `dict` of example converter function, where key is the name of the
214+
example parameter, and value is the converter function.
215+
"""
216+
217+
scenario_name = str(scenario_name)
218+
caller_module_path = get_caller_module_path()
219+
220+
# Get the feature
221+
if features_base_dir is None:
222+
features_base_dir = get_features_base_dir(caller_module_path)
223+
feature = get_feature(features_base_dir, feature_name, encoding=encoding)
224+
225+
# Get the scenario
226+
try:
227+
scenario = feature.scenarios[scenario_name]
228+
except KeyError:
229+
feature_name = feature.name or "[Empty]"
230+
raise exceptions.ScenarioNotFound(
231+
f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.'
232+
)
233+
234+
scenario.example_converters = example_converters
235+
236+
# Validate the scenario
237+
scenario.validate()
238+
239+
return _get_scenario_decorator(
240+
feature=feature, feature_name=feature_name, scenario=scenario, scenario_name=scenario_name, sync=sync
241+
)
242+
243+
244+
def get_features_base_dir(caller_module_path):
245+
default_base_dir = os.path.dirname(caller_module_path)
246+
return get_from_ini("bdd_features_base_dir", default_base_dir)
247+
248+
249+
def get_from_ini(key, default):
250+
"""Get value from ini config. Return default if value has not been set.
251+
252+
Use if the default value is dynamic. Otherwise set default on addini call.
253+
"""
254+
config = CONFIG_STACK[-1]
255+
value = config.getini(key)
256+
return value if value != "" else default
257+
258+
259+
def make_python_name(string):
260+
"""Make python attribute name out of a given string."""
261+
string = re.sub(PYTHON_REPLACE_REGEX, "", string.replace(" ", "_"))
262+
return re.sub(ALPHA_REGEX, "", string).lower()
263+
264+
265+
def make_python_docstring(string):
266+
"""Make a python docstring literal out of a given string."""
267+
return '"""{}."""'.format(string.replace('"""', '\\"\\"\\"'))
268+
269+
270+
def make_string_literal(string):
271+
"""Make python string literal out of a given string."""
272+
return "'{}'".format(string.replace("'", "\\'"))
273+
274+
275+
def get_python_name_generator(name):
276+
"""Generate a sequence of suitable python names out of given arbitrary string name."""
277+
python_name = make_python_name(name)
278+
suffix = ""
279+
index = 0
280+
281+
def get_name():
282+
return f"test_{python_name}{suffix}"
283+
284+
while True:
285+
yield get_name()
286+
index += 1
287+
suffix = f"_{index}"
288+
289+
290+
def scenarios(*feature_paths, sync=True, **kwargs):
291+
"""Parse features from the paths and put all found scenarios in the caller module.
292+
293+
:param *feature_paths: feature file paths to use for scenarios
294+
"""
295+
caller_locals = get_caller_module_locals()
296+
caller_path = get_caller_module_path()
297+
298+
features_base_dir = kwargs.get("features_base_dir")
299+
if features_base_dir is None:
300+
features_base_dir = get_features_base_dir(caller_path)
301+
302+
abs_feature_paths = []
303+
for path in feature_paths:
304+
if not os.path.isabs(path):
305+
path = os.path.abspath(os.path.join(features_base_dir, path))
306+
abs_feature_paths.append(path)
307+
found = False
308+
309+
module_scenarios = frozenset(
310+
(attr.__scenario__.feature.filename, attr.__scenario__.name)
311+
for name, attr in caller_locals.items()
312+
if hasattr(attr, "__scenario__")
313+
)
314+
315+
for feature in get_features(abs_feature_paths):
316+
for scenario_name, scenario_object in feature.scenarios.items():
317+
# skip already bound scenarios
318+
if (scenario_object.feature.filename, scenario_name) not in module_scenarios:
319+
320+
decorator = scenario(feature.filename, scenario_name, sync=sync, **kwargs)
321+
if sync:
322+
323+
@decorator
324+
def _scenario():
325+
pass # pragma: no cover
326+
327+
else:
328+
329+
@decorator
330+
async def _scenario():
331+
pass # pragma: no cover
332+
333+
for test_name in get_python_name_generator(scenario_name):
334+
if test_name not in caller_locals:
335+
# found an unique test name
336+
caller_locals[test_name] = _scenario
337+
break
338+
found = True
339+
if not found:
340+
raise exceptions.NoScenariosFound(abs_feature_paths)

setup.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
import unasync
21
from setuptools import setup
32

4-
setup(
5-
cmdclass={
6-
"build_py": unasync.cmdclass_build_py(
7-
rules=[
8-
unasync.Rule("/pytest_bdd/_async/", "/pytest_bdd/_sync/"),
9-
]
10-
)
11-
}
12-
)
3+
setup()

0 commit comments

Comments
 (0)