test_contextvars.py 10 KB

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