From de1769f700ccd633e697d1c7a47c44eb19eb2985 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 4 May 2026 10:20:23 +0300 Subject: [PATCH 1/5] gh-142389: Add colour to regrtest and pdb's help descriptions (#149332) --- Lib/pdb.py | 14 ++-- Lib/test/libregrtest/cmdline.py | 82 +++++++++---------- ...-05-03-23-29-34.gh-issue-142389.SVYiSv.rst | 2 + 3 files changed, 50 insertions(+), 48 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-03-23-29-34.gh-issue-142389.SVYiSv.rst diff --git a/Lib/pdb.py b/Lib/pdb.py index 4dd974b375c259..569599d349c49e 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -3713,18 +3713,18 @@ def help(): pydoc.pager(__doc__) _usage = """\ -Debug the Python program given by pyfile. Alternatively, +Debug the Python program given by `pyfile`. Alternatively, an executable module or package to debug can be specified using -the -m switch. You can also attach to a running Python process -using the -p option with its PID. +the `-m` switch. You can also attach to a running Python process +using the `-p` option with its PID. -Initial commands are read from .pdbrc files in your home directory +Initial commands are read from `.pdbrc` files in your home directory and in the current directory, if they exist. Commands supplied with --c are executed after commands from .pdbrc files. +`-c` are executed after commands from `.pdbrc` files. -To let the script run until an exception occurs, use "-c continue". +To let the script run until an exception occurs, use `-c continue`. To let the script run up to a given line X in the debugged file, use -"-c 'until X'".""" +`-c 'until X'`.""" def exit_with_permission_help_text(): diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index ea26cc849f8182..45e229eb19f0f9 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -15,73 +15,73 @@ Run Python regression tests. If no arguments or options are provided, finds all files matching -the pattern "test_*" in the Lib/test subdirectory and runs -them in alphabetical order (but see -M and -u, below, for exceptions). +the pattern `test_*` in the `Lib/test` subdirectory and runs +them in alphabetical order (but see `-M` and `-u`, below, for exceptions). For more rigorous testing, it is useful to use the following command line: -python -E -Wd -m test [options] [test_name1 ...] +`python -E -Wd -m test [options] [test_name1 ...]` """ EPILOG = """\ Additional option details: --r randomizes test execution order. You can use --randseed=int to provide an -int seed value for the randomizer. The randseed value will be used +`-r` randomizes test execution order. You can use `--randseed=int` to provide an +int seed value for the randomizer. The `randseed` value will be used to set seeds for all random usages in tests -(including randomizing the tests order if -r is set). +(including randomizing the tests order if `-r` is set). By default we always set random seed, but do not randomize test order. --s On the first invocation of regrtest using -s, the first test file found +`-s` On the first invocation of regrtest using `-s`, the first test file found or the first test file given on the command line is run, and the name of -the next test is recorded in a file named pynexttest. If run from the -Python build directory, pynexttest is located in the 'build' subdirectory, -otherwise it is located in tempfile.gettempdir(). On subsequent runs, -the test in pynexttest is run, and the next test is written to pynexttest. -When the last test has been run, pynexttest is deleted. In this way it +the next test is recorded in a file named `pynexttest`. If run from the +Python build directory, `pynexttest` is located in the 'build' subdirectory, +otherwise it is located in `tempfile.gettempdir()`. On subsequent runs, +the test in `pynexttest` is run, and the next test is written to `pynexttest`. +When the last test has been run, `pynexttest` is deleted. In this way it is possible to single step through the test files. This is useful when doing memory analysis on the Python interpreter, which process tends to consume too many resources to run the full regression test non-stop. --S is used to resume running tests after an interrupted run. It will -maintain the order a standard run (i.e. it assumes -r is not used). +`-S` is used to resume running tests after an interrupted run. It will +maintain the order a standard run (i.e. it assumes `-r` is not used). This is useful after the tests have prematurely stopped for some external reason and you want to resume the run from where you left off rather -than starting from the beginning. Note: this is different from --prioritize. +than starting from the beginning. Note: this is different from `--prioritize`. ---prioritize is used to influence the order of selected tests, such that +`--prioritize` is used to influence the order of selected tests, such that the tests listed as an argument are executed first. This is especially -useful when combined with -j and -r to pin the longest-running tests -to start at the beginning of a test run. Pass --prioritize=test_a,test_b -to make test_a run first, followed by test_b, and then the other tests. -If test_a wasn't selected for execution by regular means, --prioritize will +useful when combined with `-j` and `-r` to pin the longest-running tests +to start at the beginning of a test run. Pass `--prioritize=test_a,test_b` +to make `test_a` run first, followed by `test_b`, and then the other tests. +If test_a wasn't selected for execution by regular means, `--prioritize` will not make it execute. --f reads the names of tests from the file given as f's argument, one +`-f` reads the names of tests from the file given as `f`'s argument, one or more test names per line. Whitespace is ignored. Blank lines and -lines beginning with '#' are ignored. This is especially useful for +lines beginning with `#` are ignored. This is especially useful for whittling down failures involving interactions among tests. --L causes the leaks(1) command to be run just before exit if it exists. -leaks(1) is available on Mac OS X and presumably on some other +`-L` causes the leaks(1) command to be run just before exit if it exists. +leaks(1) is available on macOS and presumably on some other FreeBSD-derived systems. --R runs each test several times and examines sys.gettotalrefcount() to +`-R` runs each test several times and examines `sys.gettotalrefcount()` to see if the test appears to be leaking references. The argument should -be of the form stab:run:fname where 'stab' is the number of times the -test is run to let gettotalrefcount settle down, 'run' is the number -of times further it is run and 'fname' is the name of the file the -reports are written to. These parameters all have defaults (5, 4 and -"reflog.txt" respectively), and the minimal invocation is '-R :'. +be of the form `stab:run:fname` where `stab` is the number of times the +test is run to let gettotalrefcount settle down, `run` is the number +of times further it is run and `fname` is the name of the file the +reports are written to. These parameters all have defaults (`5`, `4` and +`"reflog.txt"` respectively), and the minimal invocation is `-R :`. --M runs tests that require an exorbitant amount of memory. These tests +`-M` runs tests that require an exorbitant amount of memory. These tests typically try to ascertain containers keep working when containing more than 2 billion objects, which only works on 64-bit systems. There are also some tests that try to exhaust the address space of the process, which only makes sense on 32-bit systems with at least 2Gb of memory. The passed-in memlimit, -which is a string in the form of '2.5Gb', determines how much memory the -tests will limit themselves to (but they may go slightly over.) The number +which is a string in the form of `'2.5Gb'`, determines how much memory the +tests will limit themselves to (but they may go slightly over). The number shouldn't be more memory than the machine has (including swap memory). You should also keep in mind that swap memory is generally much, much slower than RAM, and setting memlimit to all available RAM or higher will heavily @@ -90,7 +90,7 @@ to use more than memlimit memory will be skipped. The big-memory tests generally run very, very long. --u is used to specify which special resource intensive tests to run, +`-u` is used to specify which special resource intensive tests to run, such as those requiring large file support or network connectivity. The argument is a comma-separated list of words indicating the resources to test. Currently only the following are defined: @@ -137,16 +137,16 @@ wantobjects - Allows to run Tkinter tests with the specified value of tkinter.wantobjects. -To enable all resources except one, use '-uall,-'. For -example, to run all the tests except for the gui tests, give the -option '-uall,-gui'. +To enable all resources except one, use `-uall,-`. For +example, to run all the tests except for the `gui` tests, give the +option `-uall,-gui`. ---matchfile filters tests using a text file, one pattern per line. +`--matchfile` filters tests using a text file, one pattern per line. Pattern examples: -- test method: test_stat_attributes -- test class: FileTests -- test identifier: test_os.FileTests.test_stat_attributes +- test method: `test_stat_attributes` +- test class: `FileTests` +- test identifier: `test_os.FileTests.test_stat_attributes` """ diff --git a/Misc/NEWS.d/next/Library/2026-05-03-23-29-34.gh-issue-142389.SVYiSv.rst b/Misc/NEWS.d/next/Library/2026-05-03-23-29-34.gh-issue-142389.SVYiSv.rst new file mode 100644 index 00000000000000..6f3f53187dead7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-03-23-29-34.gh-issue-142389.SVYiSv.rst @@ -0,0 +1,2 @@ +Add backticks for colour to regrtest and pdb's help description. Patch by +Hugo van Kemenade. From 5847931d11f2d51058e41cbca253a3cba712e899 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 4 May 2026 14:09:03 +0530 Subject: [PATCH 2/5] gh-143732: allow dict subclasses to be specialized (GH-148128) --- Include/internal/pycore_dict.h | 4 + Include/internal/pycore_opcode_metadata.h | 8 +- Include/internal/pycore_uop_ids.h | 30 ++--- Include/internal/pycore_uop_metadata.h | 64 +++++----- Lib/test/test_capi/test_opt.py | 82 ++++++++++++- Lib/test/test_opcache.py | 62 +++++++++- Modules/_testinternalcapi/test_cases.c.h | 32 ++--- Objects/dictobject.c | 35 +++--- Python/bytecodes.c | 44 +++---- Python/executor_cases.c.h | 139 ++++++++++++++-------- Python/generated_cases.c.h | 32 ++--- Python/optimizer_bytecodes.c | 42 +++++-- Python/optimizer_cases.c.h | 52 +++++--- Python/record_functions.c.h | 21 ++-- Python/specialize.c | 8 +- 15 files changed, 455 insertions(+), 200 deletions(-) diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 6c6e3b77e69fab..ff6588b3e9718c 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -44,6 +44,10 @@ extern int _PyDict_Next( extern int _PyDict_HasOnlyStringKeys(PyObject *mp); +PyAPI_FUNC(PyObject *) _PyDict_Subscript(PyObject *self, PyObject *key); +PyAPI_FUNC(PyObject *) _PyDict_SubscriptKnownHash(PyObject *self, PyObject *key, Py_hash_t hash); +PyAPI_FUNC(int) _PyDict_StoreSubscript(PyObject *self, PyObject *key, PyObject *value); + // Export for '_ctypes' shared extension PyAPI_FUNC(Py_ssize_t) _PyDict_SizeOf(PyDictObject *); diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h index 0d7e367a5618b2..8c4134061de94c 100644 --- a/Include/internal/pycore_opcode_metadata.h +++ b/Include/internal/pycore_opcode_metadata.h @@ -1112,7 +1112,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [BINARY_OP_INPLACE_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_LOCAL_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [BINARY_OP_MULTIPLY_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, [BINARY_OP_MULTIPLY_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG }, - [BINARY_OP_SUBSCR_DICT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP_SUBSCR_DICT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [BINARY_OP_SUBSCR_GETITEM] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, [BINARY_OP_SUBSCR_LIST_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [BINARY_OP_SUBSCR_LIST_SLICE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, @@ -1315,7 +1315,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [STORE_NAME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [STORE_SLICE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [STORE_SUBSCR] = { true, INSTR_FMT_IXC, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [STORE_SUBSCR_DICT] = { true, INSTR_FMT_IXC, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [STORE_SUBSCR_DICT] = { true, INSTR_FMT_IXC, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [STORE_SUBSCR_LIST_INT] = { true, INSTR_FMT_IXC, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [SWAP] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_PURE_FLAG }, [TO_BOOL] = { true, INSTR_FMT_IXC00, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, @@ -1368,7 +1368,7 @@ _PyOpcode_macro_expansion[256] = { [BINARY_OP_INPLACE_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_INPLACE_ADD_UNICODE, OPARG_SIMPLE, 5 } } }, [BINARY_OP_MULTIPLY_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } }, [BINARY_OP_MULTIPLY_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_DICT] = { .nuops = 4, .uops = { { _GUARD_NOS_ANY_DICT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_DICT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_DICT] = { .nuops = 5, .uops = { { _RECORD_NOS_TYPE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_DICT_SUBSCRIPT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_DICT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, [BINARY_OP_SUBSCR_GETITEM] = { .nuops = 5, .uops = { { _RECORD_NOS, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_CHECK_FUNC, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_INIT_CALL, OPARG_SIMPLE, 5 }, { _PUSH_FRAME, OPARG_SIMPLE, 5 } } }, [BINARY_OP_SUBSCR_LIST_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, [BINARY_OP_SUBSCR_LIST_SLICE] = { .nuops = 5, .uops = { { _GUARD_TOS_SLICE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_SLICE, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, @@ -1531,7 +1531,7 @@ _PyOpcode_macro_expansion[256] = { [STORE_NAME] = { .nuops = 1, .uops = { { _STORE_NAME, OPARG_SIMPLE, 0 } } }, [STORE_SLICE] = { .nuops = 1, .uops = { { _STORE_SLICE, OPARG_SIMPLE, 0 } } }, [STORE_SUBSCR] = { .nuops = 1, .uops = { { _STORE_SUBSCR, OPARG_SIMPLE, 0 } } }, - [STORE_SUBSCR_DICT] = { .nuops = 3, .uops = { { _GUARD_NOS_DICT, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_DICT, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, + [STORE_SUBSCR_DICT] = { .nuops = 4, .uops = { { _RECORD_NOS_TYPE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_DICT_STORE_SUBSCRIPT, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_DICT, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, [STORE_SUBSCR_LIST_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_LIST_INT, OPARG_SIMPLE, 1 }, { _POP_TOP_INT, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, [SWAP] = { .nuops = 1, .uops = { { _SWAP, OPARG_SIMPLE, 0 } } }, [TO_BOOL] = { .nuops = 1, .uops = { { _TO_BOOL, OPARG_SIMPLE, 2 } } }, diff --git a/Include/internal/pycore_uop_ids.h b/Include/internal/pycore_uop_ids.h index bd1440a89bd82e..d1919b9f8df362 100644 --- a/Include/internal/pycore_uop_ids.h +++ b/Include/internal/pycore_uop_ids.h @@ -200,9 +200,9 @@ extern "C" { #define _GUARD_ITER_VIRTUAL 461 #define _GUARD_KEYS_VERSION 462 #define _GUARD_LOAD_SUPER_ATTR_METHOD 463 -#define _GUARD_NOS_ANY_DICT 464 -#define _GUARD_NOS_COMPACT_ASCII 465 -#define _GUARD_NOS_DICT 466 +#define _GUARD_NOS_COMPACT_ASCII 464 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT 465 +#define _GUARD_NOS_DICT_SUBSCRIPT 466 #define _GUARD_NOS_FLOAT 467 #define _GUARD_NOS_INT 468 #define _GUARD_NOS_ITER_VIRTUAL 469 @@ -845,18 +845,18 @@ extern "C" { #define _GUARD_LOAD_SUPER_ATTR_METHOD_r13 1057 #define _GUARD_LOAD_SUPER_ATTR_METHOD_r23 1058 #define _GUARD_LOAD_SUPER_ATTR_METHOD_r33 1059 -#define _GUARD_NOS_ANY_DICT_r02 1060 -#define _GUARD_NOS_ANY_DICT_r12 1061 -#define _GUARD_NOS_ANY_DICT_r22 1062 -#define _GUARD_NOS_ANY_DICT_r33 1063 -#define _GUARD_NOS_COMPACT_ASCII_r02 1064 -#define _GUARD_NOS_COMPACT_ASCII_r12 1065 -#define _GUARD_NOS_COMPACT_ASCII_r22 1066 -#define _GUARD_NOS_COMPACT_ASCII_r33 1067 -#define _GUARD_NOS_DICT_r02 1068 -#define _GUARD_NOS_DICT_r12 1069 -#define _GUARD_NOS_DICT_r22 1070 -#define _GUARD_NOS_DICT_r33 1071 +#define _GUARD_NOS_COMPACT_ASCII_r02 1060 +#define _GUARD_NOS_COMPACT_ASCII_r12 1061 +#define _GUARD_NOS_COMPACT_ASCII_r22 1062 +#define _GUARD_NOS_COMPACT_ASCII_r33 1063 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r03 1064 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r13 1065 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r23 1066 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r33 1067 +#define _GUARD_NOS_DICT_SUBSCRIPT_r02 1068 +#define _GUARD_NOS_DICT_SUBSCRIPT_r12 1069 +#define _GUARD_NOS_DICT_SUBSCRIPT_r22 1070 +#define _GUARD_NOS_DICT_SUBSCRIPT_r33 1071 #define _GUARD_NOS_FLOAT_r02 1072 #define _GUARD_NOS_FLOAT_r12 1073 #define _GUARD_NOS_FLOAT_r22 1074 diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h index 8f543dbeeb8bc9..d6ab67c2e2e86e 100644 --- a/Include/internal/pycore_uop_metadata.h +++ b/Include/internal/pycore_uop_metadata.h @@ -144,8 +144,8 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = { [_GUARD_TOS_TUPLE] = HAS_EXIT_FLAG, [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS] = HAS_EXIT_FLAG, [_BINARY_OP_SUBSCR_TUPLE_INT] = 0, - [_GUARD_NOS_DICT] = HAS_EXIT_FLAG, - [_GUARD_NOS_ANY_DICT] = HAS_EXIT_FLAG, + [_GUARD_NOS_DICT_SUBSCRIPT] = HAS_DEOPT_FLAG, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT] = HAS_DEOPT_FLAG, [_GUARD_TOS_ANY_DICT] = HAS_EXIT_FLAG, [_GUARD_TOS_DICT] = HAS_EXIT_FLAG, [_GUARD_TOS_FROZENDICT] = HAS_EXIT_FLAG, @@ -1423,22 +1423,22 @@ const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = { { -1, -1, -1 }, }, }, - [_GUARD_NOS_DICT] = { + [_GUARD_NOS_DICT_SUBSCRIPT] = { .best = { 0, 1, 2, 3 }, .entries = { - { 2, 0, _GUARD_NOS_DICT_r02 }, - { 2, 1, _GUARD_NOS_DICT_r12 }, - { 2, 2, _GUARD_NOS_DICT_r22 }, - { 3, 3, _GUARD_NOS_DICT_r33 }, + { 2, 0, _GUARD_NOS_DICT_SUBSCRIPT_r02 }, + { 2, 1, _GUARD_NOS_DICT_SUBSCRIPT_r12 }, + { 2, 2, _GUARD_NOS_DICT_SUBSCRIPT_r22 }, + { 3, 3, _GUARD_NOS_DICT_SUBSCRIPT_r33 }, }, }, - [_GUARD_NOS_ANY_DICT] = { + [_GUARD_NOS_DICT_STORE_SUBSCRIPT] = { .best = { 0, 1, 2, 3 }, .entries = { - { 2, 0, _GUARD_NOS_ANY_DICT_r02 }, - { 2, 1, _GUARD_NOS_ANY_DICT_r12 }, - { 2, 2, _GUARD_NOS_ANY_DICT_r22 }, - { 3, 3, _GUARD_NOS_ANY_DICT_r33 }, + { 3, 0, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r03 }, + { 3, 1, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r13 }, + { 3, 2, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r23 }, + { 3, 3, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r33 }, }, }, [_GUARD_TOS_ANY_DICT] = { @@ -4187,14 +4187,14 @@ const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = { [_BINARY_OP_SUBSCR_TUPLE_INT_r03] = _BINARY_OP_SUBSCR_TUPLE_INT, [_BINARY_OP_SUBSCR_TUPLE_INT_r13] = _BINARY_OP_SUBSCR_TUPLE_INT, [_BINARY_OP_SUBSCR_TUPLE_INT_r23] = _BINARY_OP_SUBSCR_TUPLE_INT, - [_GUARD_NOS_DICT_r02] = _GUARD_NOS_DICT, - [_GUARD_NOS_DICT_r12] = _GUARD_NOS_DICT, - [_GUARD_NOS_DICT_r22] = _GUARD_NOS_DICT, - [_GUARD_NOS_DICT_r33] = _GUARD_NOS_DICT, - [_GUARD_NOS_ANY_DICT_r02] = _GUARD_NOS_ANY_DICT, - [_GUARD_NOS_ANY_DICT_r12] = _GUARD_NOS_ANY_DICT, - [_GUARD_NOS_ANY_DICT_r22] = _GUARD_NOS_ANY_DICT, - [_GUARD_NOS_ANY_DICT_r33] = _GUARD_NOS_ANY_DICT, + [_GUARD_NOS_DICT_SUBSCRIPT_r02] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_SUBSCRIPT_r12] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_SUBSCRIPT_r22] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_SUBSCRIPT_r33] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r03] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r13] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r23] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r33] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, [_GUARD_TOS_ANY_DICT_r01] = _GUARD_TOS_ANY_DICT, [_GUARD_TOS_ANY_DICT_r11] = _GUARD_TOS_ANY_DICT, [_GUARD_TOS_ANY_DICT_r22] = _GUARD_TOS_ANY_DICT, @@ -5386,21 +5386,21 @@ const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = { [_GUARD_LOAD_SUPER_ATTR_METHOD_r13] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r13", [_GUARD_LOAD_SUPER_ATTR_METHOD_r23] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r23", [_GUARD_LOAD_SUPER_ATTR_METHOD_r33] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r33", - [_GUARD_NOS_ANY_DICT] = "_GUARD_NOS_ANY_DICT", - [_GUARD_NOS_ANY_DICT_r02] = "_GUARD_NOS_ANY_DICT_r02", - [_GUARD_NOS_ANY_DICT_r12] = "_GUARD_NOS_ANY_DICT_r12", - [_GUARD_NOS_ANY_DICT_r22] = "_GUARD_NOS_ANY_DICT_r22", - [_GUARD_NOS_ANY_DICT_r33] = "_GUARD_NOS_ANY_DICT_r33", [_GUARD_NOS_COMPACT_ASCII] = "_GUARD_NOS_COMPACT_ASCII", [_GUARD_NOS_COMPACT_ASCII_r02] = "_GUARD_NOS_COMPACT_ASCII_r02", [_GUARD_NOS_COMPACT_ASCII_r12] = "_GUARD_NOS_COMPACT_ASCII_r12", [_GUARD_NOS_COMPACT_ASCII_r22] = "_GUARD_NOS_COMPACT_ASCII_r22", [_GUARD_NOS_COMPACT_ASCII_r33] = "_GUARD_NOS_COMPACT_ASCII_r33", - [_GUARD_NOS_DICT] = "_GUARD_NOS_DICT", - [_GUARD_NOS_DICT_r02] = "_GUARD_NOS_DICT_r02", - [_GUARD_NOS_DICT_r12] = "_GUARD_NOS_DICT_r12", - [_GUARD_NOS_DICT_r22] = "_GUARD_NOS_DICT_r22", - [_GUARD_NOS_DICT_r33] = "_GUARD_NOS_DICT_r33", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r03] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r03", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r13] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r13", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r23] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r23", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r33] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r33", + [_GUARD_NOS_DICT_SUBSCRIPT] = "_GUARD_NOS_DICT_SUBSCRIPT", + [_GUARD_NOS_DICT_SUBSCRIPT_r02] = "_GUARD_NOS_DICT_SUBSCRIPT_r02", + [_GUARD_NOS_DICT_SUBSCRIPT_r12] = "_GUARD_NOS_DICT_SUBSCRIPT_r12", + [_GUARD_NOS_DICT_SUBSCRIPT_r22] = "_GUARD_NOS_DICT_SUBSCRIPT_r22", + [_GUARD_NOS_DICT_SUBSCRIPT_r33] = "_GUARD_NOS_DICT_SUBSCRIPT_r33", [_GUARD_NOS_FLOAT] = "_GUARD_NOS_FLOAT", [_GUARD_NOS_FLOAT_r02] = "_GUARD_NOS_FLOAT_r02", [_GUARD_NOS_FLOAT_r12] = "_GUARD_NOS_FLOAT_r12", @@ -6321,9 +6321,9 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _BINARY_OP_SUBSCR_TUPLE_INT: return 2; - case _GUARD_NOS_DICT: + case _GUARD_NOS_DICT_SUBSCRIPT: return 0; - case _GUARD_NOS_ANY_DICT: + case _GUARD_NOS_DICT_STORE_SUBSCRIPT: return 0; case _GUARD_TOS_ANY_DICT: return 0; diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index d4af910fb68c61..e371070d0b7c1c 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2273,10 +2273,49 @@ def f(n): self.assertEqual(res, TIER2_THRESHOLD) self.assertIsNotNone(ex) uops = get_opnames(ex) - self.assertEqual(uops.count("_GUARD_NOS_DICT"), 0) - self.assertEqual(uops.count("_STORE_SUBSCR_DICT_KNOWN_HASH"), 1) + self.assertEqual(uops.count("_GUARD_NOS_DICT_SUBSCRIPT"), 0) + self.assertEqual(uops.count("_GUARD_NOS_DICT_STORE_SUBSCRIPT"), 0) self.assertEqual(uops.count("_BINARY_OP_SUBSCR_DICT_KNOWN_HASH"), 1) + def test_dict_subclass_subscr(self): + import collections + + def f(n): + x = 0 + d = collections.defaultdict(int) + for _ in range(n): + d["key"] = 1 + x += d["key"] + return x + + res, ex = self._run_with_optimizer(f, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertEqual(uops.count("_BINARY_OP_SUBSCR_DICT_KNOWN_HASH"), 1) + self.assertEqual(uops.count("_STORE_SUBSCR_DICT_KNOWN_HASH"), 1) + self.assertEqual(uops.count("_GUARD_NOS_DICT_SUBSCRIPT"), 0) + self.assertEqual(uops.count("_GUARD_NOS_DICT_STORE_SUBSCRIPT"), 0) + self.assertEqual(uops.count("_GUARD_TYPE"), 1) + + def test_dict_subclass_subscr_with_override(self): + class MyDict(dict): + def __getitem__(self, key): + return 42 + + def f(n): + d = MyDict() + x = 0 + for _ in range(n): + x += d["anything"] + return x + + res, ex = self._run_with_optimizer(f, TIER2_THRESHOLD) + self.assertEqual(res, 42 * TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertEqual(uops.count("_BINARY_OP_SUBSCR_INIT_CALL"), 1) + def test_remove_guard_for_known_type_list(self): def f(n): x = 0 @@ -2599,6 +2638,23 @@ def testfunc(n): self.assertIn("_BINARY_OP_SUBSCR_DICT_KNOWN_HASH", uops) self.assertNotIn("_BINARY_OP_SUBSCR_DICT", uops) + def test_binary_op_subscr_defaultdict_known_hash(self): + # str, int, bytes, float, complex, tuple and any python object which has generic hash + import collections + + def testfunc(n): + x = 0 + d = collections.defaultdict(lambda: 1) + for _ in range(n): + x += d['a'] + d[1] + d[b'b'] + d[(1, 2)] + d[_GENERIC_KEY] + d[1.5] + d[1+2j] + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, 7 * TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_BINARY_OP_SUBSCR_DICT_KNOWN_HASH", uops) + self.assertNotIn("_BINARY_OP_SUBSCR_DICT", uops) def test_binary_op_subscr_constant_frozendict_known_hash(self): def testfunc(n): @@ -2635,6 +2691,28 @@ def testfunc(n): self.assertIn("_STORE_SUBSCR_DICT_KNOWN_HASH", uops) self.assertNotIn("_STORE_SUBSCR_DICT", uops) + def test_store_subscr_defaultdict_known_hash(self): + import collections + + def testfunc(n): + d = collections.defaultdict(lambda: 0) + for _ in range(n): + d['a'] += 1 + d[1] += 2 + d[b'b'] += 3 + d[(1, 2)] += 4 + d[_GENERIC_KEY] += 5 + d[1.5] += 6 + d[1+2j] += 7 + return d['a'] + d[1] + d[b'b'] + d[(1, 2)] + d[_GENERIC_KEY] + d[1.5] + d[1+2j] + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, 28 * TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_STORE_SUBSCR_DICT_KNOWN_HASH", uops) + self.assertNotIn("_STORE_SUBSCR_DICT", uops) + def test_contains_op(self): def testfunc(n): x = 0 diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index f5f408fcb4b311..9480bf8b87baf3 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1,3 +1,4 @@ +import collections import copy import pickle import dis @@ -1863,7 +1864,43 @@ class MyFrozenDict(frozendict): self.assertEqual(a[2], 3) binary_subscr_frozen_dict_subclass() - self.assert_no_opcode(binary_subscr_frozen_dict_subclass, "BINARY_OP_SUBSCR_DICT") + self.assert_specialized(binary_subscr_frozen_dict_subclass, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_frozen_dict_subclass, "BINARY_OP") + + def binary_subscr_defaultdict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = collections.defaultdict(lambda: 42, {1: 2, 2: 3}) + self.assertEqual(a[1], 2) + self.assertEqual(a[2], 3) + self.assertEqual(a[7], 42) + + binary_subscr_defaultdict() + self.assert_specialized(binary_subscr_defaultdict, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_defaultdict, "BINARY_OP") + + def binary_subscr_counter(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = collections.Counter('abcdeabcdabcaba') + self.assertEqual(a['a'], 5) + self.assertEqual(a['b'], 4) + self.assertEqual(a['m'], 0) + + binary_subscr_counter() + self.assert_specialized(binary_subscr_counter, "BINARY_OP_SUBSCR_DICT") + self.assert_no_opcode(binary_subscr_counter, "BINARY_OP") + + def binary_subscr_dict_subclass_override(): + class MyDict(dict): + def __getitem__(self, key): + return 42 + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = MyDict() + self.assertEqual(a['a'], 42) + self.assertEqual(a['b'], 42) + + binary_subscr_dict_subclass_override() + self.assert_no_opcode(binary_subscr_dict_subclass_override, "BINARY_OP_SUBSCR_DICT") def binary_subscr_str_int(): for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): @@ -1924,6 +1961,29 @@ def store_subscr_frozen_dict(): self.assert_specialized(store_subscr_frozen_dict, "STORE_SUBSCR_DICT") self.assert_no_opcode(store_subscr_frozen_dict, "STORE_SUBSCR") + def store_subscr_defaultdict(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = collections.defaultdict(int) + a[1] = 4 + self.assertEqual(a[1], 4) + + store_subscr_defaultdict() + self.assert_specialized(store_subscr_defaultdict, "STORE_SUBSCR_DICT") + self.assert_no_opcode(store_subscr_defaultdict, "STORE_SUBSCR") + + def store_subscr_dict_subclass_override(): + class MyDict(dict): + def __setitem__(self, key, value): + super().__setitem__(key, value * 2) + + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + a = MyDict() + a['x'] = 5 + self.assertEqual(a['x'], 10) + + store_subscr_dict_subclass_override() + self.assert_no_opcode(store_subscr_dict_subclass_override, "STORE_SUBSCR_DICT") + @cpython_only @requires_specialization def test_compare_op(self): diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index bd69211fd22206..dac46a3e22afa9 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -645,11 +645,16 @@ _PyStackRef ds; _PyStackRef ss; _PyStackRef value; - // _GUARD_NOS_ANY_DICT + // _GUARD_NOS_DICT_SUBSCRIPT { nos = stack_pointer[-2]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyAnyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UPDATE_MISS_STATS(BINARY_OP); + assert(_PyOpcode_Deopt[opcode] == (BINARY_OP)); + JUMP_TO_PREDICTED(BINARY_OP); + } + if (Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript) { UPDATE_MISS_STATS(BINARY_OP); assert(_PyOpcode_Deopt[opcode] == (BINARY_OP)); JUMP_TO_PREDICTED(BINARY_OP); @@ -662,18 +667,12 @@ dict_st = nos; PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; _PyFrame_SetStackPointer(frame, stack_pointer); - int rc = PyDict_GetItemRef(dict, sub, &res_o); + PyObject *res_o = _PyDict_Subscript(dict, sub); stack_pointer = _PyFrame_GetStackPointer(frame); - if (rc == 0) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_SetKeyError(sub); - stack_pointer = _PyFrame_GetStackPointer(frame); - } - if (rc <= 0) { + if (res_o == NULL) { JUMP_TO_LABEL(error); } res = PyStackRef_FromPyObjectSteal(res_o); @@ -12005,11 +12004,16 @@ _PyStackRef dict_st; _PyStackRef sub; _PyStackRef st; - // _GUARD_NOS_DICT + // _GUARD_NOS_DICT_STORE_SUBSCRIPT { nos = stack_pointer[-2]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UPDATE_MISS_STATS(STORE_SUBSCR); + assert(_PyOpcode_Deopt[opcode] == (STORE_SUBSCR)); + JUMP_TO_PREDICTED(STORE_SUBSCR); + } + if (Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript) { UPDATE_MISS_STATS(STORE_SUBSCR); assert(_PyOpcode_Deopt[opcode] == (STORE_SUBSCR)); JUMP_TO_PREDICTED(STORE_SUBSCR); @@ -12022,7 +12026,7 @@ dict_st = nos; value = stack_pointer[-3]; PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript); STAT_INC(STORE_SUBSCR, hit); _PyFrame_SetStackPointer(frame, stack_pointer); int err = _PyDict_SetItem_Take2((PyDictObject *)dict, diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 5be5baf8fc4cfe..42bc63acd9049c 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3669,19 +3669,13 @@ frozendict_length(PyObject *self) return _PyAnyDict_CAST(self)->ma_used; } -static PyObject * -dict_subscript(PyObject *self, PyObject *key) +PyObject * +_PyDict_SubscriptKnownHash(PyObject *self, PyObject *key, Py_hash_t hash) { PyDictObject *mp = (PyDictObject *)self; Py_ssize_t ix; - Py_hash_t hash; PyObject *value; - hash = _PyObject_HashFast(key); - if (hash == -1) { - dict_unhashable_type(self, key); - return NULL; - } ix = _Py_dict_lookup_threadsafe(mp, key, hash, &value); if (ix == DKIX_ERROR) return NULL; @@ -3705,8 +3699,19 @@ dict_subscript(PyObject *self, PyObject *key) return value; } -static int -dict_ass_sub(PyObject *mp, PyObject *v, PyObject *w) +PyObject * +_PyDict_Subscript(PyObject *self, PyObject *key) +{ + Py_hash_t hash = _PyObject_HashFast(key); + if (hash == -1) { + dict_unhashable_type(self, key); + return NULL; + } + return _PyDict_SubscriptKnownHash(self, key, hash); +} + +int +_PyDict_StoreSubscript(PyObject *mp, PyObject *v, PyObject *w) { if (w == NULL) return PyDict_DelItem(mp, v); @@ -3716,8 +3721,8 @@ dict_ass_sub(PyObject *mp, PyObject *v, PyObject *w) static PyMappingMethods dict_as_mapping = { dict_length, /*mp_length*/ - dict_subscript, /*mp_subscript*/ - dict_ass_sub, /*mp_ass_subscript*/ + _PyDict_Subscript, /*mp_subscript*/ + _PyDict_StoreSubscript, /*mp_ass_subscript*/ }; static PyObject * @@ -5103,7 +5108,7 @@ In either case, this is followed by: for k in F: D[k] = F[k]"); static PyMethodDef mapp_methods[] = { DICT___CONTAINS___METHODDEF - {"__getitem__", dict_subscript, METH_O | METH_COEXIST, + {"__getitem__", _PyDict_Subscript, METH_O | METH_COEXIST, getitem__doc__}, DICT___SIZEOF___METHODDEF DICT_GET_METHODDEF @@ -8153,12 +8158,12 @@ static PyNumberMethods frozendict_as_number = { static PyMappingMethods frozendict_as_mapping = { .mp_length = frozendict_length, - .mp_subscript = dict_subscript, + .mp_subscript = _PyDict_Subscript, }; static PyMethodDef frozendict_methods[] = { DICT___CONTAINS___METHODDEF - {"__getitem__", dict_subscript, METH_O | METH_COEXIST, getitem__doc__}, + {"__getitem__", _PyDict_Subscript, METH_O | METH_COEXIST, getitem__doc__}, DICT___SIZEOF___METHODDEF DICT_GET_METHODDEF DICT_KEYS_METHODDEF diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 963391e7598fb6..0277a94ab36632 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1277,14 +1277,16 @@ dummy_func( INPUTS_DEAD(); } - op(_GUARD_NOS_DICT, (nos, unused -- nos, unused)) { + op(_GUARD_NOS_DICT_SUBSCRIPT, (nos, unused -- nos, unused)) { PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - EXIT_IF(!PyDict_CheckExact(o)); + DEOPT_IF(!Py_TYPE(o)->tp_as_mapping); + DEOPT_IF(Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript); } - op(_GUARD_NOS_ANY_DICT, (nos, unused -- nos, unused)) { + op(_GUARD_NOS_DICT_STORE_SUBSCRIPT, (unused, nos, unused -- unused, nos, unused)) { PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - EXIT_IF(!PyAnyDict_CheckExact(o)); + DEOPT_IF(!Py_TYPE(o)->tp_as_mapping); + DEOPT_IF(Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript); } op(_GUARD_TOS_ANY_DICT, (tos -- tos)) { @@ -1303,20 +1305,16 @@ dummy_func( } macro(BINARY_OP_SUBSCR_DICT) = - _GUARD_NOS_ANY_DICT + unused/5 + _BINARY_OP_SUBSCR_DICT + POP_TOP + POP_TOP; + _RECORD_NOS_TYPE + + _GUARD_NOS_DICT_SUBSCRIPT + unused/5 + _BINARY_OP_SUBSCR_DICT + POP_TOP + POP_TOP; tier2 op(_BINARY_OP_SUBSCR_DICT_KNOWN_HASH, (dict_st, sub_st, hash/4 -- res, ds, ss)) { PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; - int rc = _PyDict_GetItemRef_KnownHash((PyDictObject *)dict, sub, (Py_hash_t)hash, &res_o); - if (rc == 0) { - _PyErr_SetKeyError(sub); - } - if (rc <= 0) { + PyObject *res_o = _PyDict_SubscriptKnownHash(dict, sub, (Py_hash_t)hash); + if (res_o == NULL) { ERROR_NO_POP(); } res = PyStackRef_FromPyObjectSteal(res_o); @@ -1328,15 +1326,10 @@ dummy_func( op(_BINARY_OP_SUBSCR_DICT, (dict_st, sub_st -- res, ds, ss)) { PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; - int rc = PyDict_GetItemRef(dict, sub, &res_o); - if (rc == 0) { - _PyErr_SetKeyError(sub); - } - if (rc <= 0) { + PyObject *res_o = _PyDict_Subscript(dict, sub); + if (res_o == NULL) { ERROR_NO_POP(); } res = PyStackRef_FromPyObjectSteal(res_o); @@ -1451,12 +1444,12 @@ dummy_func( } macro(STORE_SUBSCR_DICT) = - _GUARD_NOS_DICT + unused/1 + _STORE_SUBSCR_DICT + POP_TOP; + _RECORD_NOS_TYPE + + _GUARD_NOS_DICT_STORE_SUBSCRIPT + unused/1 + _STORE_SUBSCR_DICT + POP_TOP; op(_STORE_SUBSCR_DICT, (value, dict_st, sub -- st)) { PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - - assert(PyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript); STAT_INC(STORE_SUBSCR, hit); int err = _PyDict_SetItem_Take2((PyDictObject *)dict, PyStackRef_AsPyObjectSteal(sub), @@ -1471,8 +1464,7 @@ dummy_func( tier2 op(_STORE_SUBSCR_DICT_KNOWN_HASH, (value, dict_st, sub, hash/4 -- st)) { PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - - assert(PyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript); STAT_INC(STORE_SUBSCR, hit); int err = _PyDict_SetItem_Take2_KnownHash((PyDictObject *)dict, PyStackRef_AsPyObjectSteal(sub), diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index f8fc35de9d7957..b670ff3e766155 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -7460,13 +7460,18 @@ break; } - case _GUARD_NOS_DICT_r02: { + case _GUARD_NOS_DICT_SUBSCRIPT_r02: { CHECK_CURRENT_CACHED_VALUES(0); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; nos = stack_pointer[-2]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UOP_STAT_INC(uopcode, miss); + SET_CURRENT_CACHED_VALUES(0); + JUMP_TO_JUMP_TARGET(); + } + if (Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_JUMP_TARGET(); @@ -7480,14 +7485,20 @@ break; } - case _GUARD_NOS_DICT_r12: { + case _GUARD_NOS_DICT_SUBSCRIPT_r12: { CHECK_CURRENT_CACHED_VALUES(1); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; _PyStackRef _stack_item_0 = _tos_cache0; nos = stack_pointer[-1]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UOP_STAT_INC(uopcode, miss); + _tos_cache0 = _stack_item_0; + SET_CURRENT_CACHED_VALUES(1); + JUMP_TO_JUMP_TARGET(); + } + if (Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript) { UOP_STAT_INC(uopcode, miss); _tos_cache0 = _stack_item_0; SET_CURRENT_CACHED_VALUES(1); @@ -7502,7 +7513,7 @@ break; } - case _GUARD_NOS_DICT_r22: { + case _GUARD_NOS_DICT_SUBSCRIPT_r22: { CHECK_CURRENT_CACHED_VALUES(2); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; @@ -7510,7 +7521,14 @@ _PyStackRef _stack_item_1 = _tos_cache1; nos = _stack_item_0; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UOP_STAT_INC(uopcode, miss); + _tos_cache1 = _stack_item_1; + _tos_cache0 = nos; + SET_CURRENT_CACHED_VALUES(2); + JUMP_TO_JUMP_TARGET(); + } + if (Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript) { UOP_STAT_INC(uopcode, miss); _tos_cache1 = _stack_item_1; _tos_cache0 = nos; @@ -7524,7 +7542,7 @@ break; } - case _GUARD_NOS_DICT_r33: { + case _GUARD_NOS_DICT_SUBSCRIPT_r33: { CHECK_CURRENT_CACHED_VALUES(3); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; @@ -7533,7 +7551,15 @@ _PyStackRef _stack_item_2 = _tos_cache2; nos = _stack_item_1; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UOP_STAT_INC(uopcode, miss); + _tos_cache2 = _stack_item_2; + _tos_cache1 = nos; + _tos_cache0 = _stack_item_0; + SET_CURRENT_CACHED_VALUES(3); + JUMP_TO_JUMP_TARGET(); + } + if (Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript) { UOP_STAT_INC(uopcode, miss); _tos_cache2 = _stack_item_2; _tos_cache1 = nos; @@ -7549,49 +7575,62 @@ break; } - case _GUARD_NOS_ANY_DICT_r02: { + case _GUARD_NOS_DICT_STORE_SUBSCRIPT_r03: { CHECK_CURRENT_CACHED_VALUES(0); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; nos = stack_pointer[-2]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyAnyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_JUMP_TARGET(); } - _tos_cache1 = stack_pointer[-1]; - _tos_cache0 = nos; - SET_CURRENT_CACHED_VALUES(2); - stack_pointer += -2; + if (Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript) { + UOP_STAT_INC(uopcode, miss); + SET_CURRENT_CACHED_VALUES(0); + JUMP_TO_JUMP_TARGET(); + } + _tos_cache2 = stack_pointer[-1]; + _tos_cache1 = nos; + _tos_cache0 = stack_pointer[-3]; + SET_CURRENT_CACHED_VALUES(3); + stack_pointer += -3; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); break; } - case _GUARD_NOS_ANY_DICT_r12: { + case _GUARD_NOS_DICT_STORE_SUBSCRIPT_r13: { CHECK_CURRENT_CACHED_VALUES(1); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; _PyStackRef _stack_item_0 = _tos_cache0; nos = stack_pointer[-1]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyAnyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { UOP_STAT_INC(uopcode, miss); _tos_cache0 = _stack_item_0; SET_CURRENT_CACHED_VALUES(1); JUMP_TO_JUMP_TARGET(); } - _tos_cache1 = _stack_item_0; - _tos_cache0 = nos; - SET_CURRENT_CACHED_VALUES(2); - stack_pointer += -1; + if (Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript) { + UOP_STAT_INC(uopcode, miss); + _tos_cache0 = _stack_item_0; + SET_CURRENT_CACHED_VALUES(1); + JUMP_TO_JUMP_TARGET(); + } + _tos_cache2 = _stack_item_0; + _tos_cache1 = nos; + _tos_cache0 = stack_pointer[-2]; + SET_CURRENT_CACHED_VALUES(3); + stack_pointer += -2; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); break; } - case _GUARD_NOS_ANY_DICT_r22: { + case _GUARD_NOS_DICT_STORE_SUBSCRIPT_r23: { CHECK_CURRENT_CACHED_VALUES(2); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; @@ -7599,21 +7638,31 @@ _PyStackRef _stack_item_1 = _tos_cache1; nos = _stack_item_0; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyAnyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { UOP_STAT_INC(uopcode, miss); _tos_cache1 = _stack_item_1; _tos_cache0 = nos; SET_CURRENT_CACHED_VALUES(2); JUMP_TO_JUMP_TARGET(); } - _tos_cache1 = _stack_item_1; - _tos_cache0 = nos; - SET_CURRENT_CACHED_VALUES(2); + if (Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript) { + UOP_STAT_INC(uopcode, miss); + _tos_cache1 = _stack_item_1; + _tos_cache0 = nos; + SET_CURRENT_CACHED_VALUES(2); + JUMP_TO_JUMP_TARGET(); + } + _tos_cache2 = _stack_item_1; + _tos_cache1 = nos; + _tos_cache0 = stack_pointer[-1]; + SET_CURRENT_CACHED_VALUES(3); + stack_pointer += -1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); break; } - case _GUARD_NOS_ANY_DICT_r33: { + case _GUARD_NOS_DICT_STORE_SUBSCRIPT_r33: { CHECK_CURRENT_CACHED_VALUES(3); assert(WITHIN_STACK_BOUNDS_IGNORING_CACHE()); _PyStackRef nos; @@ -7622,7 +7671,15 @@ _PyStackRef _stack_item_2 = _tos_cache2; nos = _stack_item_1; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyAnyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UOP_STAT_INC(uopcode, miss); + _tos_cache2 = _stack_item_2; + _tos_cache1 = nos; + _tos_cache0 = _stack_item_0; + SET_CURRENT_CACHED_VALUES(3); + JUMP_TO_JUMP_TARGET(); + } + if (Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript) { UOP_STAT_INC(uopcode, miss); _tos_cache2 = _stack_item_2; _tos_cache1 = nos; @@ -7908,22 +7965,16 @@ PyObject *hash = (PyObject *)CURRENT_OPERAND0_64(); PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; stack_pointer[0] = dict_st; stack_pointer[1] = sub_st; stack_pointer += 2; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); - int rc = _PyDict_GetItemRef_KnownHash((PyDictObject *)dict, sub, (Py_hash_t)hash, &res_o); + PyObject *res_o = _PyDict_SubscriptKnownHash(dict, sub, (Py_hash_t)hash); stack_pointer = _PyFrame_GetStackPointer(frame); - if (rc == 0) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_SetKeyError(sub); - stack_pointer = _PyFrame_GetStackPointer(frame); - } - if (rc <= 0) { + if (res_o == NULL) { SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } @@ -7954,22 +8005,16 @@ dict_st = _stack_item_0; PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; stack_pointer[0] = dict_st; stack_pointer[1] = sub_st; stack_pointer += 2; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); - int rc = PyDict_GetItemRef(dict, sub, &res_o); + PyObject *res_o = _PyDict_Subscript(dict, sub); stack_pointer = _PyFrame_GetStackPointer(frame); - if (rc == 0) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_SetKeyError(sub); - stack_pointer = _PyFrame_GetStackPointer(frame); - } - if (rc <= 0) { + if (res_o == NULL) { SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } @@ -8317,7 +8362,7 @@ dict_st = _stack_item_1; value = _stack_item_0; PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript); STAT_INC(STORE_SUBSCR, hit); stack_pointer[0] = value; stack_pointer[1] = dict_st; @@ -8364,7 +8409,7 @@ value = _stack_item_0; PyObject *hash = (PyObject *)CURRENT_OPERAND0_64(); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript); STAT_INC(STORE_SUBSCR, hit); stack_pointer[0] = value; stack_pointer[1] = dict_st; diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 105375e41e360b..bd2cf1c9cb19e5 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -645,11 +645,16 @@ _PyStackRef ds; _PyStackRef ss; _PyStackRef value; - // _GUARD_NOS_ANY_DICT + // _GUARD_NOS_DICT_SUBSCRIPT { nos = stack_pointer[-2]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyAnyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UPDATE_MISS_STATS(BINARY_OP); + assert(_PyOpcode_Deopt[opcode] == (BINARY_OP)); + JUMP_TO_PREDICTED(BINARY_OP); + } + if (Py_TYPE(o)->tp_as_mapping->mp_subscript != _PyDict_Subscript) { UPDATE_MISS_STATS(BINARY_OP); assert(_PyOpcode_Deopt[opcode] == (BINARY_OP)); JUMP_TO_PREDICTED(BINARY_OP); @@ -662,18 +667,12 @@ dict_st = nos; PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; _PyFrame_SetStackPointer(frame, stack_pointer); - int rc = PyDict_GetItemRef(dict, sub, &res_o); + PyObject *res_o = _PyDict_Subscript(dict, sub); stack_pointer = _PyFrame_GetStackPointer(frame); - if (rc == 0) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_SetKeyError(sub); - stack_pointer = _PyFrame_GetStackPointer(frame); - } - if (rc <= 0) { + if (res_o == NULL) { JUMP_TO_LABEL(error); } res = PyStackRef_FromPyObjectSteal(res_o); @@ -12002,11 +12001,16 @@ _PyStackRef dict_st; _PyStackRef sub; _PyStackRef st; - // _GUARD_NOS_DICT + // _GUARD_NOS_DICT_STORE_SUBSCRIPT { nos = stack_pointer[-2]; PyObject *o = PyStackRef_AsPyObjectBorrow(nos); - if (!PyDict_CheckExact(o)) { + if (!Py_TYPE(o)->tp_as_mapping) { + UPDATE_MISS_STATS(STORE_SUBSCR); + assert(_PyOpcode_Deopt[opcode] == (STORE_SUBSCR)); + JUMP_TO_PREDICTED(STORE_SUBSCR); + } + if (Py_TYPE(o)->tp_as_mapping->mp_ass_subscript != _PyDict_StoreSubscript) { UPDATE_MISS_STATS(STORE_SUBSCR); assert(_PyOpcode_Deopt[opcode] == (STORE_SUBSCR)); JUMP_TO_PREDICTED(STORE_SUBSCR); @@ -12019,7 +12023,7 @@ dict_st = nos; value = stack_pointer[-3]; PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript); STAT_INC(STORE_SUBSCR, hit); _PyFrame_SetStackPointer(frame, stack_pointer); int err = _PyDict_SetItem_Take2((PyDictObject *)dict, diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 0837d57b61b29d..2dcfbb6d3418c0 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -2166,17 +2166,45 @@ dummy_func(void) { } } - op(_GUARD_NOS_DICT, (nos, unused -- nos, unused)) { - if (sym_matches_type(nos, &PyDict_Type)) { - ADD_OP(_NOP, 0, 0); + op(_GUARD_NOS_DICT_SUBSCRIPT, (nos, unused -- nos, unused)) { + PyTypeObject *tp = sym_get_type(nos); + bool definite = true; + if (!tp) { + tp = sym_get_probable_type(nos); + definite = false; + } + if (tp && tp->tp_as_mapping && + tp->tp_as_mapping->mp_subscript == _PyDict_Subscript) { + if (definite) { + ADD_OP(_NOP, 0, 0); + } + else { + ADD_OP(_GUARD_TYPE, 0, (uintptr_t)tp); + sym_set_type(nos, tp); + } + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)tp); + _Py_BloomFilter_Add(dependencies, tp); } - sym_set_type(nos, &PyDict_Type); } - op(_GUARD_NOS_ANY_DICT, (nos, unused -- nos, unused)) { + op(_GUARD_NOS_DICT_STORE_SUBSCRIPT, (unused, nos, unused -- unused, nos, unused)) { PyTypeObject *tp = sym_get_type(nos); - if (tp == &PyDict_Type || tp == &PyFrozenDict_Type) { - ADD_OP(_NOP, 0, 0); + bool definite = true; + if (!tp) { + tp = sym_get_probable_type(nos); + definite = false; + } + if (tp && tp->tp_as_mapping && + tp->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript) { + if (definite) { + ADD_OP(_NOP, 0, 0); + } + else { + ADD_OP(_GUARD_TYPE, 0, (uintptr_t)tp); + sym_set_type(nos, tp); + } + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)tp); + _Py_BloomFilter_Add(dependencies, tp); } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 1ade86f64b2b20..0942089760f639 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -1475,22 +1475,50 @@ break; } - case _GUARD_NOS_DICT: { + case _GUARD_NOS_DICT_SUBSCRIPT: { JitOptRef nos; nos = stack_pointer[-2]; - if (sym_matches_type(nos, &PyDict_Type)) { - ADD_OP(_NOP, 0, 0); + PyTypeObject *tp = sym_get_type(nos); + bool definite = true; + if (!tp) { + tp = sym_get_probable_type(nos); + definite = false; + } + if (tp && tp->tp_as_mapping && + tp->tp_as_mapping->mp_subscript == _PyDict_Subscript) { + if (definite) { + ADD_OP(_NOP, 0, 0); + } + else { + ADD_OP(_GUARD_TYPE, 0, (uintptr_t)tp); + sym_set_type(nos, tp); + } + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)tp); + _Py_BloomFilter_Add(dependencies, tp); } - sym_set_type(nos, &PyDict_Type); break; } - case _GUARD_NOS_ANY_DICT: { + case _GUARD_NOS_DICT_STORE_SUBSCRIPT: { JitOptRef nos; nos = stack_pointer[-2]; PyTypeObject *tp = sym_get_type(nos); - if (tp == &PyDict_Type || tp == &PyFrozenDict_Type) { - ADD_OP(_NOP, 0, 0); + bool definite = true; + if (!tp) { + tp = sym_get_probable_type(nos); + definite = false; + } + if (tp && tp->tp_as_mapping && + tp->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript) { + if (definite) { + ADD_OP(_NOP, 0, 0); + } + else { + ADD_OP(_GUARD_TYPE, 0, (uintptr_t)tp); + sym_set_type(nos, tp); + } + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)tp); + _Py_BloomFilter_Add(dependencies, tp); } break; } @@ -1582,14 +1610,10 @@ /* Start of uop copied from bytecodes for constant evaluation */ PyObject *sub = PyStackRef_AsPyObjectBorrow(sub_st); PyObject *dict = PyStackRef_AsPyObjectBorrow(dict_st); - assert(PyAnyDict_CheckExact(dict)); + assert(Py_TYPE(dict)->tp_as_mapping->mp_subscript == _PyDict_Subscript); STAT_INC(BINARY_OP, hit); - PyObject *res_o; - int rc = PyDict_GetItemRef(dict, sub, &res_o); - if (rc == 0) { - _PyErr_SetKeyError(sub); - } - if (rc <= 0) { + PyObject *res_o = _PyDict_Subscript(dict, sub); + if (res_o == NULL) { JUMP_TO_LABEL(error); } res_stackref = PyStackRef_FromPyObjectSteal(res_o); diff --git a/Python/record_functions.c.h b/Python/record_functions.c.h index 3163f45f4bb751..13996b30b9c03c 100644 --- a/Python/record_functions.c.h +++ b/Python/record_functions.c.h @@ -100,12 +100,13 @@ void _PyOpcode_RecordFunction_CODE(_PyInterpreterFrame *frame, _PyStackRef *stac #define _RECORD_TOS_TYPE_INDEX 1 #define _RECORD_NOS_INDEX 2 -#define _RECORD_3OS_GEN_FUNC_INDEX 3 -#define _RECORD_TOS_INDEX 4 -#define _RECORD_NOS_GEN_FUNC_INDEX 5 -#define _RECORD_CALLABLE_INDEX 6 -#define _RECORD_CALLABLE_KW_INDEX 7 -#define _RECORD_4OS_INDEX 8 +#define _RECORD_NOS_TYPE_INDEX 3 +#define _RECORD_3OS_GEN_FUNC_INDEX 4 +#define _RECORD_TOS_INDEX 5 +#define _RECORD_NOS_GEN_FUNC_INDEX 6 +#define _RECORD_CALLABLE_INDEX 7 +#define _RECORD_CALLABLE_KW_INDEX 8 +#define _RECORD_4OS_INDEX 9 const _PyOpcodeRecordEntry _PyOpcode_RecordEntries[256] = { [TO_BOOL_BOOL] = {1, {_RECORD_TOS_TYPE_INDEX}}, @@ -132,6 +133,9 @@ const _PyOpcodeRecordEntry _PyOpcode_RecordEntries[256] = { [BINARY_OP_SUBSCR_TUPLE_INT] = {2, {_RECORD_NOS_INDEX, _RECORD_TOS_TYPE_INDEX}}, [BINARY_OP_SUBSCR_DICT] = {2, {_RECORD_NOS_INDEX, _RECORD_TOS_TYPE_INDEX}}, [BINARY_OP_SUBSCR_GETITEM] = {2, {_RECORD_NOS_INDEX, _RECORD_TOS_TYPE_INDEX}}, + [STORE_SUBSCR] = {1, {_RECORD_NOS_TYPE_INDEX}}, + [STORE_SUBSCR_LIST_INT] = {1, {_RECORD_NOS_TYPE_INDEX}}, + [STORE_SUBSCR_DICT] = {1, {_RECORD_NOS_TYPE_INDEX}}, [SEND] = {1, {_RECORD_3OS_GEN_FUNC_INDEX}}, [SEND_GEN] = {1, {_RECORD_3OS_GEN_FUNC_INDEX}}, [STORE_ATTR] = {1, {_RECORD_TOS_TYPE_INDEX}}, @@ -197,7 +201,9 @@ const _PyOpcodeRecordEntry _PyOpcode_RecordEntries[256] = { const _PyOpcodeRecordSlotMap _PyOpcode_RecordSlotMaps[256] = { [TO_BOOL_ALWAYS_TRUE] = {1, 0, {0}}, + [BINARY_OP_SUBSCR_DICT] = {1, 1, {0}}, [BINARY_OP_SUBSCR_GETITEM] = {1, 0, {0}}, + [STORE_SUBSCR_DICT] = {1, 0, {0}}, [SEND_GEN] = {1, 0, {0}}, [LOAD_SUPER_ATTR_METHOD] = {1, 0, {0}}, [LOAD_ATTR_INSTANCE_VALUE] = {1, 1, {0}}, @@ -239,10 +245,11 @@ const _PyOpcodeRecordSlotMap _PyOpcode_RecordSlotMaps[256] = { [BINARY_OP] = {2, 2, {1, 0}}, }; -const _Py_RecordFuncPtr _PyOpcode_RecordFunctions[9] = { +const _Py_RecordFuncPtr _PyOpcode_RecordFunctions[10] = { [0] = NULL, [_RECORD_TOS_TYPE_INDEX] = _PyOpcode_RecordFunction_TOS_TYPE, [_RECORD_NOS_INDEX] = _PyOpcode_RecordFunction_NOS, + [_RECORD_NOS_TYPE_INDEX] = _PyOpcode_RecordFunction_NOS_TYPE, [_RECORD_3OS_GEN_FUNC_INDEX] = _PyOpcode_RecordFunction_3OS_GEN_FUNC, [_RECORD_TOS_INDEX] = _PyOpcode_RecordFunction_TOS, [_RECORD_NOS_GEN_FUNC_INDEX] = _PyOpcode_RecordFunction_NOS_GEN_FUNC, diff --git a/Python/specialize.c b/Python/specialize.c index 793bac58adf41a..b50728d4a2f3f3 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -1595,7 +1595,9 @@ _Py_Specialize_StoreSubscr(_PyStackRef container_st, _PyStackRef sub_st, _Py_COD return; } } - if (container_type == &PyDict_Type) { + if (container_type->tp_as_mapping != NULL && + container_type->tp_as_mapping->mp_ass_subscript == _PyDict_StoreSubscript) + { specialize(instr, STORE_SUBSCR_DICT); return; } @@ -2429,7 +2431,9 @@ _Py_Specialize_BinaryOp(_PyStackRef lhs_st, _PyStackRef rhs_st, _Py_CODEUNIT *in } } } - if (PyAnyDict_CheckExact(lhs)) { + if (Py_TYPE(lhs)->tp_as_mapping != NULL && + Py_TYPE(lhs)->tp_as_mapping->mp_subscript == _PyDict_Subscript) + { specialize(instr, BINARY_OP_SUBSCR_DICT); return; } From 35061259cc7064988586797f4cc0ba826188c70b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 4 May 2026 11:44:37 +0300 Subject: [PATCH 3/5] gh-146609: Use argparse for colour help timeit CLI (#149334) Co-authored-by: Stan Ulbrych Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/test/test_timeit.py | 26 +-- Lib/timeit.py | 178 ++++++++++-------- ...-05-03-23-47-59.gh-issue-146609.V9jqYf.rst | 1 + 3 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index f41a8a41834b3b..81f1a9c97393d1 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -259,11 +259,12 @@ def run_main(self, seconds_per_increment=1.0, switches=None, timer=None): return s.getvalue() def test_main_bad_switch(self): - s = self.run_main(switches=['--bad-switch']) - self.assertEqual(s, dedent("""\ - option --bad-switch not recognized - use -h/--help for command line help - """)) + with captured_stderr() as error_stringio: + s = self.run_main(switches=["--bad-switch"]) + self.assertEqual(s, "") + self.assertIn( + "unrecognized arguments: --bad-switch", error_stringio.getvalue() + ) def test_main_seconds(self): s = self.run_main(seconds_per_increment=5.5) @@ -301,10 +302,11 @@ def test_main_negative_reps(self): s = self.run_main(seconds_per_increment=60.0, switches=['-r-5']) self.assertEqual(s, "1 loop, best of 1: 60 sec per loop\n") - @unittest.skipIf(sys.flags.optimize >= 2, "need __doc__") def test_main_help(self): s = self.run_main(switches=['-h']) - self.assertEqual(s, timeit.__doc__) + self.assertIn("Tool for measuring execution time", s) + self.assertIn("-n", s) + self.assertIn("--number", s) def test_main_verbose(self): s = self.run_main(switches=['-v']) @@ -353,10 +355,12 @@ def test_main_with_time_unit(self): "100 loops, best of 5: 3e+03 usec per loop\n") # Test invalid unit input with captured_stderr() as error_stringio: - invalid = self.run_main(seconds_per_increment=0.003, - switches=['-u', 'parsec']) - self.assertEqual(error_stringio.getvalue(), - "Unrecognized unit. Please select nsec, usec, msec, or sec.\n") + invalid = self.run_main( + seconds_per_increment=0.003, switches=["-u", "parsec"] + ) + self.assertIn( + "choose from nsec, usec, msec, sec", error_stringio.getvalue() + ) def test_main_exception(self): with captured_stderr() as error_stringio: diff --git a/Lib/timeit.py b/Lib/timeit.py index f09ef43400ebd8..a897d9663c24e2 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -6,38 +6,6 @@ Library usage: see the Timer class. -Command line usage: - python timeit.py [-n N] [-r N] [-s S] [-p] [-h] [-t T] [--] [statement] - -Options: - -n/--number N: how many times to execute 'statement' (default: see below) - -r/--repeat N: how many times to repeat the timer (default 5) - -s/--setup S: statement to be executed once initially (default 'pass'). - Execution time of this setup statement is NOT timed. - -p/--process: use time.process_time() (default is time.perf_counter()) - -v/--verbose: print raw timing results; repeat for more digits precision - -u/--unit: set the output time unit (nsec, usec, msec, or sec) - -t/--target-time T: if --number is 0 the code will run until it - takes *at least* this many seconds - (default: 0.2) - -h/--help: print this usage message and exit - --: separate options from statement, use when statement starts with - - statement: statement to be timed (default 'pass') - -A multi-line statement may be given by specifying each line as a -separate argument; indented lines are possible by enclosing an -argument in quotes and using leading spaces. Multiple -s options are -treated similarly. - -If -n is not given, a suitable number of loops is calculated by trying -increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the -total time is at least --target-time seconds. - -Note: there is a certain baseline overhead associated with executing a -pass statement. It differs between versions. The code here doesn't try -to hide it, but you should be aware of it. The baseline overhead can be -measured by invoking the program without arguments. - Classes: Timer @@ -268,7 +236,7 @@ def main(args=None, *, _wrap_timer=None): is not None, it must be a callable that accepts a timer function and returns another timer function (used for unit testing). """ - import getopt + import argparse if args is None: args = sys.argv[1:] import _colorize @@ -276,54 +244,106 @@ def main(args=None, *, _wrap_timer=None): theme = _colorize.get_theme(force_color=colorize).timeit reset = theme.reset + epilog = """\ +A multi-line statement may be given by specifying each line as a +separate argument; indented lines are possible by enclosing an +argument in quotes and using leading spaces. Multiple `-s` options are +treated similarly. + +If `-n` is not given, a suitable number of loops is calculated by trying +increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the +total time is at least `--target-time` seconds. + +Note: there is a certain baseline overhead associated with executing a +pass statement. It differs between versions. The code here doesn't try +to hide it, but you should be aware of it. The baseline overhead can be +measured by invoking the program without arguments.""" + + parser = argparse.ArgumentParser( + prog="python -m timeit", + description="""\ +Tool for measuring execution time of small code snippets. + +This module avoids a number of common traps for measuring execution +times. See also Tim Peters' introduction to the Algorithms chapter in +the Python Cookbook, published by O'Reilly. + +Library usage: see the `Timer` class.""", + epilog=epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "-n", + "--number", + type=int, + default=0, + help="how many times to execute 'statement' (default: see below)", + ) + parser.add_argument( + "-r", + "--repeat", + type=int, + default=default_repeat, + help="how many times to repeat the timer (default %(default)s)", + ) + parser.add_argument( + "-s", + "--setup", + action="append", + default=[], + help="statement to be executed once initially. " + "Execution time of this setup statement is NOT timed. " + "(default 'pass')", + ) + parser.add_argument( + "-p", + "--process", + action="store_true", + help="use time.process_time() (default is time.perf_counter())", + ) + parser.add_argument( + "-t", + "--target-time", + type=float, + default=default_target_time, + help="if --number is 0 the code will run until it takes " + "at least this many seconds (default %(default)s)", + ) + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="print raw timing results; repeat for more digits precision", + ) + parser.add_argument( + "-u", + "--unit", + default=None, + choices=["nsec", "usec", "msec", "sec"], + help="set the output time unit", + ) + parser.add_argument( + "statement", + nargs="*", + default=["pass"], + help="statement to be timed (default 'pass')", + ) try: - opts, args = getopt.getopt(args, "n:u:s:r:pt:vh", - ["number=", "setup=", "repeat=", - "process", "target-time=", - "verbose", "unit=", "help"]) - except getopt.error as err: - print(err) - print("use -h/--help for command line help") - return 2 - - timer = default_timer - stmt = "\n".join(args) or "pass" - number = 0 # auto-determine - target_time = default_target_time - setup = [] - repeat = default_repeat - verbose = 0 - time_unit = None + ns = parser.parse_args(args) + except SystemExit as e: + return e.code + + timer = time.process_time if ns.process else default_timer + stmt = "\n".join(ns.statement) or "pass" + number = ns.number + target_time = ns.target_time + setup = "\n".join(ns.setup) or "pass" + repeat = max(ns.repeat, 1) + verbose = ns.verbose + time_unit = ns.unit units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0} - precision = 3 - for o, a in opts: - if o in ("-n", "--number"): - number = int(a) - if o in ("-s", "--setup"): - setup.append(a) - if o in ("-u", "--unit"): - if a in units: - time_unit = a - else: - print("Unrecognized unit. Please select nsec, usec, msec, or sec.", - file=sys.stderr) - return 2 - if o in ("-r", "--repeat"): - repeat = int(a) - if repeat <= 0: - repeat = 1 - if o in ("-p", "--process"): - timer = time.process_time - if o in ("-t", "--target-time"): - target_time = float(a) - if o in ("-v", "--verbose"): - if verbose: - precision += 1 - verbose += 1 - if o in ("-h", "--help"): - print(__doc__, end="") - return 0 - setup = "\n".join(setup) or "pass" + precision = 3 + max(verbose - 1, 0) # Include the current directory, so that local imports work (sys.path # contains the directory of this script, rather than the current diff --git a/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst b/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst new file mode 100644 index 00000000000000..51fde3b42494ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-03-23-47-59.gh-issue-146609.V9jqYf.rst @@ -0,0 +1 @@ +Use :mod:`argparse` for colour help :mod:`timeit` CLI. Patch by Hugo van Kemenade. From 0c6d2f64c0c83e7652760f770ff0c5cdc5040426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= Date: Mon, 4 May 2026 11:40:52 +0200 Subject: [PATCH 4/5] gh-148093: Raise binascii.Error from binascii.a2b_uu() on empty input (GH-149077) Instead of reading past the end of the empty buffer. --- Lib/test/test_binascii.py | 7 +++++++ .../2026-04-27-22-34-09.gh-issue-148093.9pWceM.rst | 2 ++ Modules/binascii.c | 8 ++++++++ 3 files changed, 17 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-27-22-34-09.gh-issue-148093.9pWceM.rst diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py index 6991e2ef6815e3..cedbdc61f18f34 100644 --- a/Lib/test/test_binascii.py +++ b/Lib/test/test_binascii.py @@ -1306,6 +1306,10 @@ def test_uu(self): self.assertEqual(binascii.a2b_uu(b"\xff"), b"\x00"*31) self.assertRaises(binascii.Error, binascii.a2b_uu, b"\xff\x00") self.assertRaises(binascii.Error, binascii.a2b_uu, b"!!!!") + self.assertRaises(binascii.Error, binascii.a2b_uu, + self.type2test(b"")) + self.assertRaises(binascii.Error, binascii.a2b_uu, + self.type2test(b"#86)C")[:0]) self.assertRaises(binascii.Error, binascii.b2a_uu, 46*b"!") # Issue #7701 (crash on a pydebug build) @@ -1522,6 +1526,9 @@ def test_empty_string(self): binascii.crc_hqx(empty, 0) continue f = getattr(binascii, func) + if func == 'a2b_uu': + self.assertRaises(binascii.Error, f, empty) + continue try: f(empty) except Exception as err: diff --git a/Misc/NEWS.d/next/Library/2026-04-27-22-34-09.gh-issue-148093.9pWceM.rst b/Misc/NEWS.d/next/Library/2026-04-27-22-34-09.gh-issue-148093.9pWceM.rst new file mode 100644 index 00000000000000..9418044201f8bd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-27-22-34-09.gh-issue-148093.9pWceM.rst @@ -0,0 +1,2 @@ +Fix an out-of-bounds read of one byte in :func:`binascii.a2b_uu`. Raise +:exc:`binascii.Error`, instead of reading past the buffer end. diff --git a/Modules/binascii.c b/Modules/binascii.c index 7e6e9655f8d498..673dca6ee134bd 100644 --- a/Modules/binascii.c +++ b/Modules/binascii.c @@ -508,6 +508,14 @@ binascii_a2b_uu_impl(PyObject *module, Py_buffer *data) assert(ascii_len >= 0); /* First byte: binary data length (in bytes) */ + if (ascii_len == 0) { + state = get_binascii_state(module); + if (state == NULL) { + return NULL; + } + PyErr_SetString(state->Error, "Missing length byte"); + return NULL; + } bin_len = (*ascii_data++ - ' ') & 077; ascii_len--; From 246fe14e7ccfdea62c7d92b49a272abd55acc850 Mon Sep 17 00:00:00 2001 From: kishorhange111 <101962449+kishorhange111@users.noreply.github.com> Date: Mon, 4 May 2026 15:21:17 +0530 Subject: [PATCH 5/5] gh-148849: Deprecate http.cookies.BaseCookie.js_output() (GH-148978) --- Doc/deprecations/pending-removal-in-3.19.rst | 9 ++++ Doc/library/http.cookies.rst | 12 ++++++ Doc/whatsnew/3.15.rst | 10 +++++ Lib/http/cookies.py | 20 ++++++++- Lib/test/test_http_cookies.py | 41 +++++++++++++++---- ...-04-25-12-04-27.gh-issue-148849.Vk6yEW.rst | 4 ++ 6 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-25-12-04-27.gh-issue-148849.Vk6yEW.rst diff --git a/Doc/deprecations/pending-removal-in-3.19.rst b/Doc/deprecations/pending-removal-in-3.19.rst index 25f9cba390de68..044bb8a3934a2a 100644 --- a/Doc/deprecations/pending-removal-in-3.19.rst +++ b/Doc/deprecations/pending-removal-in-3.19.rst @@ -22,3 +22,12 @@ Pending removal in Python 3.19 supported depending on the backend implementation of hash functions. Prefer passing the initial data as a positional argument for maximum backwards compatibility. + +* :mod:`http.cookies`: + + * :meth:`http.cookies.Morsel.js_output` is deprecated and will be + removed in Python 3.19. + + * :meth:`http.cookies.BaseCookie.js_output` is deprecated and will be + removed in Python 3.19. + diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst index 1122b30d29def0..4965c5fc3ba1d8 100644 --- a/Doc/library/http.cookies.rst +++ b/Doc/library/http.cookies.rst @@ -107,6 +107,12 @@ Cookie Objects The meaning for *attrs* is the same as in :meth:`output`. + .. deprecated-removed:: 3.15 3.19 + This method generates a JavaScript snippet to set cookies in the browser, + which is no longer considered a standard or recommended approach. + Use :meth:`~http.cookies.BaseCookie.output` instead to generate HTTP + headers. + .. method:: BaseCookie.load(rawdata) @@ -223,6 +229,12 @@ Morsel Objects The meaning for *attrs* is the same as in :meth:`output`. + .. deprecated-removed:: 3.15 3.19 + This method generates a JavaScript snippet to set cookies in the browser, + which is no longer considered a standard or recommended approach. + Use :meth:`~http.cookies.Morsel.output` instead to generate HTTP + headers. + .. method:: Morsel.OutputString(attrs=None) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 586a1306d83c4c..02a3a11e0564d9 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1921,6 +1921,16 @@ New deprecations (Contributed by Bénédikt Tran in :gh:`134978`.) +* :mod:`http.cookies`: + + * :meth:`Morsel.js_output ` and + :meth:`BaseCookie.js_output ` are + deprecated and will be removed in Python 3.19. Use + :meth:`Morsel.output ` or + :meth:`BaseCookie.output ` instead. + (Contributed by kishorhange111 in :gh:`148849`.) + + * :mod:`re`: * :func:`re.match` and :meth:`re.Pattern.match` are now diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py index 5c5b14788dc2f0..800a2c18e3fa41 100644 --- a/Lib/http/cookies.py +++ b/Lib/http/cookies.py @@ -132,6 +132,7 @@ import re import string import types +lazy import warnings __all__ = ["CookieError", "BaseCookie", "SimpleCookie"] @@ -390,7 +391,9 @@ def output(self, attrs=None, header="Set-Cookie:"): def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.OutputString()) - def js_output(self, attrs=None): + + def _js_output(self, attrs=None): + """Internal implementation without deprecation warning.""" import base64 # Print javascript output_string = self.OutputString(attrs) @@ -407,6 +410,14 @@ def js_output(self, attrs=None): """ % (output_encoded,) + def js_output(self, attrs=None): + warnings._deprecated( + "http.cookies.Morsel.js_output", + message=warnings._DEPRECATED_MSG + "; use output() instead", + remove=(3, 19), + ) + return self._js_output(attrs) + def OutputString(self, attrs=None): # Build up our result # @@ -541,10 +552,15 @@ def __repr__(self): def js_output(self, attrs=None): """Return a string suitable for JavaScript.""" + warnings._deprecated( + "http.cookies.BaseCookie.js_output", + message=warnings._DEPRECATED_MSG + "; use output() instead", + remove=(3, 19), + ) result = [] items = sorted(self.items()) for key, value in items: - result.append(value.js_output(attrs)) + result.append(value._js_output(attrs)) return _nulljoin(result) def load(self, rawdata): diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py index 4884b07c95b9c5..cde268e3241850 100644 --- a/Lib/test/test_http_cookies.py +++ b/Lib/test/test_http_cookies.py @@ -153,7 +153,8 @@ def test_load(self): self.assertEqual(C.output(['path']), 'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme') cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; Path=/acme; Version=1').decode('ascii') - self.assertEqual(C.js_output(), fr""" + with self.assertWarnsRegex(DeprecationWarning, r"BaseCookie\.js_output"): + self.assertEqual(C.js_output(), fr""" """) cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; Path=/acme').decode('ascii') - self.assertEqual(C.js_output(['path']), fr""" + with self.assertWarnsRegex(DeprecationWarning, r"BaseCookie\.js_output"): + self.assertEqual(C.js_output(['path']), fr""" """ % (expected_encoded_cookie,) - self.assertEqual(M.js_output(), expected_js_output) + with self.assertWarnsRegex(DeprecationWarning, r"Morsel\.js_output"): + self.assertEqual(M.js_output(), expected_js_output) for i in ["foo bar", "foo@bar"]: # Try some illegal characters self.assertRaises(cookies.CookieError, @@ -650,7 +655,8 @@ def test_control_characters_output(self): cookie = cookies.SimpleCookie() cookie["cookie"] = morsel with self.assertRaises(cookies.CookieError): - cookie.js_output() + with self.assertWarnsRegex(DeprecationWarning, r"Morsel\.js_output"): + cookie.js_output() morsel = cookies.Morsel() morsel.set("key", "value", "coded-value") @@ -658,8 +664,29 @@ def test_control_characters_output(self): cookie = cookies.SimpleCookie() cookie["cookie"] = morsel with self.assertRaises(cookies.CookieError): - cookie.js_output() + with self.assertWarnsRegex(DeprecationWarning, r"Morsel\.js_output"): + cookie.js_output() + def test_morsel_js_output_deprecated(self): + morsel = cookies.Morsel() + morsel.set("key", "value", "value") + with self.assertWarnsRegex(DeprecationWarning, r"Morsel\.js_output") as cm: + result = morsel.js_output() + self.assertEqual(cm.filename, __file__) + self.assertIn("document.cookie", result) + + + def test_basecookie_js_output_warns_once(self): + C = cookies.SimpleCookie() + C["key"] = "value" + with self.assertWarns(DeprecationWarning) as cm: + C.js_output() + deprecation_warnings = [ + w for w in cm.warnings if issubclass(w.category, DeprecationWarning) + ] + self.assertEqual(len(deprecation_warnings), 1) + self.assertRegex(str(deprecation_warnings[0].message), r"BaseCookie\.js_output") + self.assertEqual(cm.filename, __file__) def load_tests(loader, tests, pattern): tests.addTest(doctest.DocTestSuite(cookies)) diff --git a/Misc/NEWS.d/next/Library/2026-04-25-12-04-27.gh-issue-148849.Vk6yEW.rst b/Misc/NEWS.d/next/Library/2026-04-25-12-04-27.gh-issue-148849.Vk6yEW.rst new file mode 100644 index 00000000000000..9725d63747d451 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-25-12-04-27.gh-issue-148849.Vk6yEW.rst @@ -0,0 +1,4 @@ +Deprecate :meth:`http.cookies.Morsel.js_output` and +:meth:`http.cookies.BaseCookie.js_output`, which will be removed in +Python 3.19. Use :meth:`http.cookies.Morsel.output` or +:meth:`http.cookies.BaseCookie.output` instead.