Skip to content

Commit 2a4fab4

Browse files
authored
Implement PEP 567 support (contextvars) for Python 3.7 (#155)
1 parent 6e03e51 commit 2a4fab4

File tree

7 files changed

+249
-27
lines changed

7 files changed

+249
-27
lines changed

tests/test_context.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import asyncio
2+
import decimal
3+
import random
4+
import sys
5+
import unittest
6+
7+
from uvloop import _testbase as tb
8+
9+
10+
PY37 = sys.version_info >= (3, 7, 0)
11+
12+
13+
class _ContextBaseTests:
14+
15+
@unittest.skipUnless(PY37, 'requires Python 3.7')
16+
def test_task_decimal_context(self):
17+
async def fractions(t, precision, x, y):
18+
with decimal.localcontext() as ctx:
19+
ctx.prec = precision
20+
a = decimal.Decimal(x) / decimal.Decimal(y)
21+
await asyncio.sleep(t, loop=self.loop)
22+
b = decimal.Decimal(x) / decimal.Decimal(y ** 2)
23+
return a, b
24+
25+
async def main():
26+
r1, r2 = await asyncio.gather(
27+
fractions(0.1, 3, 1, 3), fractions(0.2, 6, 1, 3),
28+
loop=self.loop)
29+
30+
return r1, r2
31+
32+
r1, r2 = self.loop.run_until_complete(main())
33+
34+
self.assertEqual(str(r1[0]), '0.333')
35+
self.assertEqual(str(r1[1]), '0.111')
36+
37+
self.assertEqual(str(r2[0]), '0.333333')
38+
self.assertEqual(str(r2[1]), '0.111111')
39+
40+
@unittest.skipUnless(PY37, 'requires Python 3.7')
41+
def test_task_context_1(self):
42+
import contextvars
43+
cvar = contextvars.ContextVar('cvar', default='nope')
44+
45+
async def sub():
46+
await asyncio.sleep(0.01, loop=self.loop)
47+
self.assertEqual(cvar.get(), 'nope')
48+
cvar.set('something else')
49+
50+
async def main():
51+
self.assertEqual(cvar.get(), 'nope')
52+
subtask = self.loop.create_task(sub())
53+
cvar.set('yes')
54+
self.assertEqual(cvar.get(), 'yes')
55+
await subtask
56+
self.assertEqual(cvar.get(), 'yes')
57+
58+
task = self.loop.create_task(main())
59+
self.loop.run_until_complete(task)
60+
61+
@unittest.skipUnless(PY37, 'requires Python 3.7')
62+
def test_task_context_2(self):
63+
import contextvars
64+
cvar = contextvars.ContextVar('cvar', default='nope')
65+
66+
async def main():
67+
def fut_on_done(fut):
68+
# This change must not pollute the context
69+
# of the "main()" task.
70+
cvar.set('something else')
71+
72+
self.assertEqual(cvar.get(), 'nope')
73+
74+
for j in range(2):
75+
fut = self.loop.create_future()
76+
fut.add_done_callback(fut_on_done)
77+
cvar.set('yes{}'.format(j))
78+
self.loop.call_soon(fut.set_result, None)
79+
await fut
80+
self.assertEqual(cvar.get(), 'yes{}'.format(j))
81+
82+
for i in range(3):
83+
# Test that task passed its context to add_done_callback:
84+
cvar.set('yes{}-{}'.format(i, j))
85+
await asyncio.sleep(0.001, loop=self.loop)
86+
self.assertEqual(cvar.get(), 'yes{}-{}'.format(i, j))
87+
88+
task = self.loop.create_task(main())
89+
self.loop.run_until_complete(task)
90+
91+
self.assertEqual(cvar.get(), 'nope')
92+
93+
@unittest.skipUnless(PY37, 'requires Python 3.7')
94+
def test_task_context_3(self):
95+
import contextvars
96+
cvar = contextvars.ContextVar('cvar', default=-1)
97+
98+
# Run 100 Tasks in parallel, each modifying cvar.
99+
100+
async def sub(num):
101+
for i in range(10):
102+
cvar.set(num + i)
103+
await asyncio.sleep(
104+
random.uniform(0.001, 0.05), loop=self.loop)
105+
self.assertEqual(cvar.get(), num + i)
106+
107+
async def main():
108+
tasks = []
109+
for i in range(100):
110+
task = self.loop.create_task(sub(random.randint(0, 10)))
111+
tasks.append(task)
112+
113+
await asyncio.gather(
114+
*tasks, loop=self.loop, return_exceptions=True)
115+
116+
self.loop.run_until_complete(main())
117+
118+
self.assertEqual(cvar.get(), -1)
119+
120+
121+
class Test_UV_Context(_ContextBaseTests, tb.UVTestCase):
122+
123+
@unittest.skipIf(PY37, 'requires Python <3.6')
124+
def test_context_arg(self):
125+
def cb():
126+
pass
127+
128+
with self.assertRaisesRegex(RuntimeError, 'requires Python 3.7'):
129+
self.loop.call_soon(cb, context=1)
130+
131+
with self.assertRaisesRegex(RuntimeError, 'requires Python 3.7'):
132+
self.loop.call_soon_threadsafe(cb, context=1)
133+
134+
with self.assertRaisesRegex(RuntimeError, 'requires Python 3.7'):
135+
self.loop.call_later(0.1, cb, context=1)
136+
137+
138+
class Test_AIO_Context(_ContextBaseTests, tb.AIOTestCase):
139+
pass

uvloop/cbhandles.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
cdef class Handle:
22
cdef:
33
Loop loop
4+
object context
45
bint _cancelled
56

67
str meth_name
@@ -13,6 +14,8 @@ cdef class Handle:
1314
readonly _source_traceback
1415

1516
cdef inline _set_loop(self, Loop loop)
17+
cdef inline _set_context(self, object context)
18+
1619
cdef inline _run(self)
1720
cdef _cancel(self)
1821

@@ -25,6 +28,7 @@ cdef class TimerHandle:
2528
bint _cancelled
2629
UVTimer timer
2730
Loop loop
31+
object context
2832
object __weakref__
2933

3034
readonly _source_traceback

uvloop/cbhandles.pyx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ cdef class Handle:
1414
if loop._debug:
1515
self._source_traceback = extract_stack()
1616

17+
cdef inline _set_context(self, object context):
18+
if PY37:
19+
if context is None:
20+
context = <object>PyContext_CopyCurrent()
21+
self.context = context
22+
else:
23+
if context is not None:
24+
raise RuntimeError('"context" argument requires Python 3.7')
25+
self.context = None
26+
1727
def __dealloc__(self):
1828
if UVLOOP_DEBUG and self.loop is not None:
1929
self.loop._debug_cb_handles_count -= 1
@@ -39,6 +49,10 @@ cdef class Handle:
3949
# we guard 'self' manually (since the callback
4050
# might cause GC of the handle.)
4151
try:
52+
if PY37:
53+
assert self.context is not None
54+
PyContext_Enter(<PyContext*>self.context)
55+
4256
if cb_type == 1:
4357
callback = self.arg1
4458
args = self.arg2
@@ -83,7 +97,10 @@ cdef class Handle:
8397
self.loop.call_exception_handler(context)
8498

8599
finally:
100+
context = self.context
86101
Py_DECREF(self)
102+
if PY37:
103+
PyContext_Exit(<PyContext*>context)
87104

88105
cdef _cancel(self):
89106
self._cancelled = 1
@@ -148,13 +165,26 @@ cdef class Handle:
148165
@cython.freelist(DEFAULT_FREELIST_SIZE)
149166
cdef class TimerHandle:
150167
def __cinit__(self, Loop loop, object callback, object args,
151-
uint64_t delay):
168+
uint64_t delay, object context):
152169

153170
self.loop = loop
154171
self.callback = callback
155172
self.args = args
156173
self._cancelled = 0
157174

175+
if UVLOOP_DEBUG:
176+
self.loop._debug_cb_timer_handles_total += 1
177+
self.loop._debug_cb_timer_handles_count += 1
178+
179+
if PY37:
180+
if context is None:
181+
context = <object>PyContext_CopyCurrent()
182+
self.context = context
183+
else:
184+
if context is not None:
185+
raise RuntimeError('"context" argument requires Python 3.7')
186+
self.context = None
187+
158188
if loop._debug:
159189
self._source_traceback = extract_stack()
160190

@@ -166,10 +196,6 @@ cdef class TimerHandle:
166196
# Only add to loop._timers when `self.timer` is successfully created
167197
loop._timers.add(self)
168198

169-
if UVLOOP_DEBUG:
170-
self.loop._debug_cb_timer_handles_total += 1
171-
self.loop._debug_cb_timer_handles_count += 1
172-
173199
def __dealloc__(self):
174200
if UVLOOP_DEBUG:
175201
self.loop._debug_cb_timer_handles_count -= 1
@@ -207,6 +233,10 @@ cdef class TimerHandle:
207233
if self.loop._debug:
208234
started = time_monotonic()
209235
try:
236+
if PY37:
237+
assert self.context is not None
238+
PyContext_Enter(<PyContext*>self.context)
239+
210240
if args is not None:
211241
callback(*args)
212242
else:
@@ -230,7 +260,10 @@ cdef class TimerHandle:
230260
'Executing %r took %.3f seconds',
231261
self, delta)
232262
finally:
263+
context = self.context
233264
Py_DECREF(self)
265+
if PY37:
266+
PyContext_Exit(<PyContext*>context)
234267

235268
# Public API
236269

@@ -263,10 +296,12 @@ cdef class TimerHandle:
263296
self._cancel()
264297

265298

266-
cdef new_Handle(Loop loop, object callback, object args):
299+
300+
cdef new_Handle(Loop loop, object callback, object args, object context):
267301
cdef Handle handle
268302
handle = Handle.__new__(Handle)
269303
handle._set_loop(loop)
304+
handle._set_context(context)
270305

271306
handle.cb_type = 1
272307

@@ -280,6 +315,7 @@ cdef new_MethodHandle(Loop loop, str name, method_t callback, object ctx):
280315
cdef Handle handle
281316
handle = Handle.__new__(Handle)
282317
handle._set_loop(loop)
318+
handle._set_context(None)
283319

284320
handle.cb_type = 2
285321
handle.meth_name = name
@@ -296,6 +332,7 @@ cdef new_MethodHandle1(Loop loop, str name, method1_t callback,
296332
cdef Handle handle
297333
handle = Handle.__new__(Handle)
298334
handle._set_loop(loop)
335+
handle._set_context(None)
299336

300337
handle.cb_type = 3
301338
handle.meth_name = name
@@ -312,6 +349,7 @@ cdef new_MethodHandle2(Loop loop, str name, method2_t callback, object ctx,
312349
cdef Handle handle
313350
handle = Handle.__new__(Handle)
314351
handle._set_loop(loop)
352+
handle._set_context(None)
315353

316354
handle.cb_type = 4
317355
handle.meth_name = name
@@ -329,6 +367,7 @@ cdef new_MethodHandle3(Loop loop, str name, method3_t callback, object ctx,
329367
cdef Handle handle
330368
handle = Handle.__new__(Handle)
331369
handle._set_loop(loop)
370+
handle._set_context(None)
332371

333372
handle.cb_type = 5
334373
handle.meth_name = name

uvloop/includes/compat.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <errno.h>
2+
#include "Python.h"
23
#include "uv.h"
34

45

@@ -27,3 +28,22 @@
2728
struct epoll_event {};
2829
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) {};
2930
#endif
31+
32+
33+
#if PY_VERSION_HEX < 0x03070000
34+
typedef struct {
35+
PyObject_HEAD
36+
} PyContext;
37+
38+
PyContext * PyContext_CopyCurrent(void) {
39+
return NULL;
40+
};
41+
42+
int PyContext_Enter(PyContext *ctx) {
43+
return -1;
44+
}
45+
46+
int PyContext_Exit(PyContext *ctx) {
47+
return -1;
48+
}
49+
#endif

uvloop/includes/python.pxd

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
cdef extern from "Python.h":
2+
int PY_VERSION_HEX
3+
24
void* PyMem_RawMalloc(size_t n)
35
void* PyMem_RawRealloc(void *p, size_t n)
4-
void* PyMem_RawCalloc(size_t nelem, size_t elsize) # Python >= 3.5!
6+
void* PyMem_RawCalloc(size_t nelem, size_t elsize)
57
void PyMem_RawFree(void *p)
68

79
object PyUnicode_EncodeFSDefault(object)
@@ -11,3 +13,10 @@ cdef extern from "Python.h":
1113
void _PyImport_AcquireLock()
1214
int _PyImport_ReleaseLock()
1315
void _Py_RestoreSignals()
16+
17+
18+
cdef extern from "includes/compat.h":
19+
ctypedef struct PyContext
20+
PyContext* PyContext_CopyCurrent() except NULL
21+
int PyContext_Enter(PyContext *) except -1
22+
int PyContext_Exit(PyContext *) except -1

uvloop/loop.pxd

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,11 @@ cdef class Loop:
142142
cdef inline _queue_write(self, UVStream stream)
143143
cdef _exec_queued_writes(self)
144144

145-
cdef inline _call_soon(self, object callback, object args)
145+
cdef inline _call_soon(self, object callback, object args, object context)
146146
cdef inline _call_soon_handle(self, Handle handle)
147147

148-
cdef _call_later(self, uint64_t delay, object callback, object args)
148+
cdef _call_later(self, uint64_t delay, object callback, object args,
149+
object context)
149150

150151
cdef void _handle_exception(self, object ex)
151152

0 commit comments

Comments
 (0)