diff --git a/.gitignore b/.gitignore index 3bf5187d531c23..660a2524144d21 100644 --- a/.gitignore +++ b/.gitignore @@ -140,6 +140,7 @@ Tools/unicode/data/ /.ccache /cross-build*/ /jit_stencils*.h +/jit_unwind_info*.h /platform /profile-clean-stamp /profile-run-stamp diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 5f325df55705bd..772f2633b29443 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -366,7 +366,8 @@ instantiation, of which this module provides three different variants: delays, it now always returns the IP address. -.. class:: SimpleHTTPRequestHandler(request, client_address, server, directory=None) +.. class:: SimpleHTTPRequestHandler(request, client_address, server, \ + *, directory=None, extra_response_headers=None) This class serves files from the directory *directory* and below, or the current directory if *directory* is not provided, directly @@ -378,6 +379,9 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.9 The *directory* parameter accepts a :term:`path-like object`. + .. versionchanged:: next + Added *extra_response_headers* parameter. + A lot of the work, such as parsing the request, is done by the base class :class:`BaseHTTPRequestHandler`. This class implements the :func:`do_GET` and :func:`do_HEAD` functions. @@ -408,6 +412,15 @@ instantiation, of which this module provides three different variants: This dictionary is no longer filled with the default system mappings, but only contains overrides. + .. attribute:: extra_response_headers + + A sequence of ``(name, value)`` pairs containing user-defined extra HTTP + response headers to add to each successful HTTP status 200 response. These + headers are not included in other status code responses. + + Headers that the server sends automatically such as ``Content-Type`` + will not be overwritten by :attr:`!extra_response_headers`. + The :class:`SimpleHTTPRequestHandler` class defines the following methods: .. method:: do_HEAD() @@ -440,6 +453,9 @@ instantiation, of which this module provides three different variants: followed by a ``'Content-Length:'`` header with the file's size and a ``'Last-Modified:'`` header with the file's modification time. + The instance attribute :attr:`extra_response_headers` is a sequence of + ``(name, value)`` pairs containing user-defined extra response headers. + Then follows a blank line signifying the end of the headers, and then the contents of the file are output. @@ -581,6 +597,15 @@ The following options are accepted: .. versionadded:: 3.14 +.. option:: -H, --header
+ + Specify an additional extra HTTP Response Header to send on successful HTTP + 200 responses. Can be used multiple times to send additional custom response + headers. Headers that are sent automatically by the server (for instance + Content-Type) will not be overwritten by the server. + + .. versionadded:: next + .. _http.server-security: diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 6e0df0648fb8bf..d55bc9f9662360 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1095,6 +1095,13 @@ Test cases self.assertIn('myfile.py', cm.filename) self.assertEqual(320, cm.lineno) + The context managers can be nested to test that multiple different + warnings are emitted:: + + with (self.assertWarns(SomeWarning), + self.assertWarns(OtherWarning)): + do_something() + This method works regardless of the warning filters in place when it is called. @@ -1103,6 +1110,10 @@ Test cases .. versionchanged:: 3.3 Added the *msg* keyword argument when used as a context manager. + .. versionchanged:: next + Warnings that do not match the specified category are no longer + swallowed. + Nested context managers are now supported. .. method:: assertWarnsRegex(warning, regex, callable, *args, **kwds) assertWarnsRegex(warning, regex, *, msg=None) @@ -1121,11 +1132,23 @@ Test cases with self.assertWarnsRegex(RuntimeWarning, 'unsafe frobnicating'): frobnicate('/etc/passwd') + The context managers can be nested to test that multiple different + warnings are emitted:: + + with (self.assertWarns(SomeWarning, regex1), + self.assertWarns(OtherWarning, regex2)): + do_something() + .. versionadded:: 3.2 .. versionchanged:: 3.3 Added the *msg* keyword argument when used as a context manager. + .. versionchanged:: next + Warnings that do not match the specified category or regex are + no longer swallowed. + Nested context managers are now supported. + .. method:: assertLogs(logger=None, level=None, formatter=None) A context manager to test that at least one message is logged on diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 78e464f2a5a6d8..61a440d2ad6f8d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -974,6 +974,15 @@ http.server for files with unknown extensions. (Contributed by John Comeau and Hugo van Kemenade in :gh:`113471`.) +* Add a new ``extra_response_headers`` keyword argument to + :class:`~http.server.SimpleHTTPRequestHandler` to support custom headers in + HTTP responses. + (Contributed by Anton I. Sipos in :gh:`135057`.) + +* Add a ``-H/--header`` option to the :program:`python -m http.server` + command-line interface to support custom headers in HTTP responses. + (Contributed by Anton I. Sipos in :gh:`135057`.) + inspect ------- @@ -1471,10 +1480,16 @@ unicodedata unittest -------- -* :func:`unittest.TestCase.assertLogs` will now accept a formatter +* :meth:`unittest.TestCase.assertLogs` will now accept a formatter to control how messages are formatted. (Contributed by Garry Cairns in :gh:`134567`.) +* :meth:`unittest.TestCase.assertWarns` and + :meth:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that + do not match the specified category or regex. + Nested context managers are now supported. + (Contributed by Serhiy Storchaka in :gh:`143231`.) + urllib.parse ------------ @@ -2352,3 +2367,11 @@ that may require changes to your code. with argument ``altchars=b'-_'`` (this works with older Python versions) to make padding required. (Contributed by Serhiy Storchaka in :gh:`73613`.) + +* Since :meth:`unittest.TestCase.assertWarns` and + :meth:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that + do not match the specified category or regex, your tests may start leaking + some warnings that were previously masked. + Use warning filters to silence them or additional :meth:`!assertWarns*` + to catch and check them. + (Contributed by Serhiy Storchaka in :gh:`143231`.) diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h index 4f8b48db23d1ef..0d7e367a5618b2 100644 --- a/Include/internal/pycore_opcode_metadata.h +++ b/Include/internal/pycore_opcode_metadata.h @@ -1386,7 +1386,7 @@ _PyOpcode_macro_expansion[256] = { [BUILD_STRING] = { .nuops = 1, .uops = { { _BUILD_STRING, OPARG_SIMPLE, 0 } } }, [BUILD_TEMPLATE] = { .nuops = 1, .uops = { { _BUILD_TEMPLATE, OPARG_SIMPLE, 0 } } }, [BUILD_TUPLE] = { .nuops = 1, .uops = { { _BUILD_TUPLE, OPARG_SIMPLE, 0 } } }, - [CALL_ALLOC_AND_ENTER_INIT] = { .nuops = 6, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_OBJECT, 2, 1 }, { _ALLOCATE_OBJECT, OPARG_SIMPLE, 3 }, { _CREATE_INIT_FRAME, OPARG_SIMPLE, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_ALLOC_AND_ENTER_INIT] = { .nuops = 7, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_OBJECT, 2, 1 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _ALLOCATE_OBJECT, OPARG_SIMPLE, 3 }, { _CREATE_INIT_FRAME, OPARG_SIMPLE, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, [CALL_BOUND_METHOD_EXACT_ARGS] = { .nuops = 11, .uops = { { _RECORD_BOUND_METHOD, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_CALL_BOUND_METHOD_EXACT_ARGS, OPARG_SIMPLE, 1 }, { _INIT_CALL_BOUND_METHOD_EXACT_ARGS, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_FUNCTION_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _CHECK_STACK_SPACE, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _INIT_CALL_PY_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, [CALL_BOUND_METHOD_GENERAL] = { .nuops = 8, .uops = { { _RECORD_BOUND_METHOD, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_METHOD_VERSION, 2, 1 }, { _EXPAND_METHOD, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_GENERAL, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, [CALL_BUILTIN_CLASS] = { .nuops = 6, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_BUILTIN_CLASS, OPARG_SIMPLE, 3 }, { _CALL_BUILTIN_CLASS, OPARG_SIMPLE, 3 }, { _POP_TOP_OPARG, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, diff --git a/Lib/_py_warnings.py b/Lib/_py_warnings.py index 81a386c4487d95..ab09913de6812d 100644 --- a/Lib/_py_warnings.py +++ b/Lib/_py_warnings.py @@ -620,17 +620,18 @@ def warn_explicit(message, category, filename, lineno, linecache.getlines(filename, module_globals) # Print message and context - msg = _wm.WarningMessage(message, category, filename, lineno, source=source) + msg = _wm.WarningMessage(message, category, filename, lineno, + module=module, source=source) _wm._showwarnmsg(msg) class WarningMessage(object): _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file", - "line", "source") + "line", "source", "module") def __init__(self, message, category, filename, lineno, file=None, - line=None, source=None): + line=None, source=None, module=None): self.message = message self.category = category self.filename = filename @@ -638,12 +639,14 @@ def __init__(self, message, category, filename, lineno, file=None, self.file = file self.line = line self.source = source + self.module = module self._category_name = category.__name__ if category else None def __str__(self): - return ("{message : %r, category : %r, filename : %r, lineno : %s, " - "line : %r}" % (self.message, self._category_name, - self.filename, self.lineno, self.line)) + return ("{message : %r, category : %r, module : %r, " + "filename : %r, lineno : %s, line : %r}" % ( + self.message, self._category_name, self.module, + self.filename, self.lineno, self.line)) def __repr__(self): return f'<{type(self).__qualname__} {self}>' diff --git a/Lib/http/server.py b/Lib/http/server.py index 27ab37303a085c..16ea7f3f93693f 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -551,13 +551,17 @@ def send_response_only(self, code, message=None): (self.protocol_version, code, message)).encode( 'latin-1', 'strict')) - def send_header(self, keyword, value): + def send_header(self, keyword, value, *, _is_extra=False): """Send a MIME header to the headers buffer.""" if self.request_version != 'HTTP/0.9': if not hasattr(self, '_headers_buffer'): self._headers_buffer = [] self._headers_buffer.append( ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) + if not hasattr(self, '_default_response_headers'): + self._default_response_headers = [] + if not _is_extra: + self._default_response_headers.append((keyword, value)) if keyword.lower() == 'connection': if value.lower() == 'close': @@ -575,6 +579,8 @@ def flush_headers(self): if hasattr(self, '_headers_buffer'): self.wfile.write(b"".join(self._headers_buffer)) self._headers_buffer = [] + if hasattr(self, '_default_response_headers'): + self._default_response_headers = [] def _colorize_request(self, code, size, t): try: @@ -736,10 +742,11 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): '.xz': 'application/x-xz', } - def __init__(self, *args, directory=None, **kwargs): + def __init__(self, *args, directory=None, extra_response_headers=None, **kwargs): if directory is None: directory = os.getcwd() self.directory = os.fspath(directory) + self.extra_response_headers = extra_response_headers super().__init__(*args, **kwargs) def do_GET(self): @@ -757,6 +764,16 @@ def do_HEAD(self): if f: f.close() + def _send_extra_response_headers(self): + """Send the headers stored in self.extra_response_headers.""" + if self.extra_response_headers is not None: + default_headers = {h.lower() for h, _ in self._default_response_headers} + for header, value in self.extra_response_headers: + # Don't send the header if it's already sent + # as part of the default response headers + if header.lower() not in default_headers: + self.send_header(header, value, _is_extra=True) + def send_head(self): """Common code for GET and HEAD commands. @@ -839,6 +856,7 @@ def send_head(self): self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self._send_extra_response_headers() self.end_headers() return f except: @@ -903,6 +921,7 @@ def list_directory(self, path): self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html; charset=%s" % enc) self.send_header("Content-Length", str(len(encoded))) + self._send_extra_response_headers() self.end_headers() return f @@ -1011,6 +1030,22 @@ def _get_best_family(*address): return family, sockaddr +def _make_server(HandlerClass=BaseHTTPRequestHandler, + ServerClass=ThreadingHTTPServer, + protocol="HTTP/1.0", port=8000, bind=None, + tls_cert=None, tls_key=None, tls_password=None, + default_content_type=SimpleHTTPRequestHandler.default_content_type): + ServerClass.address_family, addr = _get_best_family(bind, port) + HandlerClass.protocol_version = protocol + HandlerClass.default_content_type = default_content_type + + if tls_cert: + return ServerClass(addr, HandlerClass, certfile=tls_cert, + keyfile=tls_key, password=tls_password) + else: + return ServerClass(addr, HandlerClass) + + def test(HandlerClass=SimpleHTTPRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.0", port=8000, bind=None, @@ -1019,19 +1054,13 @@ def test(HandlerClass=SimpleHTTPRequestHandler, """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). - """ - ServerClass.address_family, addr = _get_best_family(bind, port) - HandlerClass.protocol_version = protocol - HandlerClass.default_content_type = content_type - - if tls_cert: - server = ServerClass(addr, HandlerClass, certfile=tls_cert, - keyfile=tls_key, password=tls_password) - else: - server = ServerClass(addr, HandlerClass) - - with server as httpd: + with _make_server( + HandlerClass=HandlerClass, ServerClass=ServerClass, + protocol=protocol, port=port, bind=bind, + tls_cert=tls_cert, tls_key=tls_key, tls_password=tls_password, + default_content_type=content_type, + ) as httpd: host, port = httpd.socket.getsockname()[:2] url_host = f'[{host}]' if ':' in host else host protocol = 'HTTPS' if tls_cert else 'HTTP' @@ -1076,6 +1105,10 @@ def _main(args=None): parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') + parser.add_argument('-H', '--header', nargs=2, action='append', + metavar=('HEADER', 'VALUE'), + help='Add a custom response header ' + '(can be specified multiple times)') args = parser.parse_args(args) if not args.tls_cert and args.tls_key: @@ -1104,7 +1137,8 @@ def server_bind(self): def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self, - directory=args.directory) + directory=args.directory, + extra_response_headers=args.header) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index bee2aceb187027..bb307191dffcc1 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -504,17 +504,25 @@ def check(z, x, y): with self.assertWarnsRegex(DeprecationWarning, "argument 'imag' must be a real number, not complex"): check(complex(0.0, 4.25j), -4.25, 0.0) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(4.25+0j, 0j), 4.25, 0.0) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(4.25j, 0j), 0.0, 4.25) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(0j, 4.25+0j), 0.0, 4.25) - with self.assertWarnsRegex(DeprecationWarning, - "argument 'real' must be a real number, not complex"): + with (self.assertWarnsRegex(DeprecationWarning, + "argument 'real' must be a real number, not complex"), + self.assertWarnsRegex(DeprecationWarning, + "argument 'imag' must be a real number, not complex")): check(complex(0j, 4.25j), -4.25, 0.0) check(complex(real=4.25), 4.25, 0.0) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 1f7a5a42fdaeb7..44fe6f771de2cb 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -540,8 +540,16 @@ def test_err(self): self.assertIn(f"{t.status_client_error}404", lines[1]) +class CustomHeaderSimpleHTTPRequestHandler(SimpleHTTPRequestHandler): + extra_response_headers = None + + def __init__(self, *args, **kwargs): + kwargs.setdefault('extra_response_headers', self.extra_response_headers) + super().__init__(*args, **kwargs) + + class SimpleHTTPServerTestCase(BaseTestCase): - class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): + class request_handler(NoLogRequestHandler, CustomHeaderSimpleHTTPRequestHandler): pass def setUp(self): @@ -898,6 +906,65 @@ def test_path_without_leading_slash(self): self.assertEqual(response.getheader("Location"), self.tempdir_name + "/?hi=1") + def test_extra_response_headers_list_dir(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('X-Test1', 'test1'), + ('X-Test2', 'test2'), + ]): + response = self.request(self.base_url + '/') + self.assertEqual(response.status, 200) + self.assertEqual(response.getheader("X-Test1"), 'test1') + self.assertEqual(response.getheader("X-Test2"), 'test2') + + def test_extra_response_headers_get_file(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('Set-Cookie', 'test1=value1'), + ('Set-Cookie', 'test2=value2'), + ('X-Test1', 'value3'), + ]): + data = b"Dummy index file\r\n" + with open(os.path.join(self.tempdir_name, 'index.html'), 'wb') as f: + f.write(data) + response = self.request(self.base_url + '/') + self.assertEqual(response.status, 200) + self.assertEqual(response.getheader("Set-Cookie"), + 'test1=value1, test2=value2') + self.assertEqual(response.getheader("X-Test1"), 'value3') + + def test_extra_response_headers_missing_on_404(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('X-Test1', 'value'), + ]): + response = self.request(self.base_url + '/missing.html') + self.assertEqual(response.status, 404) + self.assertEqual(response.getheader("X-Test1"), None) + + def test_extra_response_headers_dont_overwrite_default_headers(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('Content-Type', 'test/not_allowed'), + ('Server', 'not_allowed'), + ('Set-Cookie', 'test=allowed'), + ]): + # The Content-Type header should not be overwritten by the extra_response_headers + # But cookies in the extra_allowed_duplicate_headers are allowed, + # including Set-Cookie + response = self.request(self.base_url + '/') + self.assertEqual(response.status, 200) + self.assertNotEqual(response.getheader("Content-Type"), 'test/not_allowed') + self.assertNotEqual(response.getheader("Server"), 'not_allowed') + self.assertEqual(response.getheader("Set-Cookie"), 'test=allowed') + + def test_multiple_requests_dont_duplicate_extra_response_headers(self): + with mock.patch.object(self.request_handler, 'extra_response_headers', [ + ('x-test', 'test-value'), + ]): + response = self.request(self.base_url + '/') + self.assertEqual(response.status, 200) + self.assertEqual(response.getheader("x-test"), 'test-value') + response = self.request(self.base_url + '/') + self.assertEqual(response.status, 200) + self.assertEqual(response.getheader("x-test"), 'test-value') + class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): @@ -1458,6 +1525,21 @@ def test_content_type_flag(self, mock_func): mock_func.assert_called_once_with(**call_args) mock_func.reset_mock() + @mock.patch('http.server.test') + def test_header_flag(self, mock_func): + call_args = self.args + self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2') + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + + def test_extra_header_flag_too_few_args(self): + with self.assertRaises(SystemExit): + self.invoke_httpd('--header', 'h1') + + def test_extra_header_flag_too_many_args(self): + with self.assertRaises(SystemExit): + self.invoke_httpd('--header', 'h1', 'v1', 'h2') + @unittest.skipIf(ssl is None, "requires ssl") @mock.patch('http.server.test') def test_tls_cert_and_key_flags(self, mock_func): @@ -1541,6 +1623,30 @@ def test_unknown_flag(self, _): self.assertEqual(stdout.getvalue(), '') self.assertIn('error', stderr.getvalue()) + @mock.patch('http.server.test') + def test_extra_response_headers_arg(self, mock_test): + # Call the main function with extra response headers cli args + server._main( + ['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2:v3 v4', '8080'] + ) + # Get the ServerClass (DualStackServerMixin subclass) that _main() + # passed to test(), and verify its finish_request passes + # extra_response_headers to the handler. + _, kwargs = mock_test.call_args + server_class = kwargs['ServerClass'] + + mock_handler_class = mock.MagicMock() + mock_server = mock.Mock() + mock_server.RequestHandlerClass = mock_handler_class + server_class.finish_request(mock_server, mock.Mock(), '127.0.0.1') + mock_handler_class.assert_called_once_with( + mock.ANY, mock.ANY, mock_server, + directory=mock.ANY, + extra_response_headers=[ + ['Set-Cookie', 'k=v'], ['Set-Cookie', 'k2=v2:v3 v4'] + ] + ) + class CommandLineRunTimeTestCase(unittest.TestCase): served_data = os.urandom(32) diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 1dec947ea76d23..df95e558949aee 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -406,11 +406,12 @@ def testAssertWarnsRegex(self): # test warning raised but with wrong message def raise_wrong_message(): warnings.warn('foo') - self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'), - raise_wrong_message, - ['^"regex" does not match "foo"$', '^oops$', - '^"regex" does not match "foo"$', - '^"regex" does not match "foo" : oops$']) + with self.assertWarnsRegex(UserWarning, 'foo'): + self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'), + raise_wrong_message, + ['^"regex" does not match "foo"$', '^oops$', + '^"regex" does not match "foo"$', + '^"regex" does not match "foo" : oops$']) if __name__ == "__main__": diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index cf10e956bf2bdc..83b42918e1eff6 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -1631,11 +1631,11 @@ def testAssertRaisesRegexNoExceptionType(self): self.assertRaisesRegex((ValueError, object), 'expect') def testAssertWarnsCallable(self): - def _runtime_warn(): - warnings.warn("foo", RuntimeWarning) + def _runtime_warn(categories=(RuntimeWarning,)): + for category in categories: + warnings.warn("foo", category) # Success when the right warning is triggered, even several times - self.assertWarns(RuntimeWarning, _runtime_warn) - self.assertWarns(RuntimeWarning, _runtime_warn) + self.assertWarns(RuntimeWarning, _runtime_warn, (RuntimeWarning, RuntimeWarning)) # A tuple of warning classes is accepted self.assertWarns((DeprecationWarning, RuntimeWarning), _runtime_warn) # *args and **kwargs also work @@ -1648,22 +1648,35 @@ def _runtime_warn(): with self.assertRaises(TypeError): self.assertWarns(RuntimeWarning, None) # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): - self.assertWarns(DeprecationWarning, _runtime_warn) + self.assertWarns(DeprecationWarning, _runtime_warn, + (RuntimeWarning, RuntimeWarning)) + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) # Filters for other warnings are not modified with warnings.catch_warnings(): warnings.simplefilter("error", RuntimeWarning) with self.assertRaises(RuntimeWarning): self.assertWarns(DeprecationWarning, _runtime_warn) + # Warnings that do not match the category are not swallowed. + with self.assertWarns(RuntimeWarning): + with self.assertRaises(self.failureException): + self.assertWarns(DeprecationWarning, _runtime_warn) + with self.assertWarns(RuntimeWarning): + self.assertWarns(DeprecationWarning, _runtime_warn, + (RuntimeWarning, DeprecationWarning)) + with self.assertWarns(RuntimeWarning): + self.assertWarns(DeprecationWarning, _runtime_warn, + (DeprecationWarning, RuntimeWarning)) def testAssertWarnsContext(self): # Believe it or not, it is preferable to duplicate all tests above, # to make sure the __warningregistry__ $@ is circumvented correctly. - def _runtime_warn(): - warnings.warn("foo", RuntimeWarning) + def _runtime_warn(category=RuntimeWarning): + warnings.warn("foo", category) _runtime_warn_lineno = inspect.getsourcelines(_runtime_warn)[1] with self.assertWarns(RuntimeWarning) as cm: _runtime_warn() @@ -1694,18 +1707,58 @@ def _runtime_warn(): with self.assertWarns(RuntimeWarning, foobar=42): pass # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): with self.assertWarns(DeprecationWarning): _runtime_warn() + _runtime_warn() + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) + with warnings.catch_warnings(record=True) as log: + # Force default filter (in case tests are run with -We) + warnings.simplefilter("error", RuntimeWarning) + warnings.filterwarnings("default", category=RuntimeWarning, + module=__name__) + with self.assertRaises(self.failureException): + with self.assertWarns(DeprecationWarning): + _runtime_warn() + _runtime_warn() + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) # Filters for other warnings are not modified with warnings.catch_warnings(): warnings.simplefilter("error", RuntimeWarning) with self.assertRaises(RuntimeWarning): with self.assertWarns(DeprecationWarning): _runtime_warn() + # Warnings that do not match the category are not swallowed. + with self.assertWarns(RuntimeWarning): + with self.assertRaises(self.failureException): + with self.assertWarns(DeprecationWarning): + _runtime_warn() + with self.assertWarns(RuntimeWarning): + with self.assertWarns(DeprecationWarning): + _runtime_warn() + _runtime_warn(DeprecationWarning) + with self.assertWarns(RuntimeWarning): + with self.assertWarns(DeprecationWarning): + _runtime_warn(DeprecationWarning) + _runtime_warn() + # Filters by module name work for other warnings. + with warnings.catch_warnings(record=True) as log: + warnings.filterwarnings("error", category=RuntimeWarning) + warnings.filterwarnings("default", category=RuntimeWarning, + module=re.escape(__name__)) + warnings.filterwarnings("error", category=RuntimeWarning, + module='test_case') + with self.assertWarns(DeprecationWarning): + _runtime_warn(DeprecationWarning) + _runtime_warn() + _runtime_warn() + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) def testAssertWarnsNoExceptionType(self): with self.assertRaises(TypeError): @@ -1722,8 +1775,9 @@ def testAssertWarnsNoExceptionType(self): self.assertWarns((UserWarning, Exception)) def testAssertWarnsRegexCallable(self): - def _runtime_warn(msg): - warnings.warn(msg, RuntimeWarning) + def _runtime_warn(*msgs): + for msg in msgs: + warnings.warn(msg, RuntimeWarning) self.assertWarnsRegex(RuntimeWarning, "o+", _runtime_warn, "foox") # Failure when no warning is triggered @@ -1734,16 +1788,26 @@ def _runtime_warn(msg): with self.assertRaises(TypeError): self.assertWarnsRegex(RuntimeWarning, "o+", None) # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): self.assertWarnsRegex(DeprecationWarning, "o+", - _runtime_warn, "foox") - # Failure when message doesn't match - with self.assertRaises(self.failureException): + _runtime_warn, "foox", "foox") + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) + # Failure when message doesn't match. + # Warnings that do not match the regex are not swallowed. + with self.assertWarnsRegex(RuntimeWarning, "ar"): + with self.assertRaises(self.failureException): + self.assertWarnsRegex(RuntimeWarning, "o+", + _runtime_warn, "barz") + with self.assertWarnsRegex(RuntimeWarning, "ar"): self.assertWarnsRegex(RuntimeWarning, "o+", - _runtime_warn, "barz") + _runtime_warn, "barz", "foox") + with self.assertWarnsRegex(RuntimeWarning, "ar"): + self.assertWarnsRegex(RuntimeWarning, "o+", + _runtime_warn, "foox", "barz") # A little trickier: we ask RuntimeWarnings to be raised, and then # check for some of them. It is implementation-defined whether # non-matching RuntimeWarnings are simply re-raised, or produce a @@ -1778,15 +1842,28 @@ def _runtime_warn(msg): with self.assertWarnsRegex(RuntimeWarning, 'o+', foobar=42): pass # Failure when another warning is triggered - with warnings.catch_warnings(): + with warnings.catch_warnings(record=True) as log: # Force default filter (in case tests are run with -We) warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): with self.assertWarnsRegex(DeprecationWarning, "o+"): _runtime_warn("foox") - # Failure when message doesn't match - with self.assertRaises(self.failureException): + _runtime_warn("foox") + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) + # Failure when message doesn't match. + # Warnings that do not match the regex are not swallowed. + with self.assertWarnsRegex(RuntimeWarning, "ar"): + with self.assertRaises(self.failureException): + with self.assertWarnsRegex(RuntimeWarning, "o+"): + _runtime_warn("barz") + with self.assertWarnsRegex(RuntimeWarning, "ar"): + with self.assertWarnsRegex(RuntimeWarning, "o+"): + _runtime_warn("barz") + _runtime_warn("foox") + with self.assertWarnsRegex(RuntimeWarning, "ar"): with self.assertWarnsRegex(RuntimeWarning, "o+"): + _runtime_warn("foox") _runtime_warn("barz") # A little trickier: we ask RuntimeWarnings to be raised, and then # check for some of them. It is implementation-defined whether @@ -1797,6 +1874,19 @@ def _runtime_warn(msg): with self.assertRaises((RuntimeWarning, self.failureException)): with self.assertWarnsRegex(RuntimeWarning, "o+"): _runtime_warn("barz") + # Filters by module name work for warnings with other message. + with warnings.catch_warnings(record=True) as log: + warnings.filterwarnings("error", category=RuntimeWarning) + warnings.filterwarnings("default", category=RuntimeWarning, + module=re.escape(__name__)) + warnings.filterwarnings("error", category=RuntimeWarning, + module='test_case') + with self.assertWarnsRegex(RuntimeWarning, "ar"): + _runtime_warn("bar") + _runtime_warn("foox") + _runtime_warn("foox") + self.assertEqual(len(log), 1, log) + self.assertIsInstance(log[0].message, RuntimeWarning) def testAssertWarnsRegexNoExceptionType(self): with self.assertRaises(TypeError): diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index d86844c1a29a9a..bf1bcf8e6ed5d9 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -582,15 +582,19 @@ def test_warn_nonstandard_types(self): # ``Warning() != Warning()``. self.assertEqual(str(w[-1].message), str(UserWarning(ob))) - def test_filename(self): + def test_filename_module(self): with warnings_state(self.module): with self.module.catch_warnings(record=True) as w: warning_tests.inner("spam1") self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") + self.assertEqual(w[-1].module, + "test.test_warnings.data.stacklevel") warning_tests.outer("spam2") self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") + self.assertEqual(w[-1].module, + "test.test_warnings.data.stacklevel") def test_stacklevel(self): # Test stacklevel argument @@ -600,23 +604,32 @@ def test_stacklevel(self): warning_tests.inner("spam3", stacklevel=1) self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") + self.assertEqual(w[-1].module, + "test.test_warnings.data.stacklevel") warning_tests.outer("spam4", stacklevel=1) self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") + self.assertEqual(w[-1].module, + "test.test_warnings.data.stacklevel") warning_tests.inner("spam5", stacklevel=2) self.assertEqual(os.path.basename(w[-1].filename), "__init__.py") + self.assertEqual(w[-1].module, __name__) warning_tests.outer("spam6", stacklevel=2) self.assertEqual(os.path.basename(w[-1].filename), "stacklevel.py") + self.assertEqual(w[-1].module, + "test.test_warnings.data.stacklevel") warning_tests.outer("spam6.5", stacklevel=3) self.assertEqual(os.path.basename(w[-1].filename), "__init__.py") + self.assertEqual(w[-1].module, __name__) warning_tests.inner("spam7", stacklevel=9999) self.assertEqual(os.path.basename(w[-1].filename), "") + self.assertEqual(w[-1].module, "sys") def test_stacklevel_import(self): # Issue #24305: With stacklevel=2, module-level warnings should work. @@ -627,6 +640,7 @@ def test_stacklevel_import(self): import test.test_warnings.data.import_warning # noqa: F401 self.assertEqual(len(w), 1) self.assertEqual(w[0].filename, __file__) + self.assertEqual(w[0].module, __name__) def test_skip_file_prefixes(self): with warnings_state(self.module): @@ -638,20 +652,27 @@ def test_skip_file_prefixes(self): "inner_api", stacklevel=2, warnings_module=warning_tests.warnings) self.assertEqual(w[-1].filename, __file__) + self.assertEqual(w[-1].module, __name__) warning_tests.package("package api", stacklevel=2) self.assertEqual(w[-1].filename, __file__) + self.assertEqual(w[-1].module, __name__) self.assertEqual(w[-2].filename, w[-1].filename) + self.assertEqual(w[-2].module, w[-1].module) # Low stacklevels are overridden to 2 behavior. warning_tests.package("package api 1", stacklevel=1) self.assertEqual(w[-1].filename, __file__) + self.assertEqual(w[-1].module, __name__) warning_tests.package("package api 0", stacklevel=0) self.assertEqual(w[-1].filename, __file__) + self.assertEqual(w[-1].module, __name__) warning_tests.package("package api -99", stacklevel=-99) self.assertEqual(w[-1].filename, __file__) + self.assertEqual(w[-1].module, __name__) # The stacklevel still goes up out of the package. warning_tests.package("prefix02", stacklevel=3) self.assertIn("unittest", w[-1].filename) + self.assertStartsWith(w[-1].module, "unittest") def test_skip_file_prefixes_file_path(self): # see: gh-126209 @@ -662,6 +683,8 @@ def test_skip_file_prefixes_file_path(self): self.assertEqual(len(w), 1) self.assertNotEqual(w[-1].filename, skipped) + self.assertEqual(w[-1].filename, __file__) + self.assertEqual(w[-1].module, __name__) def test_skip_file_prefixes_type_errors(self): with warnings_state(self.module): @@ -673,7 +696,7 @@ def test_skip_file_prefixes_type_errors(self): with self.assertRaises(TypeError): warn("msg", skip_file_prefixes="a sequence of strs") - def test_exec_filename(self): + def test_exec_filename_module(self): filename = "" codeobj = compile(("import warnings\n" "warnings.warn('hello', UserWarning)"), @@ -682,6 +705,12 @@ def test_exec_filename(self): self.module.simplefilter("always", category=UserWarning) exec(codeobj) self.assertEqual(w[0].filename, filename) + self.assertEqual(w[0].module, __name__) + with self.module.catch_warnings(record=True) as w: + self.module.simplefilter("always", category=UserWarning) + exec(codeobj, {}) + self.assertEqual(w[0].filename, filename) + self.assertEqual(w[0].module, '') def test_warn_explicit_non_ascii_filename(self): with self.module.catch_warnings(record=True) as w: diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index eba50839cd33ae..a392238c85abfa 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -301,7 +301,7 @@ def __enter__(self): v.__warningregistry__ = {} self.warnings_manager = warnings.catch_warnings(record=True) self.warnings = self.warnings_manager.__enter__() - warnings.simplefilter("always", self.expected) + warnings.simplefilter("always") return self def __exit__(self, exc_type, exc_value, tb): @@ -314,19 +314,44 @@ def __exit__(self, exc_type, exc_value, tb): except AttributeError: exc_name = str(self.expected) first_matching = None + matched = False + non_matching_warnings = [] for m in self.warnings: w = m.message if not isinstance(w, self.expected): + non_matching_warnings.append(m) continue if first_matching is None: first_matching = w if (self.expected_regex is not None and not self.expected_regex.search(str(w))): + non_matching_warnings.append(m) continue + if matched: + continue + matched = True # store warning for later retrieval self.warning = w self.filename = m.filename self.lineno = m.lineno + for m in non_matching_warnings: + module = m.module + module_globals = None + registry = None + if module is not None: + try: + module_globals = vars(sys.modules[module]) + except (KeyError, TypeError): + # module == "" or sys.modules[module] is None + pass + else: + registry = module_globals.setdefault("__warningregistry__", {}) + warnings.warn_explicit(m.message, m.category, m.filename, m.lineno, + module=module, + registry=registry, + module_globals=module_globals, + source=m.source) + if matched: return # Now we simply try to choose a helpful failure message if first_matching is not None: @@ -338,7 +363,6 @@ def __exit__(self, exc_type, exc_value, tb): else: self._raiseFailure("{} not triggered".format(exc_name)) - class _AssertNotWarnsContext(_AssertWarnsContext): def __exit__(self, exc_type, exc_value, tb): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-03-10-24-50.gh-issue-149243.Zh1q9_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-03-10-24-50.gh-issue-149243.Zh1q9_.rst new file mode 100644 index 00000000000000..5c1956fe398364 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-03-10-24-50.gh-issue-149243.Zh1q9_.rst @@ -0,0 +1 @@ +Check for recursion limits in ``CALL_ALLOC_AND_ENTER_INIT`` opcode. diff --git a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst new file mode 100644 index 00000000000000..754df083ab1063 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst @@ -0,0 +1,2 @@ +Add a ``-H`` or ``--header`` CLI option to :program:`python -m http.server`. Contributed by +Anton I. Sipos. diff --git a/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst b/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst new file mode 100644 index 00000000000000..05c9fa79904154 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst @@ -0,0 +1,4 @@ +:func:`unittest.TestCase.assertWarns` and +:func:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that +do not match the specified category or regex. +Nested context managers are now supported. diff --git a/Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst b/Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst new file mode 100644 index 00000000000000..e4769866c2045d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst @@ -0,0 +1 @@ +A *module* attribute has been added to :class:`!warnings.WarningMessage`. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 8897854078bd45..bd69211fd22206 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -1925,6 +1925,14 @@ JUMP_TO_PREDICTED(CALL); } } + // _CHECK_RECURSION_REMAINING + { + if (tstate->py_recursion_remaining <= 1) { + UPDATE_MISS_STATS(CALL); + assert(_PyOpcode_Deopt[opcode] == (CALL)); + JUMP_TO_PREDICTED(CALL); + } + } // _ALLOCATE_OBJECT { PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); diff --git a/Python/_warnings.c b/Python/_warnings.c index 6b8fa19ff3f606..4f6de50efa14a8 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -716,7 +716,7 @@ static int call_show_warning(PyThreadState *tstate, PyObject *category, PyObject *text, PyObject *message, PyObject *filename, int lineno, PyObject *lineno_obj, - PyObject *sourceline, PyObject *source) + PyObject *sourceline, PyObject *source, PyObject *module) { PyObject *show_fn, *msg, *res, *warnmsg_cls = NULL; PyInterpreterState *interp = tstate->interp; @@ -747,7 +747,8 @@ call_show_warning(PyThreadState *tstate, PyObject *category, } msg = PyObject_CallFunctionObjArgs(warnmsg_cls, message, category, - filename, lineno_obj, Py_None, Py_None, source, + filename, lineno_obj, Py_None, Py_None, + source ? source : Py_None, module, NULL); Py_DECREF(warnmsg_cls); if (msg == NULL) @@ -878,7 +879,7 @@ warn_explicit(PyThreadState *tstate, PyObject *category, PyObject *message, goto return_none; if (rc == 0) { if (call_show_warning(tstate, category, text, message, filename, - lineno, lineno_obj, sourceline, source) < 0) + lineno, lineno_obj, sourceline, source, module) < 0) goto cleanup; } else /* if (rc == -1) */ diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 4239ba58bc390b..963391e7598fb6 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -4737,6 +4737,7 @@ dummy_func( unused/1 + _CHECK_PEP_523 + _CHECK_OBJECT + + _CHECK_RECURSION_REMAINING + _ALLOCATE_OBJECT + _CREATE_INIT_FRAME + _PUSH_FRAME; diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index dccee0e4a3b110..105375e41e360b 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -1925,6 +1925,14 @@ JUMP_TO_PREDICTED(CALL); } } + // _CHECK_RECURSION_REMAINING + { + if (tstate->py_recursion_remaining <= 1) { + UPDATE_MISS_STATS(CALL); + assert(_PyOpcode_Deopt[opcode] == (CALL)); + JUMP_TO_PREDICTED(CALL); + } + } // _ALLOCATE_OBJECT { PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); diff --git a/Python/optimizer_analysis.c b/Python/optimizer_analysis.c index 9f6ce206ef4722..1dc3a248f45f0c 100644 --- a/Python/optimizer_analysis.c +++ b/Python/optimizer_analysis.c @@ -156,6 +156,16 @@ type_watcher_callback(PyTypeObject* type) return 0; } +static void +watch_type(PyTypeObject *type, _PyBloomFilter *filter) +{ + if (_Py_IsImmortal(type) && (type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE)) { + return; + } + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)type); + _Py_BloomFilter_Add(filter, type); +} + static PyObject * convert_global_to_const(_PyUOpInstruction *inst, PyObject *obj) { @@ -367,8 +377,7 @@ optimize_dict_known_hash( // for user-defined objects which don't override tp_hash Py_hash_t hash = PyObject_Hash(sub); ADD_OP(opcode, 0, hash); - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)Py_TYPE(sub)); - _Py_BloomFilter_Add(dependencies, Py_TYPE(sub)); + watch_type(Py_TYPE(sub), dependencies); } } @@ -401,8 +410,7 @@ lookup_attr(JitOptContext *ctx, _PyBloomFilter *dependencies, _PyUOpInstruction ADD_OP(suffix, 2, 0); } if ((type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)type); - _Py_BloomFilter_Add(dependencies, type); + watch_type(type, dependencies); } return sym_new_const(ctx, lookup); } @@ -473,10 +481,8 @@ lookup_super_attr(JitOptContext *ctx, _PyBloomFilter *dependencies, } // if obj_type is immutable, then all its superclasses are immutable if ((obj_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)su_type); - _Py_BloomFilter_Add(dependencies, su_type); - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)obj_type); - _Py_BloomFilter_Add(dependencies, obj_type); + watch_type(su_type, dependencies); + watch_type(obj_type, dependencies); } return sym_new_const_steal(ctx, lookup); } diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index fa34fe4cbb3417..0837d57b61b29d 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -149,10 +149,7 @@ dummy_func(void) { // Promote the probable type version to a known one. sym_set_type(owner, probable_type); sym_set_type_version(owner, type_version); - if ((probable_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)probable_type); - _Py_BloomFilter_Add(dependencies, probable_type); - } + watch_type(probable_type, dependencies); } else { ctx->contradiction = true; @@ -238,10 +235,7 @@ dummy_func(void) { } else { sym_set_const(owner, type); - if ((((PyTypeObject *)type)->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, type); - _Py_BloomFilter_Add(dependencies, type); - } + watch_type((PyTypeObject *)type, dependencies); } } } @@ -258,8 +252,7 @@ dummy_func(void) { probable_type->tp_version_tag == type_version) { sym_set_type(owner, probable_type); sym_set_type_version(owner, type_version); - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)probable_type); - _Py_BloomFilter_Add(dependencies, probable_type); + watch_type(probable_type, dependencies); } else { ctx->contradiction = true; @@ -1326,8 +1319,7 @@ dummy_func(void) { assert(init != NULL); assert(PyFunction_Check(init)); callable = sym_new_const(ctx, init); - PyType_Watch(TYPE_WATCHER_ID, callable_o); - _Py_BloomFilter_Add(dependencies, callable_o);; + watch_type((PyTypeObject *)callable_o, dependencies); } else { callable = sym_new_not_null(ctx); @@ -2033,10 +2025,7 @@ dummy_func(void) { 0, (uintptr_t)descr); ADD_OP(_SWAP, 3, 0); optimize_pop_top(ctx, this_instr, method_and_self[0]); - if ((type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)type); - _Py_BloomFilter_Add(dependencies, type); - } + watch_type(type, dependencies); method_and_self[0] = sym_new_const(ctx, descr); optimized = true; } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 553bd7302026f0..1ade86f64b2b20 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2534,8 +2534,7 @@ probable_type->tp_version_tag == type_version) { sym_set_type(owner, probable_type); sym_set_type_version(owner, type_version); - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)probable_type); - _Py_BloomFilter_Add(dependencies, probable_type); + watch_type(probable_type, dependencies); } else { ctx->contradiction = true; @@ -2560,10 +2559,7 @@ probable_type->tp_version_tag == type_version) { sym_set_type(owner, probable_type); sym_set_type_version(owner, type_version); - if ((probable_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)probable_type); - _Py_BloomFilter_Add(dependencies, probable_type); - } + watch_type(probable_type, dependencies); } else { ctx->contradiction = true; @@ -2689,10 +2685,7 @@ } else { sym_set_const(owner, type); - if ((((PyTypeObject *)type)->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, type); - _Py_BloomFilter_Add(dependencies, type); - } + watch_type((PyTypeObject *)type, dependencies); } } break; @@ -3781,10 +3774,7 @@ 0, (uintptr_t)descr); ADD_OP(_SWAP, 3, 0); optimize_pop_top(ctx, this_instr, method_and_self[0]); - if ((type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0) { - PyType_Watch(TYPE_WATCHER_ID, (PyObject *)type); - _Py_BloomFilter_Add(dependencies, type); - } + watch_type(type, dependencies); method_and_self[0] = sym_new_const(ctx, descr); optimized = true; } @@ -4320,8 +4310,7 @@ assert(PyFunction_Check(init)); callable = sym_new_const(ctx, init); stack_pointer[-2 - oparg] = callable; - PyType_Watch(TYPE_WATCHER_ID, callable_o); - _Py_BloomFilter_Add(dependencies, callable_o);; + watch_type((PyTypeObject *)callable_o, dependencies); } else { callable = sym_new_not_null(ctx);