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