##// END OF EJS Templates
generalize to arbitrary errors by depth
felixzhuologist -
Show More
@@ -1,163 +1,165 b''
1 """
1 """
2 Async helper function that are invalid syntax on Python 3.5 and below.
2 Async helper function that are invalid syntax on Python 3.5 and below.
3
3
4 This code is best effort, and may have edge cases not behaving as expected. In
4 This code is best effort, and may have edge cases not behaving as expected. In
5 particular it contain a number of heuristics to detect whether code is
5 particular it contain a number of heuristics to detect whether code is
6 effectively async and need to run in an event loop or not.
6 effectively async and need to run in an event loop or not.
7
7
8 Some constructs (like top-level `return`, or `yield`) are taken care of
8 Some constructs (like top-level `return`, or `yield`) are taken care of
9 explicitly to actually raise a SyntaxError and stay as close as possible to
9 explicitly to actually raise a SyntaxError and stay as close as possible to
10 Python semantics.
10 Python semantics.
11 """
11 """
12
12
13
13
14 import ast
14 import ast
15 import sys
15 import sys
16 from textwrap import dedent, indent
16 from textwrap import dedent, indent
17
17
18
18
19 class _AsyncIORunner:
19 class _AsyncIORunner:
20
20
21 def __call__(self, coro):
21 def __call__(self, coro):
22 """
22 """
23 Handler for asyncio autoawait
23 Handler for asyncio autoawait
24 """
24 """
25 import asyncio
25 import asyncio
26
26
27 return asyncio.get_event_loop().run_until_complete(coro)
27 return asyncio.get_event_loop().run_until_complete(coro)
28
28
29 def __str__(self):
29 def __str__(self):
30 return 'asyncio'
30 return 'asyncio'
31
31
32 _asyncio_runner = _AsyncIORunner()
32 _asyncio_runner = _AsyncIORunner()
33
33
34
34
35 def _curio_runner(coroutine):
35 def _curio_runner(coroutine):
36 """
36 """
37 handler for curio autoawait
37 handler for curio autoawait
38 """
38 """
39 import curio
39 import curio
40
40
41 return curio.run(coroutine)
41 return curio.run(coroutine)
42
42
43
43
44 def _trio_runner(async_fn):
44 def _trio_runner(async_fn):
45 import trio
45 import trio
46
46
47 async def loc(coro):
47 async def loc(coro):
48 """
48 """
49 We need the dummy no-op async def to protect from
49 We need the dummy no-op async def to protect from
50 trio's internal. See https://github.com/python-trio/trio/issues/89
50 trio's internal. See https://github.com/python-trio/trio/issues/89
51 """
51 """
52 return await coro
52 return await coro
53
53
54 return trio.run(loc, async_fn)
54 return trio.run(loc, async_fn)
55
55
56
56
57 def _pseudo_sync_runner(coro):
57 def _pseudo_sync_runner(coro):
58 """
58 """
59 A runner that does not really allow async execution, and just advance the coroutine.
59 A runner that does not really allow async execution, and just advance the coroutine.
60
60
61 See discussion in https://github.com/python-trio/trio/issues/608,
61 See discussion in https://github.com/python-trio/trio/issues/608,
62
62
63 Credit to Nathaniel Smith
63 Credit to Nathaniel Smith
64
64
65 """
65 """
66 try:
66 try:
67 coro.send(None)
67 coro.send(None)
68 except StopIteration as exc:
68 except StopIteration as exc:
69 return exc.value
69 return exc.value
70 else:
70 else:
71 # TODO: do not raise but return an execution result with the right info.
71 # TODO: do not raise but return an execution result with the right info.
72 raise RuntimeError(
72 raise RuntimeError(
73 "{coro_name!r} needs a real async loop".format(coro_name=coro.__name__)
73 "{coro_name!r} needs a real async loop".format(coro_name=coro.__name__)
74 )
74 )
75
75
76
76
77 def _asyncify(code: str) -> str:
77 def _asyncify(code: str) -> str:
78 """wrap code in async def definition.
78 """wrap code in async def definition.
79
79
80 And setup a bit of context to run it later.
80 And setup a bit of context to run it later.
81 """
81 """
82 res = dedent(
82 res = dedent(
83 """
83 """
84 async def __wrapper__():
84 async def __wrapper__():
85 try:
85 try:
86 {usercode}
86 {usercode}
87 finally:
87 finally:
88 locals()
88 locals()
89 """
89 """
90 ).format(usercode=indent(code, " " * 8))
90 ).format(usercode=indent(code, " " * 8))
91 return res
91 return res
92
92
93
93
94 class _AsyncSyntaxErrorVisitor(ast.NodeVisitor):
94 class _AsyncSyntaxErrorVisitor(ast.NodeVisitor):
95 """
95 """
96 Find syntax errors that would be an error in an async repl, but because
96 Find syntax errors that would be an error in an async repl, but because
97 the implementation involves wrapping the repl in an async function, it
97 the implementation involves wrapping the repl in an async function, it
98 is erroneously allowed (e.g. yield or return at the top level)
98 is erroneously allowed (e.g. yield or return at the top level)
99 """
99 """
100 def __init__(self, is_toplevel=True):
100 def __init__(self):
101 self.is_toplevel = is_toplevel
101 self.depth = 0
102 super().__init__()
102 super().__init__()
103
103
104 def generic_visit(self, node):
104 def generic_visit(self, node):
105 func_types = (ast.FunctionDef, ast.AsyncFunctionDef)
105 func_types = (ast.FunctionDef, ast.AsyncFunctionDef)
106 toplevel_invalid_types = (ast.Return, ast.Yield, ast.YieldFrom)
106 invalid_types_by_depth = {
107 inner_invalid_types = (ast.Nonlocal,)
107 0: (ast.Return, ast.Yield, ast.YieldFrom),
108
108 1: (ast.Nonlocal,)
109 if isinstance(node, func_types) and self.is_toplevel:
109 }
110 self.is_toplevel = False
110
111 should_traverse = self.depth < max(invalid_types_by_depth.keys())
112 if isinstance(node, func_types) and should_traverse:
113 self.depth += 1
111 super().generic_visit(node)
114 super().generic_visit(node)
112 elif self.is_toplevel and isinstance(node, toplevel_invalid_types):
115 elif isinstance(node, invalid_types_by_depth[self.depth]):
113 raise SyntaxError()
114 elif not self.is_toplevel and isinstance(node, inner_invalid_types):
115 raise SyntaxError()
116 raise SyntaxError()
116 else:
117 else:
117 super().generic_visit(node)
118 super().generic_visit(node)
118
119
120
119 def _async_parse_cell(cell: str) -> ast.AST:
121 def _async_parse_cell(cell: str) -> ast.AST:
120 """
122 """
121 This is a compatibility shim for pre-3.7 when async outside of a function
123 This is a compatibility shim for pre-3.7 when async outside of a function
122 is a syntax error at the parse stage.
124 is a syntax error at the parse stage.
123
125
124 It will return an abstract syntax tree parsed as if async and await outside
126 It will return an abstract syntax tree parsed as if async and await outside
125 of a function were not a syntax error.
127 of a function were not a syntax error.
126 """
128 """
127 if sys.version_info < (3, 7):
129 if sys.version_info < (3, 7):
128 # Prior to 3.7 you need to asyncify before parse
130 # Prior to 3.7 you need to asyncify before parse
129 wrapped_parse_tree = ast.parse(_asyncify(cell))
131 wrapped_parse_tree = ast.parse(_asyncify(cell))
130 return wrapped_parse_tree.body[0].body[0]
132 return wrapped_parse_tree.body[0].body[0]
131 else:
133 else:
132 return ast.parse(cell)
134 return ast.parse(cell)
133
135
134
136
135 def _should_be_async(cell: str) -> bool:
137 def _should_be_async(cell: str) -> bool:
136 """Detect if a block of code need to be wrapped in an `async def`
138 """Detect if a block of code need to be wrapped in an `async def`
137
139
138 Attempt to parse the block of code, it it compile we're fine.
140 Attempt to parse the block of code, it it compile we're fine.
139 Otherwise we wrap if and try to compile.
141 Otherwise we wrap if and try to compile.
140
142
141 If it works, assume it should be async. Otherwise Return False.
143 If it works, assume it should be async. Otherwise Return False.
142
144
143 Not handled yet: If the block of code has a return statement as the top
145 Not handled yet: If the block of code has a return statement as the top
144 level, it will be seen as async. This is a know limitation.
146 level, it will be seen as async. This is a know limitation.
145 """
147 """
146
148
147 try:
149 try:
148 # we can't limit ourself to ast.parse, as it __accepts__ to parse on
150 # we can't limit ourself to ast.parse, as it __accepts__ to parse on
149 # 3.7+, but just does not _compile_
151 # 3.7+, but just does not _compile_
150 compile(cell, "<>", "exec")
152 compile(cell, "<>", "exec")
151 return False
153 return False
152 except SyntaxError:
154 except SyntaxError:
153 try:
155 try:
154 parse_tree = _async_parse_cell(cell)
156 parse_tree = _async_parse_cell(cell)
155
157
156 # Raise a SyntaxError if there are top-level return or yields
158 # Raise a SyntaxError if there are top-level return or yields
157 v = _AsyncSyntaxErrorVisitor()
159 v = _AsyncSyntaxErrorVisitor()
158 v.visit(parse_tree)
160 v.visit(parse_tree)
159
161
160 except SyntaxError:
162 except SyntaxError:
161 return False
163 return False
162 return True
164 return True
163 return False
165 return False
General Comments 0
You need to be logged in to leave comments. Login now