|
| 1 | +import sys |
| 2 | +import threading |
1 | 3 | import unittest |
2 | 4 | import contextvars |
3 | 5 |
|
@@ -659,5 +661,74 @@ def test_exit_base_context(self): |
659 | 661 | ctx.run(lambda: None) |
660 | 662 | self.assertEqual(switches, [ctx, None]) |
661 | 663 |
|
| 664 | + def test_reenter_default_context(self): |
| 665 | + _testcapi.clear_context_stack() |
| 666 | + # contextvars.copy_context() creates the thread's default context (via |
| 667 | + # the context_get C function). |
| 668 | + ctx = contextvars.copy_context() |
| 669 | + with self.context_watcher(0) as switches: |
| 670 | + ctx.run(lambda: None) |
| 671 | + self.assertEqual(len(switches), 2) |
| 672 | + self.assertEqual(switches[0], ctx) |
| 673 | + base_ctx = switches[1] |
| 674 | + self.assertIsNotNone(base_ctx) |
| 675 | + self.assertIsNot(base_ctx, ctx) |
| 676 | + with self.assertRaisesRegex(RuntimeError, 'already entered'): |
| 677 | + base_ctx.run(lambda: None) |
| 678 | + |
| 679 | + def test_default_context_enter(self): |
| 680 | + _testcapi.clear_context_stack() |
| 681 | + with self.context_watcher(0) as switches: |
| 682 | + ctx = contextvars.copy_context() |
| 683 | + ctx.run(lambda: None) |
| 684 | + self.assertEqual(len(switches), 3) |
| 685 | + base_ctx = switches[0] |
| 686 | + self.assertIsNotNone(base_ctx) |
| 687 | + self.assertEqual(switches, [base_ctx, ctx, base_ctx]) |
| 688 | + |
| 689 | + def test_default_context_exit_during_thread_cleanup(self): |
| 690 | + # Context watchers are per-interpreter, not per-thread. |
| 691 | + # https://discuss.python.org/t/v3-14a1-design-limitations-of-pycontext-addwatcher/68177 |
| 692 | + with self.context_watcher(0) as switches: |
| 693 | + def _thread_main(): |
| 694 | + _testcapi.clear_context_stack() |
| 695 | + # contextvars.copy_context() creates the thread's default |
| 696 | + # context (via the context_get C function). |
| 697 | + contextvars.copy_context() |
| 698 | + # This test only cares about the final switch that happens when |
| 699 | + # exiting the thread's default context during thread cleanup. |
| 700 | + switches.clear() |
| 701 | + |
| 702 | + thread = threading.Thread(target=_thread_main) |
| 703 | + thread.start() |
| 704 | + thread.join() |
| 705 | + self.assertEqual(switches, [None]) |
| 706 | + |
| 707 | + def test_thread_cleanup_with_entered_context(self): |
| 708 | + unraisables = [] |
| 709 | + try: |
| 710 | + with catch_unraisable_exception() as cm: |
| 711 | + with self.context_watcher(0) as switches: |
| 712 | + def _thread_main(): |
| 713 | + _testcapi.clear_context_stack() |
| 714 | + ctx = contextvars.copy_context() |
| 715 | + _testcapi.context_enter(ctx) |
| 716 | + switches.clear() |
| 717 | + |
| 718 | + thread = threading.Thread(target=_thread_main) |
| 719 | + thread.start() |
| 720 | + thread.join() |
| 721 | + unraisables.append(cm.unraisable) |
| 722 | + self.assertEqual(switches, []) |
| 723 | + self.assertEqual(len(unraisables), 1) |
| 724 | + self.assertIsNotNone(unraisables[0]) |
| 725 | + self.assertRegex(unraisables[0].err_msg, |
| 726 | + r'^Exception ignored during reset of thread state') |
| 727 | + self.assertRegex(str(unraisables[0].exc_value), r'still entered') |
| 728 | + finally: |
| 729 | + # Break reference cycle |
| 730 | + unraisables = None |
| 731 | + |
| 732 | + |
662 | 733 | if __name__ == "__main__": |
663 | 734 | unittest.main() |
0 commit comments