-
-
Notifications
You must be signed in to change notification settings - Fork 31.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Context manager support for contextvars.Context
#99633
Comments
The idea should probably be reviewed by @1st1. |
Friendly ping @1st1 |
+1 |
Maybe someone could contribute a PR? |
@gvanrossum I opened a PR at the same time I opened this feature request, see #99634. I just rebased it onto latest |
This isn't just I'm a little surprised a thing called a Anyway, the PR looks great, but until Python 3.14 comes out, is there any way we can get this context manager behaviour on 3.12 (or even earlier!)? |
I'm not sure we can. Neither 3.12 and 3.13 accept features now (per PEP 719, features to 3.13 are no more accepted since May). |
That's not what I meant: I was curious if there's any way to do in pure python, rather than doing some kind of backported pypi package containing the C changes, what's in #99634 such that it could be used in, say, 3.11+ |
Oh sorry for misunderstanding it! Mmh, unless we can implicitly call the |
No, it's not possible. Probably the easiest solution is to use Cython to create an adapter class that calls the C functions Edit: This won't work; see my comment below. |
I've left a comment in the PR -- I'm not sure this will jive well with async/await. Let's start by adding tests that would torture this idea to make sure the API is properly composable. |
@1st1 - even if it doesn't, I'd still like to see this land: in my context, pardon the pun, I'm not using async at all. I have to admit, I find the documentation around this for async use pretty confusing: it's not at all clear to me where the scope of the context begins and ends, there's mention of "Task", but there's nothing other than a some stuff the reader has to infer about there the scope of the effect of Ignoring any implementation details, I feel a context manager should work in the async world, and I think the following would make it very clear where the scope of the context lies: async def handle_request(reader, writer):
with contextvars.copy_context():
addr = writer.transport.get_extra_info('socket').getpeername()
client_addr_var.set(addr)
# In any code that we call is now possible to get
# client's address by calling 'client_addr_var.get()'.
while True:
line = await reader.readline()
print(line)
if not line.strip():
break
writer.write(line)
writer.write(render_goodbye())
writer.close() |
In its current state, PR #99634 doesn't work with async/await. Specifically, the following hangs after printing some exceptions: $ ./python -m asyncio
>>> import asyncio
>>> import contextvars
>>> async def foo():
... with contextvars.copy_context():
... await asyncio.sleep(0)
...
>>> await foo()
Exception in callback <_asyncio.TaskStepMethWrapper object at 0x7a5c946ae800>()
handle: <Handle <_asyncio.TaskStepMethWrapper object at 0x7a5c946ae800>()>
Traceback (most recent call last):
File "/home/rhansen/floss/cpython/Lib/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *self._args)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: cannot exit context: thread state references a different context object
Exception in callback <_asyncio.TaskStepMethWrapper object at 0x7a5c946ae850>()
handle: <Handle <_asyncio.TaskStepMethWrapper object at 0x7a5c946ae850>()>
Traceback (most recent call last):
File "/home/rhansen/floss/cpython/Lib/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *self._args)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: cannot enter context: <_contextvars.Context object at 0x7a5c9469c890> is already entered I spent some time trying to understand coroutine internals to figure out what's going wrong. From what I learned, the issue is that each coroutine "step" (the code executed between each Specifically, in the
Original generators suffer from a similar problem (the current context can change arbitrarily during It seems to me that I am willing to code up a fix for this, but I don't know enough about Python's internals to feel comfortable diving in. Any direction would be appreciated. In particular, I'd like some pointers on what code I should touch to make We could continue with the PR as-is except document that |
…ched" Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
…ched" Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
This was the idea I had in https://peps.python.org/pep-0550/ -- ultimately rejected for its complexity and runtime impact. I'd say it's unlikely that we can convince enough people to do that (and even I myself don't think we need that anymore). That said given that you've already spent a lot of time on this, I recommend you to read that PEP because it did have a complete reference implementation and all of its ideas were validated to work. That said, I'm looking at this PR's motivation and I think we can add two "semi-private" low-level APIs that would mirror the C API and allow people to build context managers if they absolutely have to. I'd just add |
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Change `PyObject *` to/from `PyContext *` to reduce the amount of casting and improve readability.
…rowed Improve readability by moving destination assignment next to source reset, and comment that the ref is stolen.
No public API provides access to the current context yet (only a new copy), however: * This is good defensive practice. * This improves code readability. * Context watchers are now notified about the initial context. * A planned future commit will make it possible for users to access the thread's initial context object. Without this change, users would be able to enter the context a second time, causing a cycle in the context stack.
This will make it easier to refactor `_PyContext_Enter` and `_PyContext_Exit` for a planned feature.
Add a new `_context` property to generator (and coroutine) objects to get/set the "current context" that is observed by (and only by) the generator and the functions it calls. When `generator._context` is set to `None` (the default), the generator is called a "dependent generator". It behaves the same as it always has: the "current context" observed by the generator is the thread's context. This means that the observed context can change arbitrarily during a `yield`; the generator *depends* on the sender to enter the appropriate context before it calls `generator.send`. When `generator._context` is set to a `contextvars.Context` object, the generator is called an "independent generator". It acts more like a separate thread with its own independent context stack. The value of `_context` is the head of that independent stack. Whenever the generator starts or resumes execution (via `generator.send`), the current context temporarily becomes the generator's associated context. When the generator yields, returns, or propagates an exception, the current context reverts back to what it was before. The generator's context is *independent* from the sender's context. If an independent generator calls `contextvars.Context.run`, then the value of the `_context` property will (temporarily) change to the newly entered context. If an independent generator sends a value to a second independent generator, the second generator's context will shadow the first generator's context until the second generator returns or yields. The `generator._context` property is private for now until experience and feedback is collected. Nothing is using this yet, but that will change in future commits. Motivations for this change: * First, this change makes it possible for a future commit to add context manager support to `contextvars.Context`. A `yield` after entering a context causes execution to leave the generator with a different context at the top of the context stack than when execution started. Swapping contexts in and out when execution suspends and resumes can only be done by the generator itself. * Second, this paves the way for a public API that will enable developers to guarantee that the context remains consistent throughout a generator's execution. Right now the context can change arbitrarily during a `yield`, which can lead to subtle bugs that are difficult to root cause. (Coroutines run by an asyncio event loop do not suffer from this same problem because asyncio manually sets the context each time it executes a step of an asynchronous function. See the call to `contextvars.Context.run` in `asyncio.Handle._run`.) * Finally, this makes it possible to move the responsibility for activating an async coroutine's context from the event loop to the coroutine, where it more naturally belongs (alongside the rest of the execution state such as local variable bindings and the instruction pointer). This ensures consistent behavior between different event loop implementations. Example: ```python import contextvars cvar = contextvars.ContextVar('cvar', default='initial') def make_generator(): yield cvar.get() yield cvar.get() yield cvar.get() yield cvar.get() cvar.set('updated by generator') yield cvar.get() gen = make_generator() print('1.', next(gen)) def callback(): cvar.set('updated by callback') print('2.', next(gen)) contextvars.copy_context().run(callback) print('3.', next(gen)) cvar.set('updated at top level') print('4.', next(gen)) print('5.', next(gen)) print('6.', cvar.get()) ``` The above prints: ``` 1. initial 2. updated by callback 3. initial 4. updated at top level 5. updated by generator 6. updated by generator ``` Now add the following line after the creation of the generator: ```python gen._context = contextvars.copy_context() ``` With that change, the script now outputs: ``` 1. initial 2. initial 3. initial 4. initial 5. updated by generator 6. updated by top level ```
The functions `_PyGen_ActivateContext` and `_PyGen_DeactivateContext` are called every time a value or exception is sent to a coroutine. These functions are no-ops for dependent coroutines (coroutines without their own independent context stack). Coroutines are dependent by default, and the vast majority of performance-sensitive coroutines are expected to be dependent, so move the check that determines whether the coroutine is dependent or independent to an inline function to speed up send calls.
Change `PyObject *` to/from `PyContext *` to reduce the amount of casting and improve readability.
…rowed Improve readability by moving destination assignment next to source reset, and comment that the ref is stolen.
No public API provides access to the current context yet (only a new copy), however: * This is good defensive practice. * This improves code readability. * Context watchers are now notified about the initial context. * A planned future commit will make it possible for users to access the thread's initial context object. Without this change, users would be able to enter the context a second time, causing a cycle in the context stack.
This will make it easier to refactor `_PyContext_Enter` and `_PyContext_Exit` for a planned feature.
Add a new `_context` property to generator (and coroutine) objects to get/set the "current context" that is observed by (and only by) the generator and the functions it calls. When `generator._context` is set to `None` (the default), the generator is called a "dependent generator". It behaves the same as it always has: the "current context" observed by the generator is the thread's context. This means that the observed context can change arbitrarily during a `yield`; the generator *depends* on the sender to enter the appropriate context before it calls `generator.send`. When `generator._context` is set to a `contextvars.Context` object, the generator is called an "independent generator". It acts more like a separate thread with its own independent context stack. The value of `_context` is the head of that independent stack. Whenever the generator starts or resumes execution (via `generator.send`), the current context temporarily becomes the generator's associated context. When the generator yields, returns, or propagates an exception, the current context reverts back to what it was before. The generator's context is *independent* from the sender's context. If an independent generator calls `contextvars.Context.run`, then the value of the `_context` property will (temporarily) change to the newly entered context. If an independent generator sends a value to a second independent generator, the second generator's context will shadow the first generator's context until the second generator returns or yields. The `generator._context` property is private for now until experience and feedback is collected. Nothing is using this yet, but that will change in future commits. Motivations for this change: * First, this change makes it possible for a future commit to add context manager support to `contextvars.Context`. A `yield` after entering a context causes execution to leave the generator with a different context at the top of the context stack than when execution started. Swapping contexts in and out when execution suspends and resumes can only be done by the generator itself. * Second, this paves the way for a public API that will enable developers to guarantee that the context remains consistent throughout a generator's execution. Right now the context can change arbitrarily during a `yield`, which can lead to subtle bugs that are difficult to root cause. (Coroutines run by an asyncio event loop do not suffer from this same problem because asyncio manually sets the context each time it executes a step of an asynchronous function. See the call to `contextvars.Context.run` in `asyncio.Handle._run`.) * Finally, this makes it possible to move the responsibility for activating an async coroutine's context from the event loop to the coroutine, where it more naturally belongs (alongside the rest of the execution state such as local variable bindings and the instruction pointer). This ensures consistent behavior between different event loop implementations. Example: ```python import contextvars cvar = contextvars.ContextVar('cvar', default='initial') def make_generator(): yield cvar.get() yield cvar.get() yield cvar.get() yield cvar.get() cvar.set('updated by generator') yield cvar.get() gen = make_generator() print('1.', next(gen)) def callback(): cvar.set('updated by callback') print('2.', next(gen)) contextvars.copy_context().run(callback) print('3.', next(gen)) cvar.set('updated at top level') print('4.', next(gen)) print('5.', next(gen)) print('6.', cvar.get()) ``` The above prints: ``` 1. initial 2. updated by callback 3. initial 4. updated at top level 5. updated by generator 6. updated by generator ``` Now add the following line after the creation of the generator: ```python gen._context = contextvars.copy_context() ``` With that change, the script now outputs: ``` 1. initial 2. initial 3. initial 4. initial 5. updated by generator 6. updated by top level ```
The functions `_PyGen_ActivateContext` and `_PyGen_DeactivateContext` are called every time a value or exception is sent to a coroutine. These functions are no-ops for dependent coroutines (coroutines without their own independent context stack). Coroutines are dependent by default, and the vast majority of performance-sensitive coroutines are expected to be dependent, so move the check that determines whether the coroutine is dependent or independent to an inline function to speed up send calls.
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for gh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
…4776) Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for gh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum). Co-authored-by: Richard Hansen <[email protected]> Co-authored-by: Victor Stinner <[email protected]>
…5532) Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum). Co-authored-by: Richard Hansen <[email protected]> Co-authored-by: Victor Stinner <[email protected]>
Feature or enhancement
I would like to add
__enter__
and__exit__
methods tocontextvars.Context
such that these two are mostly equivalent:and:
Pitch
This makes it possible to combine Context enter/exit with other context managers:
My personal motivating interest is controlling ContextVar values in pytest fixtures. For example:
Previous discussion
https://stackoverflow.com/q/59264158
Linked PRs
contextvars.Context
#99634The text was updated successfully, but these errors were encountered: