##// END OF EJS Templates
More work on long-lived Trio loop...
Mark E. Haase -
Show More
@@ -1,208 +1,213 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 import inspect
17 17 from textwrap import dedent, indent
18 18
19 19
20 20 class _AsyncIORunner:
21 21
22 22 def __call__(self, coro):
23 23 """
24 24 Handler for asyncio autoawait
25 25 """
26 26 import asyncio
27 27
28 28 return asyncio.get_event_loop().run_until_complete(coro)
29 29
30 30 def __str__(self):
31 31 return 'asyncio'
32 32
33 33 _asyncio_runner = _AsyncIORunner()
34 34
35 35
36 36 def _curio_runner(coroutine):
37 37 """
38 38 handler for curio autoawait
39 39 """
40 40 import curio
41 41
42 42 return curio.run(coroutine)
43 43
44 44
45 45 _TRIO_TOKEN = None
46 _TRIO_NURSERY = None
47 46
48 47
49 48 def _init_trio(trio):
50 49 global _TRIO_TOKEN
51 50 import traceback
52 51 import builtins
53 52 import threading
53 # We use an Event to avoid a race condition between starting the Trio thread
54 # and running Trio code.
55 thread_start = threading.Event()
56
57 def log_nursery_exc(exc):
58 import logging
59 import traceback
60 exc = '\n'.join(traceback.format_exception(type(exc), exc,
61 exc.__traceback__))
62 logging.error('An exception occurred in a global nursery task.\n%s',
63 exc)
54 64
55 65 async def trio_entry():
56 66 global _TRIO_TOKEN, _TRIO_NURSERY
57 67 _TRIO_TOKEN = trio.hazmat.current_trio_token()
58 68 async with trio.open_nursery() as nursery:
59 _TRIO_NURSERY = nursery
69 # TODO This hack prevents the nursery from cancelling all child
70 # tasks when but it's ugly.
71 nursery._add_exc = log_nursery_exc
60 72 builtins.GLOBAL_NURSERY = nursery
73 thread_start.set()
61 74 await trio.sleep_forever()
62 75
63 def trio_entry_sync():
64 while True:
65 try:
66 trio.run(trio_entry)
67 except Exception:
68 print("Exception in trio event loop:", traceback.format_exc())
69
70 threading.Thread(target=trio_entry_sync).start()
71 #TODO fix this race condition
72 import time
73 while not _TRIO_TOKEN:
74 time.sleep(0.1)
76 threading.Thread(target=trio.run, args=(trio_entry,)).start()
77 thread_start.wait()
75 78
76 79
77 80 def _trio_runner(async_fn):
81 global _TRIO_TOKEN
82 import logging
78 83 import trio
79 84
80 85 if not _TRIO_TOKEN:
81 86 _init_trio(trio)
82 87
83 88 async def loc(coro):
84 89 """
85 90 We need the dummy no-op async def to protect from
86 91 trio's internal. See https://github.com/python-trio/trio/issues/89
87 92 """
88 93 return await coro
89 94
90 95 return trio.from_thread.run(loc, async_fn, trio_token=_TRIO_TOKEN)
91 96
92 97
93 98 def _pseudo_sync_runner(coro):
94 99 """
95 100 A runner that does not really allow async execution, and just advance the coroutine.
96 101
97 102 See discussion in https://github.com/python-trio/trio/issues/608,
98 103
99 104 Credit to Nathaniel Smith
100 105
101 106 """
102 107 try:
103 108 coro.send(None)
104 109 except StopIteration as exc:
105 110 return exc.value
106 111 else:
107 112 # TODO: do not raise but return an execution result with the right info.
108 113 raise RuntimeError(
109 114 "{coro_name!r} needs a real async loop".format(coro_name=coro.__name__)
110 115 )
111 116
112 117
113 118 def _asyncify(code: str) -> str:
114 119 """wrap code in async def definition.
115 120
116 121 And setup a bit of context to run it later.
117 122 """
118 123 res = dedent(
119 124 """
120 125 async def __wrapper__():
121 126 try:
122 127 {usercode}
123 128 finally:
124 129 locals()
125 130 """
126 131 ).format(usercode=indent(code, " " * 8))
127 132 return res
128 133
129 134
130 135 class _AsyncSyntaxErrorVisitor(ast.NodeVisitor):
131 136 """
132 137 Find syntax errors that would be an error in an async repl, but because
133 138 the implementation involves wrapping the repl in an async function, it
134 139 is erroneously allowed (e.g. yield or return at the top level)
135 140 """
136 141 def __init__(self):
137 142 if sys.version_info >= (3,8):
138 143 raise ValueError('DEPRECATED in Python 3.8+')
139 144 self.depth = 0
140 145 super().__init__()
141 146
142 147 def generic_visit(self, node):
143 148 func_types = (ast.FunctionDef, ast.AsyncFunctionDef)
144 149 invalid_types_by_depth = {
145 150 0: (ast.Return, ast.Yield, ast.YieldFrom),
146 151 1: (ast.Nonlocal,)
147 152 }
148 153
149 154 should_traverse = self.depth < max(invalid_types_by_depth.keys())
150 155 if isinstance(node, func_types) and should_traverse:
151 156 self.depth += 1
152 157 super().generic_visit(node)
153 158 self.depth -= 1
154 159 elif isinstance(node, invalid_types_by_depth[self.depth]):
155 160 raise SyntaxError()
156 161 else:
157 162 super().generic_visit(node)
158 163
159 164
160 165 def _async_parse_cell(cell: str) -> ast.AST:
161 166 """
162 167 This is a compatibility shim for pre-3.7 when async outside of a function
163 168 is a syntax error at the parse stage.
164 169
165 170 It will return an abstract syntax tree parsed as if async and await outside
166 171 of a function were not a syntax error.
167 172 """
168 173 if sys.version_info < (3, 7):
169 174 # Prior to 3.7 you need to asyncify before parse
170 175 wrapped_parse_tree = ast.parse(_asyncify(cell))
171 176 return wrapped_parse_tree.body[0].body[0]
172 177 else:
173 178 return ast.parse(cell)
174 179
175 180
176 181 def _should_be_async(cell: str) -> bool:
177 182 """Detect if a block of code need to be wrapped in an `async def`
178 183
179 184 Attempt to parse the block of code, it it compile we're fine.
180 185 Otherwise we wrap if and try to compile.
181 186
182 187 If it works, assume it should be async. Otherwise Return False.
183 188
184 189 Not handled yet: If the block of code has a return statement as the top
185 190 level, it will be seen as async. This is a know limitation.
186 191 """
187 192 if sys.version_info > (3, 8):
188 193 try:
189 194 code = compile(cell, "<>", "exec", flags=getattr(ast,'PyCF_ALLOW_TOP_LEVEL_AWAIT', 0x0))
190 195 return inspect.CO_COROUTINE & code.co_flags == inspect.CO_COROUTINE
191 196 except SyntaxError:
192 197 return False
193 198 try:
194 199 # we can't limit ourself to ast.parse, as it __accepts__ to parse on
195 200 # 3.7+, but just does not _compile_
196 201 code = compile(cell, "<>", "exec")
197 202 except SyntaxError:
198 203 try:
199 204 parse_tree = _async_parse_cell(cell)
200 205
201 206 # Raise a SyntaxError if there are top-level return or yields
202 207 v = _AsyncSyntaxErrorVisitor()
203 208 v.visit(parse_tree)
204 209
205 210 except SyntaxError:
206 211 return False
207 212 return True
208 213 return False
General Comments 0
You need to be logged in to leave comments. Login now