| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2781ec9 commit eb110b6
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -144,16 +144,72 @@ Manual Context Management | |||
| 144 | 144 | To get a copy of the current context use the | |
| 145 | 145 | :func:`~contextvars.copy_context` function. | |
| 146 | 146 | ||
| 147 | + When *entering* a Context, either by calling the :meth:`Context.run` method | ||
| 148 | + or using the Context object as a :term:`context manager`, the Context becomes | ||
| 149 | + the *current Context*. When *exiting* the current Context, either by | ||
| 150 | + returning from the callback passed to :meth:`Context.run` or by exiting the | ||
| 151 | + :keyword:`with` statement suite, the current Context reverts back to what it | ||
| 152 | + was before the exited Context was entered. | ||
| 153 | + | ||
| 154 | + Attempting to do any of the following will raise a :exc:`RuntimeError`: | ||
| 155 | + | ||
| 156 | + * Entering an already entered Context. | ||
| 157 | + * Exiting from a Context that is not the current Context. | ||
| 158 | + * Exiting a Context from a different thread than the one used to enter the | ||
| 159 | + Context. | ||
| 160 | + | ||
| 161 | + After exiting a Context, it can later be re-entered (from any thread). | ||
| 162 | + | ||
| 163 | + Any changes to :class:`ContextVar` values via the :meth:`ContextVar.set` | ||
| 164 | + method are recorded in the current Context. The :meth:`ContextVar.get` | ||
| 165 | + method returns the value associated with the current Context. Thus, exiting | ||
| 166 | + a Context effectively reverts any changes made to context variables while the | ||
| 167 | + Context was entered. (If desired, the values can be restored by re-entering | ||
| 168 | + the Context.) | ||
| 169 | + | ||
| 147 | 170 | Context implements the :class:`collections.abc.Mapping` interface. | |
| 148 | 171 | ||
| 172 | + .. versionadded:: 3.12 | ||
| 173 | + A Context object can be used as a :term:`context manager`. The | ||
| 174 | + :meth:`Context.__enter__` and :meth:`Context.__exit__` methods | ||
| 175 | + (automatically called by the :keyword:`with` statement) enters and exits | ||
| 176 | + the Context, respectively. The value returned from | ||
| 177 | + :meth:`Context.__enter__`, and thus bound to the identifier given in the | ||
| 178 | + :keyword:`with` statement's :keyword:`!as` clause if present, is the | ||
| 179 | + Context object itself. | ||
| 180 | + | ||
| 181 | + Example: | ||
| 182 | + | ||
| 183 | + .. testcode:: | ||
| 184 | + | ||
| 185 | + import contextvars | ||
| 186 | + | ||
| 187 | + var = contextvars.ContextVar("var") | ||
| 188 | + var.set("initial") | ||
| 189 | + | ||
| 190 | + # Copy the current Context and enter it. | ||
| 191 | + with contextvars.copy_context() as ctx: | ||
| 192 | + var.set("updated") | ||
| 193 | + assert var in ctx | ||
| 194 | + assert ctx[var] == "updated" | ||
| 195 | + assert var.get() == "updated" | ||
| 196 | + | ||
| 197 | + # Exited ctx, so the value of var should have reverted. | ||
| 198 | + assert var.get() == "initial" | ||
| 199 | + # But the updated value is still recorded in ctx. | ||
| 200 | + assert ctx[var] == "updated" | ||
| 201 | + | ||
| 202 | + # Re-entering ctx should restore the updated value of var. | ||
| 203 | + with ctx: | ||
| 204 | + assert var.get() == "updated" | ||
| 205 | + | ||
| 149 | 206 | .. method:: run(callable, *args, **kwargs) | |
| 150 | 207 | ||
| 151 | - Execute ``callable(*args, **kwargs)`` code in the context object | ||
| 152 | - the *run* method is called on. Return the result of the execution | ||
| 153 | - or propagate an exception if one occurred. | ||
| 208 | + Enters the Context, executes ``callable(*args, **kwargs)``, then exits the | ||
| 209 | + Context. Returns *callable*'s return value, or propagates an exception if | ||
| 210 | + one occurred. | ||
| 154 | 211 | ||
| 155 | - Any changes to any context variables that *callable* makes will | ||
| 156 | - be contained in the context object:: | ||
| 212 | + Example:: | ||
| 157 | 213 | ||
| 158 | 214 | var = ContextVar('var') | |
| 159 | 215 | var.set('spam') | |
@@ -181,10 +237,6 @@ Manual Context Management | |||
| 181 | 237 | # However, outside of 'ctx', 'var' is still set to 'spam': | |
| 182 | 238 | # var.get() == 'spam' | |
| 183 | 239 | ||
| 184 | - The method raises a :exc:`RuntimeError` when called on the same | ||
| 185 | - context object from more than one OS thread, or when called | ||
| 186 | - recursively. | ||
| 187 | - | ||
| 188 | 240 | .. method:: copy() | |
| 189 | 241 | ||
| 190 | 242 | Return a shallow copy of the context object. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,6 +3,7 @@ | |||
| 3 | 3 | import functools | |
| 4 | 4 | import gc | |
| 5 | 5 | import random | |
| 6 | + import threading | ||
| 6 | 7 | import time | |
| 7 | 8 | import unittest | |
| 8 | 9 | import weakref | |
@@ -360,6 +361,74 @@ def sub(num): | |||
| 360 | 361 | tp.shutdown() | |
| 361 | 362 | self.assertEqual(results, list(range(10))) | |
| 362 | 363 | ||
| 364 | + @isolated_context | ||
| 365 | + def test_context_manager_works(self): | ||
| 366 | + cvar = contextvars.ContextVar('cvar', default='initial') | ||
| 367 | + self.assertEqual(cvar.get(), 'initial') | ||
| 368 | + with contextvars.copy_context(): | ||
| 369 | + self.assertEqual(cvar.get(), 'initial') | ||
| 370 | + cvar.set('updated') | ||
| 371 | + self.assertEqual(cvar.get(), 'updated') | ||
| 372 | + self.assertEqual(cvar.get(), 'initial') | ||
| 373 | + | ||
| 374 | + def test_context_manager_as_binding(self): | ||
| 375 | + ctx = contextvars.copy_context() | ||
| 376 | + with ctx as ctx_as_binding: | ||
| 377 | + self.assertIs(ctx_as_binding, ctx) | ||
| 378 | + | ||
| 379 | + @isolated_context | ||
| 380 | + def test_context_manager_enter_again_after_exit(self): | ||
| 381 | + cvar = contextvars.ContextVar('cvar', default='initial') | ||
| 382 | + self.assertEqual(cvar.get(), 'initial') | ||
| 383 | + with contextvars.copy_context() as ctx: | ||
| 384 | + cvar.set('updated') | ||
| 385 | + self.assertEqual(cvar.get(), 'updated') | ||
| 386 | + self.assertEqual(cvar.get(), 'initial') | ||
| 387 | + with ctx: | ||
| 388 | + self.assertEqual(cvar.get(), 'updated') | ||
| 389 | + self.assertEqual(cvar.get(), 'initial') | ||
| 390 | + | ||
| 391 | + @threading_helper.requires_working_threading() | ||
| 392 | + def test_context_manager_rejects_exit_from_different_thread(self): | ||
| 393 | + ctx = contextvars.copy_context() | ||
| 394 | + thread = threading.Thread(target=ctx.__enter__) | ||
| 395 | + thread.start() | ||
| 396 | + thread.join() | ||
| 397 | + with self.assertRaises(RuntimeError): | ||
| 398 | + ctx.__exit__(None, None, None) | ||
| 399 | + | ||
| 400 | + def test_context_manager_rejects_recursive_enter_mgr_then_mgr(self): | ||
| 401 | + with contextvars.copy_context() as ctx: | ||
| 402 | + with self.assertRaises(RuntimeError): | ||
| 403 | + with ctx: | ||
| 404 | + pass | ||
| 405 | + | ||
| 406 | + def test_context_manager_rejects_recursive_enter_mgr_then_run(self): | ||
| 407 | + with contextvars.copy_context() as ctx: | ||
| 408 | + with self.assertRaises(RuntimeError): | ||
| 409 | + ctx.run(lambda: None) | ||
| 410 | + | ||
| 411 | + def test_context_manager_rejects_recursive_enter_run_then_mgr(self): | ||
| 412 | + ctx = contextvars.copy_context() | ||
| 413 | + | ||
| 414 | + def fn(): | ||
| 415 | + with self.assertRaises(RuntimeError): | ||
| 416 | + with ctx: | ||
| 417 | + pass | ||
| 418 | + | ||
| 419 | + ctx.run(fn) | ||
| 420 | + | ||
| 421 | + def test_context_manager_rejects_noncurrent_exit(self): | ||
| 422 | + with contextvars.copy_context() as ctx: | ||
| 423 | + with contextvars.copy_context(): | ||
| 424 | + with self.assertRaises(RuntimeError): | ||
| 425 | + ctx.__exit__(None, None, None) | ||
| 426 | + | ||
| 427 | + def test_context_manager_rejects_nonentered_exit(self): | ||
| 428 | + ctx = contextvars.copy_context() | ||
| 429 | + with self.assertRaises(RuntimeError): | ||
| 430 | + ctx.__exit__(None, None, None) | ||
| 431 | + | ||
| 363 | 432 | ||
| 364 | 433 | # HAMT Tests | |
| 365 | 434 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -691,6 +691,7 @@ Manus Hand | |||
| 691 | 691 | Andreas Hangauer | |
| 692 | 692 | Milton L. Hankins | |
| 693 | 693 | Carl Bordum Hansen | |
| 694 | + Richard Hansen | ||
| 694 | 695 | Stephen Hansen | |
| 695 | 696 | Barry Hantman | |
| 696 | 697 | Lynda Hardman | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1 @@ | |||
| 1 | + Add :term:`context manager` methods to :class:`contextvars.Context`. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -153,7 +153,6 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx) | |||
| 153 | 153 | } | |
| 154 | 154 | ||
| 155 | 155 | if (ts->context != (PyObject *)ctx) { | |
| 156 | - /* Can only happen if someone misuses the C API */ | ||
| 157 | 156 | PyErr_SetString(PyExc_RuntimeError, | |
| 158 | 157 | "cannot exit context: thread state references " | |
| 159 | 158 | "a different context object"); | |
@@ -558,6 +557,69 @@ context_tp_contains(PyContext *self, PyObject *key) | |||
| 558 | 557 | } | |
| 559 | 558 | ||
| 560 | 559 | ||
| 560 | + /*[clinic input] | ||
| 561 | + _contextvars.Context.__enter__ | ||
| 562 | + | ||
| 563 | + Context manager enter. | ||
| 564 | + | ||
| 565 | + Automatically called by the 'with' statement. Using the Context object as a | ||
| 566 | + context manager is an alternative to calling the Context.run() method. Example | ||
| 567 | + usage: | ||
| 568 | + | ||
| 569 | + var = contextvars.ContextVar('var') | ||
| 570 | + var.set('initial') | ||
| 571 | + | ||
| 572 | + with contextvars.copy_context(): | ||
| 573 | + # The current Context is a new copy of the previous Context. Updating a | ||
| 574 | + # context variable inside this 'with' statement only affects the new | ||
| 575 | + # copy. | ||
| 576 | + var.set('updated') | ||
| 577 | + do_something_interesting() | ||
| 578 | + | ||
| 579 | + # Now that the 'with' statement is done executing, the value of the 'var' | ||
| 580 | + # context variable has reverted back to its value before the 'with' | ||
| 581 | + # statement. | ||
| 582 | + assert var.get() == 'initial' | ||
| 583 | + [clinic start generated code]*/ | ||
| 584 | + | ||
| 585 | + static PyObject * | ||
| 586 | + _contextvars_Context___enter___impl(PyContext *self) | ||
| 587 | + /*[clinic end generated code: output=7374aea8983b777a input=22868e8274d3cd32]*/ | ||
| 588 | + { | ||
| 589 | + PyThreadState *ts = _PyThreadState_GET(); | ||
| 590 | + if (_PyContext_Enter(ts, (PyObject *)self)) { | ||
| 591 | + return NULL; | ||
| 592 | + } | ||
| 593 | + return Py_NewRef(self); | ||
| 594 | + } | ||
| 595 | + | ||
| 596 | + | ||
| 597 | + /*[clinic input] | ||
| 598 | + _contextvars.Context.__exit__ | ||
| 599 | + exc_type: object | ||
| 600 | + exc_val: object | ||
| 601 | + exc_tb: object | ||
| 602 | + / | ||
| 603 | + | ||
| 604 | + Context manager exit. | ||
| 605 | + | ||
| 606 | + Automatically called at the conclusion of a 'with' statement when the Context is | ||
| 607 | + used as a context manager. See the Context.__enter__() method for more details. | ||
| 608 | + [clinic start generated code]*/ | ||
| 609 | + | ||
| 610 | + static PyObject * | ||
| 611 | + _contextvars_Context___exit___impl(PyContext *self, PyObject *exc_type, | ||
| 612 | + PyObject *exc_val, PyObject *exc_tb) | ||
| 613 | + /*[clinic end generated code: output=4608fa9151f968f1 input=ff70cbbf6a112b1d]*/ | ||
| 614 | + { | ||
| 615 | + PyThreadState *ts = _PyThreadState_GET(); | ||
| 616 | + if (_PyContext_Exit(ts, (PyObject *)self)) { | ||
| 617 | + return NULL; | ||
| 618 | + } | ||
| 619 | + Py_RETURN_NONE; | ||
| 620 | + } | ||
| 621 | + | ||
| 622 | + | ||
| 561 | 623 | /*[clinic input] | |
| 562 | 624 | _contextvars.Context.get | |
| 563 | 625 | key: object | |
@@ -677,6 +739,8 @@ context_run(PyContext *self, PyObject *const *args, | |||
| 677 | 739 | ||
| 678 | 740 | ||
| 679 | 741 | static PyMethodDef PyContext_methods[] = { | |
| 742 | + _CONTEXTVARS_CONTEXT___ENTER___METHODDEF | ||
| 743 | + _CONTEXTVARS_CONTEXT___EXIT___METHODDEF | ||
| 680 | 744 | _CONTEXTVARS_CONTEXT_GET_METHODDEF | |
| 681 | 745 | _CONTEXTVARS_CONTEXT_ITEMS_METHODDEF | |
| 682 | 746 | _CONTEXTVARS_CONTEXT_KEYS_METHODDEF | |
| Back | FazBrowse Home | New Git URL |
0 commit comments