gh-128384: Add thread-safe context manager to "warnings" module by nascheme · Pull Request #128300 · python/cpython (original) (raw)

This PR adds a new thread-local (and async friendly) context manager that uses contextvars. The recommended pattern for new code that doesn't care about backwards compatibility would be:

with warnings.local_context() as ctx:
    ctx.simplefilter(...)
    ...

For code that wants to be compatible with older versions of Python, the suggested code is:

import warnings
try:
    warn_ctx = warnings.get_context
except AttributeError:
   def warn_ctx():
       return warnings

with warn_ctx().catch_warnings():
    warn_ctx().simplefilter(...)
   ...

with warn_ctx().catch_warnings(record=True) as log:
    ...

This change retains warnings.filters and warnings.catch_warnings() as mechanisms that use process global state. Conceptually, there will be two sets of warning filters: the legacy process global warnings.filters list and the thread local get_context()._filters list. The context object returned by get_context() has an API that overlaps with the warnings module function API.

Perhaps we could eventually warn when the process global versions are used but I think that would be many years in the future and not worth considering now.

It would be intuitive if the thread local filtering was inherited when a new thread is created. I've created a separate PR that adds this feature to threading.Thread: gh-128209. That's a potentially controversial change but I think it's the correct thing to do. It will make contextvars behave similarly between asyncio tasks (which already inherit context) and threads. I think people will expect the warnings context manager to have lexical scope. If you see the code:

with warnings.local_context() as ctx:
    ctx.simplefilter(MyWarning)
    inner_function()

Then you would assume that inner_function() is going to have MyWarning filtered, even if it internally spawns a thread.

Regarding backwards compatibility, I did a code search and there are quite a few examples for code that manipulates warnings.filters directly, either mutating it or assigning a different list to the module global. There is also code that does this:

warnings.simplefilter(...)
...
warnings.filters.pop(0)

📚 Documentation preview 📚: https://cpython-previews--128300.org.readthedocs.build/