Skip to content

gh-128384: Use a context variable for warnings.catch_warnings #130010

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

Merged
merged 47 commits into from
Apr 9, 2025

Conversation

nascheme
Copy link
Member

@nascheme nascheme commented Feb 11, 2025

Make warnings.catch_warnings() use a context variable for holding the warning filtering state if the sys.flags.context_aware_warnings flag is set to true. This makes using the context manager thread-safe in multi-threaded programs and safe for asyncio coroutines and tasks. The flag is true by default in free-threaded builds and is otherwise false. The value of the flag can be overridden by the the -X thread_safe_warnings command-line option or by the PYTHON_CONTEXT_AWARE_WARNINGS environment variable.

It is expected that one day the flag might default to true for all Python builds, not just the free-threaded one. However, I think it makes sense to not commit to a schedule to do that until we have a better idea of what code is affected by this change. Feedback from people using the free-threaded build should provide guidance.

This PR is on top of gh-128209.

A previous version of this PR used a single flag to control both behavior of Thread inheriting the context and also the catch_warnings context manager. However, based on some feedback, I've decided that two flags makes things more clear. Likely programs would like to set both flags to true to get the most intuitive behavior.

When the context_aware_warnings flag is true and a catch_warnings() context is active, added filters go into a list of filters stored in the contextvar, not the warnings.filters list. The filters in warnings.filters are still applied but they apply after the contextvar filters.

That difference with how warnings.filters works is probably the most likely thing to cause broken user code. In the unit tests, I had to change a number of references to warnings.filters to warnings._get_filters(). That function returns the list of filters from the current context or the global filters if there is no context active. Perhaps _get_filters() should become a public function? I think it would be better if examining and manipulating that list was discouraged and so that's why I made the function non-public, at least for now.

I created _py_warnings.py to contain the Python implementation for the warnings module. This matches other modules like decimal that have Python and C versions of the same module. In the case of warnings things are a bit more complicated since you can assign to module globals (the filters list or the showwarning() function are common to override). This is a cleaner organization, IMHO. Now warnings.py just imports the parts it needs and the unit tests are a bit simpler.

I cleaned up the unit tests for warnings a bit. Passing module to catch_warnings() actually serves no purpose. Since sys.modules['warnings'] is already swapped to the module under test, the code does the right thing even when that parameter is not given. I also replaced calls to original_warnings.catch_warnings() with self.module.catch_warnings(). Both those calls do the same thing but the second spelling seems less confusing to me.

pyperformance comparison


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

Since the unittest/runner.py establishes a new warnings context with
`catch_warnings`, these tests must use `warnings._get_filters()` to
retrieve the current list of filters, not the `warnings.filters` global.
@nascheme nascheme marked this pull request as ready for review February 13, 2025 21:03
@kumaraditya303
Copy link
Contributor

I think we should add some tests which use both asyncio and warnings to check for correct contexts in async environments too.

@nascheme nascheme force-pushed the gh-128384-warnings-contextvar branch from 51da563 to 543927b Compare April 9, 2025 19:50
nascheme added 2 commits April 9, 2025 13:53
This seems a bit simpler since gather() will create tasks as needed.
This is needed because unit tests are now run with "-W error" on the
command line in CI (pythonGH-128770).  We want to start with an empty list of
filters to avoid supurious errors.
@nascheme nascheme merged commit d687900 into python:main Apr 9, 2025
42 checks passed
seehwan pushed a commit to seehwan/cpython that referenced this pull request Apr 16, 2025
…ythongh-130010)

Make `warnings.catch_warnings()` use a context variable for holding
the warning filtering state if the `sys.flags.context_aware_warnings`
flag is set to true.  This makes using the context manager thread-safe in
multi-threaded programs.

Add the `sys.flags.thread_inherit_context` flag.  If true, starting a new
thread with `threading.Thread` will use a copy of the context
from the caller of `Thread.start()`.

Both these flags are set to true by default for the free-threaded build
and false for the default build.

Move the Python implementation of warnings.py into _py_warnings.py.

Make _contextvars a builtin module.

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
@hugovk
Copy link
Member

hugovk commented Apr 16, 2025

@nascheme @kumaraditya303 Please could you check the buildbots? https://buildbot.python.org/#/release_status

AMD64 CentOS9 NoGIL Refleaks 3.x (tier-1: breaks need immediate fix/revert) and ARM64 MacOS M1 Refleaks NoGIL 3.x (tier-2: breaks need fix/revert within 24h) have been failing for 6 days, and this PR seems a likely cause?

For example:

test_threaded_context (test.test_warnings.CThreadTests.test_threaded_context) ... Warning -- Uncaught thread exception: TypeErrorException in thread Thread-5 (run_a):
Traceback (most recent call last):
  File "/Users/ec2-user/buildbot/buildarea/3.x.itamaro-macos-arm64-aws.macos-with-brew.refleak.nogil/build/Lib/threading.py", line 1079, in _bootstrap_inner
    self._context.run(self.run)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/Users/ec2-user/buildbot/buildarea/3.x.itamaro-macos-arm64-aws.macos-with-brew.refleak.nogil/build/Lib/threading.py", line 1021, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ec2-user/buildbot/buildarea/3.x.itamaro-macos-arm64-aws.macos-with-brew.refleak.nogil/build/Lib/test/test_warnings/__init__.py", line 1650, in run_a
    self.module.warn('run_a warning', UserWarning)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ec2-user/buildbot/buildarea/3.x.itamaro-macos-arm64-aws.macos-with-brew.refleak.nogil/build/Lib/_py_warnings.py", line 227, in _showwarnmsg
    raise TypeError("warnings.showwarning() must be set to a "
                    "function or method")TypeError: warnings.showwarning() must be set to a function or method

https://buildbot.python.org/#/builders/1368/builds/2970/steps/6/logs/stdio

hugovk added a commit to hugovk/cpython that referenced this pull request Apr 16, 2025
@nascheme
Copy link
Member Author

I'll look into it. My guess is it is a flaky test and the test could just be disabled as a stop-gap.

@hugovk
Copy link
Member

hugovk commented Apr 16, 2025

Thanks, here's a draft revert PR has passing buildbots: #132601.

@nascheme
Copy link
Member Author

I believe this should fix the test failure without reverting the whole change: GH-132611. I'm waiting for CI tests to complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants