test_contextvars.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. from __future__ import print_function
  2. import gc
  3. import sys
  4. import unittest
  5. from functools import partial
  6. from unittest import skipUnless
  7. from unittest import skipIf
  8. from greenlet import greenlet
  9. from greenlet import getcurrent
  10. from . import TestCase
  11. try:
  12. from contextvars import Context
  13. from contextvars import ContextVar
  14. from contextvars import copy_context
  15. # From the documentation:
  16. #
  17. # Important: Context Variables should be created at the top module
  18. # level and never in closures. Context objects hold strong
  19. # references to context variables which prevents context variables
  20. # from being properly garbage collected.
  21. ID_VAR = ContextVar("id", default=None)
  22. VAR_VAR = ContextVar("var", default=None)
  23. ContextVar = None
  24. except ImportError:
  25. Context = ContextVar = copy_context = None
  26. # We don't support testing if greenlet's built-in context var support is disabled.
  27. @skipUnless(Context is not None, "ContextVar not supported")
  28. class ContextVarsTests(TestCase):
  29. def _new_ctx_run(self, *args, **kwargs):
  30. return copy_context().run(*args, **kwargs)
  31. def _increment(self, greenlet_id, callback, counts, expect):
  32. ctx_var = ID_VAR
  33. if expect is None:
  34. self.assertIsNone(ctx_var.get())
  35. else:
  36. self.assertEqual(ctx_var.get(), expect)
  37. ctx_var.set(greenlet_id)
  38. for _ in range(2):
  39. counts[ctx_var.get()] += 1
  40. callback()
  41. def _test_context(self, propagate_by):
  42. # pylint:disable=too-many-branches
  43. ID_VAR.set(0)
  44. callback = getcurrent().switch
  45. counts = dict((i, 0) for i in range(5))
  46. lets = [
  47. greenlet(partial(
  48. partial(
  49. copy_context().run,
  50. self._increment
  51. ) if propagate_by == "run" else self._increment,
  52. greenlet_id=i,
  53. callback=callback,
  54. counts=counts,
  55. expect=(
  56. i - 1 if propagate_by == "share" else
  57. 0 if propagate_by in ("set", "run") else None
  58. )
  59. ))
  60. for i in range(1, 5)
  61. ]
  62. for let in lets:
  63. if propagate_by == "set":
  64. let.gr_context = copy_context()
  65. elif propagate_by == "share":
  66. let.gr_context = getcurrent().gr_context
  67. for i in range(2):
  68. counts[ID_VAR.get()] += 1
  69. for let in lets:
  70. let.switch()
  71. if propagate_by == "run":
  72. # Must leave each context.run() in reverse order of entry
  73. for let in reversed(lets):
  74. let.switch()
  75. else:
  76. # No context.run(), so fine to exit in any order.
  77. for let in lets:
  78. let.switch()
  79. for let in lets:
  80. self.assertTrue(let.dead)
  81. # When using run(), we leave the run() as the greenlet dies,
  82. # and there's no context "underneath". When not using run(),
  83. # gr_context still reflects the context the greenlet was
  84. # running in.
  85. if propagate_by == 'run':
  86. self.assertIsNone(let.gr_context)
  87. else:
  88. self.assertIsNotNone(let.gr_context)
  89. if propagate_by == "share":
  90. self.assertEqual(counts, {0: 1, 1: 1, 2: 1, 3: 1, 4: 6})
  91. else:
  92. self.assertEqual(set(counts.values()), set([2]))
  93. def test_context_propagated_by_context_run(self):
  94. self._new_ctx_run(self._test_context, "run")
  95. def test_context_propagated_by_setting_attribute(self):
  96. self._new_ctx_run(self._test_context, "set")
  97. def test_context_not_propagated(self):
  98. self._new_ctx_run(self._test_context, None)
  99. def test_context_shared(self):
  100. self._new_ctx_run(self._test_context, "share")
  101. def test_break_ctxvars(self):
  102. let1 = greenlet(copy_context().run)
  103. let2 = greenlet(copy_context().run)
  104. let1.switch(getcurrent().switch)
  105. let2.switch(getcurrent().switch)
  106. # Since let2 entered the current context and let1 exits its own, the
  107. # interpreter emits:
  108. # RuntimeError: cannot exit context: thread state references a different context object
  109. let1.switch()
  110. def test_not_broken_if_using_attribute_instead_of_context_run(self):
  111. let1 = greenlet(getcurrent().switch)
  112. let2 = greenlet(getcurrent().switch)
  113. let1.gr_context = copy_context()
  114. let2.gr_context = copy_context()
  115. let1.switch()
  116. let2.switch()
  117. let1.switch()
  118. let2.switch()
  119. def test_context_assignment_while_running(self):
  120. # pylint:disable=too-many-statements
  121. ID_VAR.set(None)
  122. def target():
  123. self.assertIsNone(ID_VAR.get())
  124. self.assertIsNone(gr.gr_context)
  125. # Context is created on first use
  126. ID_VAR.set(1)
  127. self.assertIsInstance(gr.gr_context, Context)
  128. self.assertEqual(ID_VAR.get(), 1)
  129. self.assertEqual(gr.gr_context[ID_VAR], 1)
  130. # Clearing the context makes it get re-created as another
  131. # empty context when next used
  132. old_context = gr.gr_context
  133. gr.gr_context = None # assign None while running
  134. self.assertIsNone(ID_VAR.get())
  135. self.assertIsNone(gr.gr_context)
  136. ID_VAR.set(2)
  137. self.assertIsInstance(gr.gr_context, Context)
  138. self.assertEqual(ID_VAR.get(), 2)
  139. self.assertEqual(gr.gr_context[ID_VAR], 2)
  140. new_context = gr.gr_context
  141. getcurrent().parent.switch((old_context, new_context))
  142. # parent switches us back to old_context
  143. self.assertEqual(ID_VAR.get(), 1)
  144. gr.gr_context = new_context # assign non-None while running
  145. self.assertEqual(ID_VAR.get(), 2)
  146. getcurrent().parent.switch()
  147. # parent switches us back to no context
  148. self.assertIsNone(ID_VAR.get())
  149. self.assertIsNone(gr.gr_context)
  150. gr.gr_context = old_context
  151. self.assertEqual(ID_VAR.get(), 1)
  152. getcurrent().parent.switch()
  153. # parent switches us back to no context
  154. self.assertIsNone(ID_VAR.get())
  155. self.assertIsNone(gr.gr_context)
  156. gr = greenlet(target)
  157. with self.assertRaisesRegex(AttributeError, "can't delete context attribute"):
  158. del gr.gr_context
  159. self.assertIsNone(gr.gr_context)
  160. old_context, new_context = gr.switch()
  161. self.assertIs(new_context, gr.gr_context)
  162. self.assertEqual(old_context[ID_VAR], 1)
  163. self.assertEqual(new_context[ID_VAR], 2)
  164. self.assertEqual(new_context.run(ID_VAR.get), 2)
  165. gr.gr_context = old_context # assign non-None while suspended
  166. gr.switch()
  167. self.assertIs(gr.gr_context, new_context)
  168. gr.gr_context = None # assign None while suspended
  169. gr.switch()
  170. self.assertIs(gr.gr_context, old_context)
  171. gr.gr_context = None
  172. gr.switch()
  173. self.assertIsNone(gr.gr_context)
  174. # Make sure there are no reference leaks
  175. gr = None
  176. gc.collect()
  177. self.assertEqual(sys.getrefcount(old_context), 2)
  178. self.assertEqual(sys.getrefcount(new_context), 2)
  179. def test_context_assignment_different_thread(self):
  180. import threading
  181. VAR_VAR.set(None)
  182. ctx = Context()
  183. is_running = threading.Event()
  184. should_suspend = threading.Event()
  185. did_suspend = threading.Event()
  186. should_exit = threading.Event()
  187. holder = []
  188. def greenlet_in_thread_fn():
  189. VAR_VAR.set(1)
  190. is_running.set()
  191. should_suspend.wait(10)
  192. VAR_VAR.set(2)
  193. getcurrent().parent.switch()
  194. holder.append(VAR_VAR.get())
  195. def thread_fn():
  196. gr = greenlet(greenlet_in_thread_fn)
  197. gr.gr_context = ctx
  198. holder.append(gr)
  199. gr.switch()
  200. did_suspend.set()
  201. should_exit.wait(10)
  202. gr.switch()
  203. del gr
  204. greenlet() # trigger cleanup
  205. thread = threading.Thread(target=thread_fn, daemon=True)
  206. thread.start()
  207. is_running.wait(10)
  208. gr = holder[0]
  209. # Can't access or modify context if the greenlet is running
  210. # in a different thread
  211. with self.assertRaisesRegex(ValueError, "running in a different"):
  212. getattr(gr, 'gr_context')
  213. with self.assertRaisesRegex(ValueError, "running in a different"):
  214. gr.gr_context = None
  215. should_suspend.set()
  216. did_suspend.wait(10)
  217. # OK to access and modify context if greenlet is suspended
  218. self.assertIs(gr.gr_context, ctx)
  219. self.assertEqual(gr.gr_context[VAR_VAR], 2)
  220. gr.gr_context = None
  221. should_exit.set()
  222. thread.join(10)
  223. self.assertEqual(holder, [gr, None])
  224. # Context can still be accessed/modified when greenlet is dead:
  225. self.assertIsNone(gr.gr_context)
  226. gr.gr_context = ctx
  227. self.assertIs(gr.gr_context, ctx)
  228. # Otherwise we leak greenlets on some platforms.
  229. # XXX: Should be able to do this automatically
  230. del holder[:]
  231. gr = None
  232. thread = None
  233. def test_context_assignment_wrong_type(self):
  234. g = greenlet()
  235. with self.assertRaisesRegex(TypeError,
  236. "greenlet context must be a contextvars.Context or None"):
  237. g.gr_context = self
  238. @skipIf(Context is not None, "ContextVar supported")
  239. class NoContextVarsTests(TestCase):
  240. def test_contextvars_errors(self):
  241. let1 = greenlet(getcurrent().switch)
  242. self.assertFalse(hasattr(let1, 'gr_context'))
  243. with self.assertRaises(AttributeError):
  244. getattr(let1, 'gr_context')
  245. with self.assertRaises(AttributeError):
  246. let1.gr_context = None
  247. let1.switch()
  248. with self.assertRaises(AttributeError):
  249. getattr(let1, 'gr_context')
  250. with self.assertRaises(AttributeError):
  251. let1.gr_context = None
  252. del let1
  253. if __name__ == '__main__':
  254. unittest.main()