8000 feat: support coroutine functions as story step definitions · proofit404/stories@55cbfda · GitHub
[go: up one dir, main page]

Skip to content

Commit 55cbfda

Browse files
dry-python-botdry-python-bot
dry-python-bot
authored and
dry-python-bot
committed
feat: support coroutine functions as story step definitions
You can use async def syntax to define story steps. Story objects become available, so you can apply in your aiohttp views. All story steps should be either coroutines or functions. Most part of the work in this PR was done by @supadrupa and @thedrow
1 parent a4da2e3 commit 55cbfda

14 files changed

+271
-37
lines changed

src/_stories/collect.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# -*- coding: utf-8 -*-
2+
from _stories.compat import iscoroutinefunction
23
from _stories.exceptions import StoryDefinitionError
34

45

56
def collect_story(f):
7+
if iscoroutinefunction(f):
8+
raise StoryDefinitionError("Story should be a regular function")
69

710
calls = []
811

src/_stories/compat.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,11 @@ def indent(text, prefix): # type: ignore
5959
except ImportError:
6060
# Prettyprinter package is not installed.
6161
from pprint import pformat # noqa: F401
62+
63+
64+
try:
65+
from asyncio import iscoroutinefunction
66+
except ImportError:
67+
# We are on Python 2.7
68+
def iscoroutinefunction(func):
69+
return False

src/_stories/compat.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from typing import Callable
2+
3+
def iscoroutinefunction(func: Callable) -> bool: ...

src/_stories/execute/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# -*- coding: utf-8 -*-
2+
from _stories.compat import iscoroutinefunction
3+
from _stories.exceptions import StoryDefinitionError
4+
from _stories.execute import function
5+
6+
try:
7+
from _stories.execute import coroutine
8+
except SyntaxError:
9+
pass
10+
11+
12+
def get_executor(method, previous, cls_name, story_name):
13+
if iscoroutinefunction(method):
14+
executor = coroutine.execute
15+
other_kind = "function"
16+
else:
17+
executor = function.execute
18+
other_kind = "coroutine"
19+
20+
if previous is not executor and previous is not None:
21+
message = mixed_method_template.format(
22+
other_kind=other_kind,
23+
cls=cls_name,
24+
method=method.__name__,
25+
story_name=story_name,
26+
)
27+
raise StoryDefinitionError(message)
28+
return executor
29+
30+
31+
# Messages.
32+
33+
34+
mixed_method_template = """
35+
Coroutines and functions can not be used together in story definition.
36+
37+
This method should be a {other_kind}: {cls}.{method}
38+
39+
Story method: {cls}.{story_name}
40+
""".strip()

src/_stories/execute/__init__.pyi

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Callable
2+
from typing import Optional
3+
from typing import Union
4+
5+
from _stories.returned import Failure
6+
from _stories.returned import Result
7+
from _stories.returned import Skip
8+
from _stories.returned import Success
9+
10+
def get_executor(
11+
method: Callable[[object], Union[Result, Success, Failure, Skip]],
12+
previous: Optional[Callable],
13+
cls_name: str,
14+
story_name: str,
15+
) -> Callable: ...

src/_stories/execute/coroutine.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# -*- coding: utf-8 -*-
2+
from _stories.context import assign_namespace
3+
from _stories.marker import BeginningOfStory
4+
from _stories.marker import EndOfStory
5+
from _stories.returned import Failure
6+
from _stories.returned import Result
7+
from _stories.returned import Skip
8+
from _stories.returned import Success
9+
10+
11+
async def execute(runner, ctx, ns, lines, history, methods):
12+
__tracebackhide__ = True
13+
14+
skipped = 0
15+
16+
for method, contract, protocol in methods:
17+
18+
method_type = type(method)
19+
20+
if skipped > 0:
21+
if method_type is EndOfStory:
22+
skipped -= 1
23+
elif method_type is BeginningOfStory:
24+
skipped += 1
25+
continue
26+
27+
if method_type is BeginningOfStory:
28+
history.on_substory_start(method.story_name)
29+
try:
30+
contract.check_substory_call(ctx, ns)
31+
except Exception as error:
32+
history.on_error(error.__class__.__name__)
33+
raise
34+
continue
35+
36+
if method_type is EndOfStory:
37+
history.on_substory_end()
38+
continue
39+
40+
history.before_call(method.__name__)
41+
42+
try:
43+
result = await method(ctx)
44+
except Exception as error:
45+
history.on_error(error.__class__.__name__)
46+
raise
47+
48+
restype = type(result)
49+
if restype not in (Result, Success, Failure, Skip):
50+
raise AssertionError
51+
52+
if restype is Failure:
53+
try:
54+
protocol.check_return_statement(method, result.reason)
55+
except Exception as error:
56+
history.on_error(error.__class__.__name__)
57+
raise
58+
history.on_failure(result.reason)
59+
return runner.got_failure(ctx, method.__name__, result.reason)
60+
61+
if restype is Result:
62+
history.on_result(result.value)
63+
return runner.got_result(result.value)
64+
65+
if restype is Skip:
66+
history.on_skip()
67+
skipped = 1
68+
continue
69+
70+
try:
71+
kwargs = contract.check_success_statement(method, ctx, ns, result.kwargs)
72+
except Exception as error:
73+
history.on_error(error.__class__.__name__)
74+
raise
75+
76+
assign_namespace(ns, lines, method, kwargs)
77+
78+
return runner.finished()

src/_stories/execute/coroutine.pyi

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Any
2+
from typing import Callable
3+
from typing import List
4+
from typing import overload
5+
from typing import Tuple
6+
from typing import Union
7+
8+
from _stories.contract import NullContract
9+
from _stories.contract import SpecContract
10+
from _stories.failures import DisabledNullExecProtocol
11+
from _stories.failures import NotNullExecProtocol
12+
from _stories.failures import NullExecProtocol
13+
from _stories.history import History
14+
from _stories.marker import BeginningOfStory
15+
from _stories.marker import EndOfStory
16+
from _stories.returned import Failure
17+
from _stories.returned import Result
18+
from _stories.returned import Skip
19+
from _stories.returned import Success
20+
from _stories.run import Call
21+
from _stories.run import Run
22+
@overload
23+
async def execute(
24+
runner: Call,
25+
ctx: object,
26+
history: History,
27+
methods: List[
28+
Tuple[
29+
Union[
30+
BeginningOfStory,
31+
Callable[[object], Union[Result, Success, Failure, Skip]],
32+
EndOfStory,
33+
],
34+
Union[NullContract, SpecContract],
35+
Union[NullExecProtocol, DisabledNullExecProtocol, NotNullExecProtocol],
36+
],
37+
],
38+
) -> Any: ...
39+
@overload
40+
async def execute(
41+
runner: Run,
42+
ctx: object,
43+
history: History,
44+
methods: List[
45+
Tuple[
46+
Union[
47+
BeginningOfStory,
48+
Callable[[object], Union[Result, Success, Failure, Skip]],
49+
EndOfStory,
50+
],
51+
Union[NullContract, SpecContract],
52+
Union[NullExecProtocol, DisabledNullExecProtocol, NotNullExecProtocol],
53+
],
54+
],
55+
) -> object: ...

src/_stories/execute/function.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ def execute(runner, ctx, ns, lines, history, methods):
2424
skipped += 1
2525
continue
2626

27+
if method_type is BeginningOfStory:
28+
history.on_substory_start(method.story_name)
29+
try:
30+
contract.check_substory_call(ctx, ns)
31+
except Exception as error:
32+
history.on_error(error.__class__.__name__)
33+
raise
34+
continue
35+
36+
if method_type is EndOfStory:
37+
history.on_substory_end()
38+
continue
39+
2740
history.before_call(method.__name__)
2841

2942
try:
@@ -54,19 +67,6 @@ def execute(runner, ctx, ns, lines, history, methods):
5467
skipped = 1
5568
continue
5669

57-
if method_type is BeginningOfStory:
58-
try:
59-
contract.check_substory_call(ctx, ns)
60-
except Exception as error:
61-
history.on_error(error.__class__.__name__)
62-
raise
63-
history.on_substory_start()
64-
continue
65-
66-
if method_type is EndOfStory:
67-
history.on_substory_end()
68-
continue
69-
7070
try:
7171
kwargs = contract.check_success_statement(method, ctx, ns, result.kwargs)
7272
except Exception as error:

src/_stories/history.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ def on_skip(self):
2525
def on_error(self, error_name):
2626
self.lines[-1] += " (errored: " + error_name + ")"
2727

28-
def on_substory_start(self):
28+
def on_substory_start(self, story_name):
29+
self.before_call(story_name)
2930
self.indent += 1
3031

3132
def on_substory_end(self):
32-
self.lines.pop()
3333
self.indent -= 1

src/_stories/marker.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
from _stories.returned import Success
32

43

54
class BeginningOfStory(object):
@@ -9,11 +8,8 @@ def __init__(self, cls_name, name):
98
self.parent_name = None
109
self.same_object = None
1110

12-
def __call__(self, ctx):
13-
return Success()
14-
1511
@property
16-
def __name__(self):
12+
def story_name(self):
1713
if self.parent_name is None:
1814
return self.cls_name + "." + self.name
1915
elif self.same_object:
@@ -27,7 +23,4 @@ def set_parent(self, parent_name, same_object):
2723

2824

2925
class EndOfStory(object):
30-
def __call__(self, ctx):
31-
return Success()
32-
33-
__name__ = "end_of_story"
26+
pass

src/_stories/marker.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ from _stories.returned import Success
22

33
class BeginningOfStory:
44
def __init__(self, cls_name: str, name: str) -> None: ...
5-
def __call__(self, ctx: object) -> Success: ...
5+
@property
6+
def story_name(self) -> str: ...
67
def set_parent(self, parent_name: str, same_object: bool) -> None: ...
78

8-
class EndOfStory:
9-
def __call__(self, ctx: object) -> Success: ...
9+
class EndOfStory: ...

src/_stories/mounted.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# -*- coding: utf-8 -*-
22
from _stories.context import make_context
3-
from _stories.execute import function
43
from _stories.failures import make_run_protocol
54
from _stories.history import History
65
from _stories.marker import BeginningOfStory
@@ -31,39 +30,43 @@ def __repr__(self):
3130

3231

3332
class MountedStory(object):
34-
def __init__(self, obj, cls_name, name, arguments, methods, contract, failures):
33+
def __init__(
34+
self, obj, cls_name, name, arguments, methods, contract, failures, executor
35+
):
3536
self.obj = obj
3637
self.cls_name = cls_name
3738
self.name = name
3839
self.arguments = arguments
3940
self.methods = methods
4041
self.contract = contract
4142
self.failures = failures
43+
self.executor = executor
4244

4345
def __call__(self, **kwargs):
4446
__tracebackhide__ = True
4547
history = History()
4648
ctx, ns, lines = make_context(self.methods[0][1], kwargs, history)
4749
runner = Call()
48-
return function.execute(runner, ctx, ns, lines, history, self.methods)
50+
return self.executor(runner, ctx, ns, lines, history, self.methods)
4951

5052
def run(self, **kwargs):
5153
__tracebackhide__ = True
5254
history = History()
5355
ctx, ns, lines = make_context(self.methods[0][1], kwargs, history)
5456
run_protocol = make_run_protocol(self.failures, self.cls_name, self.name)
5557
runner = Run(run_protocol)
56-
return function.execute(runner, ctx, ns, lines, history, self.methods)
58+
return self.executor(runner, ctx, ns, lines, history, self.methods)
5759

5860
def __repr__(self):
5961
result = []
6062
indent = 0
6163
for method, _contract, _protocol in self.methods:
6264
method_type = type(method)
63-
if method_type is EndOfStory:
65+
if method_type is BeginningOfStory:
66+
result.append(" " * indent + method.story_name)
67+
indent += 1
68+
elif method_type is EndOfStory:
6469
indent -= 1
6570
else:
6671
result.append(" " * indent + method.__name__)
67-
if method_type is BeginningOfStory:
68-
indent += 1
6972
return "\n".join(result)

src/_stories/story.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def get_method(self, obj, cls):
3131
cls, name, collected, contract_method, failures_method
3232
)
3333
else:
34-
methods, contract, failures = wrap_story(
34+
methods, contract, failures, executor = wrap_story(
3535
arguments,
3636
collected,
3737
cls.__name__,
@@ -41,7 +41,14 @@ def get_method(self, obj, cls):
4141
this["failures"],
4242
)
4343
return MountedStory(
44-
obj, cls.__name__, name, arguments, methods, contract, failures
44+
obj,
45+
cls.__name__,
46+
name,
47+
arguments,
48+
methods,
49+
contract,
50+
failures,
51+
executor,
4552
)
4653

4754
return type(

0 commit comments

Comments
 (0)
0