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