From 1637c046fc5ad7f0a375fd17792ad7e49899d2a3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 2 Nov 2025 22:22:58 +0100 Subject: [PATCH 01/22] [WIP] PEP 814: Add built-in frozendict type * Basic tests * Bare minimum documentation * Support frozendict in marshal, pickle, json * Replace dict with frozendict in many stdlib modules Co-Authored-by: Donghee Na Co-Authored-by: Marco Sulla --- Doc/library/stdtypes.rst | 19 +- Include/cpython/dictobject.h | 15 +- Include/internal/pycore_dict.h | 9 + Include/internal/pycore_object.h | 3 +- Lib/_collections_abc.py | 1 + Lib/_compat_pickle.py | 2 + Lib/_opcode_metadata.py | 516 +++++++++--------- Lib/_pydatetime.py | 14 +- Lib/_pydecimal.py | 9 +- Lib/bdb.py | 4 +- Lib/dataclasses.py | 5 +- Lib/dis.py | 8 +- Lib/email/headerregistry.py | 4 +- Lib/enum.py | 4 +- Lib/functools.py | 4 +- Lib/gettext.py | 5 +- Lib/imaplib.py | 6 +- Lib/json/decoder.py | 8 +- Lib/json/encoder.py | 12 +- Lib/json/tool.py | 8 +- Lib/locale.py | 9 +- Lib/opcode.py | 46 +- Lib/optparse.py | 9 +- Lib/pickle.py | 33 ++ Lib/pickletools.py | 22 + Lib/platform.py | 8 +- Lib/plistlib.py | 2 +- Lib/pydoc_data/topics.py | 5 +- Lib/ssl.py | 3 +- Lib/stringprep.py | 4 +- Lib/symtable.py | 2 +- Lib/tarfile.py | 8 +- Lib/test/mapping_tests.py | 129 ++--- Lib/test/test_dict.py | 3 + Lib/test/test_doctest/test_doctest.py | 2 +- Lib/test/test_inspect/test_inspect.py | 3 +- Lib/test/test_pickletools.py | 2 +- Lib/token.py | 11 +- Lib/tomllib/_parser.py | 4 +- Lib/typing.py | 4 +- Modules/_json.c | 2 +- Modules/_pickle.c | 124 ++++- Modules/errnomodule.c | 22 +- Objects/dictobject.c | 258 ++++++++- Objects/moduleobject.c | 1 + Objects/typeobject.c | 4 +- Python/bltinmodule.c | 1 + Python/marshal.c | 22 +- .../cases_generator/py_metadata_generator.py | 18 +- 49 files changed, 935 insertions(+), 482 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 97e7e08364e0bd..d74345416f3644 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4914,8 +4914,8 @@ The constructors for both classes work the same: .. _typesmapping: -Mapping Types --- :class:`dict` -=============================== +Mapping Types --- :class:`dict`, :class:`frozendict` +==================================================== .. index:: pair: object; mapping @@ -4926,8 +4926,9 @@ Mapping Types --- :class:`dict` pair: built-in function; len A :term:`mapping` object maps :term:`hashable` values to arbitrary objects. -Mappings are mutable objects. There is currently only one standard mapping -type, the :dfn:`dictionary`. (For other containers see the built-in +Mappings are mutable objects. There is currently two standard mapping +types, the :dfn:`dictionary` and :class:`frozendict`. +(For other containers see the built-in :class:`list`, :class:`set`, and :class:`tuple` classes, and the :mod:`collections` module.) @@ -5199,6 +5200,15 @@ can be used interchangeably to index the same dictionary entry. .. versionchanged:: 3.8 Dictionaries are now reversible. +.. class:: frozendict(**kwargs) + frozendict(mapping, /, **kwargs) + frozendict(iterable, /, **kwargs) + + Return a new frozen dictionary initialized from an optional positional + argument and a possibly empty set of keyword arguments. + + .. versionadded:: next + .. seealso:: :class:`types.MappingProxyType` can be used to create a read-only view @@ -5532,6 +5542,7 @@ list is non-exhaustive. * :class:`list` * :class:`dict` * :class:`set` +* :class:`frozendict` * :class:`frozenset` * :class:`type` * :class:`asyncio.Future` diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h index df9ec7050fca1a..17bbed74cda862 100644 --- a/Include/cpython/dictobject.h +++ b/Include/cpython/dictobject.h @@ -32,6 +32,16 @@ typedef struct { PyDictValues *ma_values; } PyDictObject; +// frozendict +PyAPI_DATA(PyTypeObject) PyFrozenDict_Type; +#define PyFrozenDict_Check(op) PyObject_TypeCheck((op), &PyFrozenDict_Type) +#define PyFrozenDict_CheckExact(op) Py_IS_TYPE((op), &PyFrozenDict_Type) + +#define _PyAnyDict_CheckExact(ob) \ + (PyDict_CheckExact(ob) || PyFrozenDict_CheckExact(ob)) +#define _PyAnyDict_Check(ob) \ + (PyDict_Check(ob) || PyFrozenDict_Check(ob)) + PyAPI_FUNC(PyObject *) _PyDict_GetItem_KnownHash(PyObject *mp, PyObject *key, Py_hash_t hash); // PyDict_GetItemStringRef() can be used instead @@ -52,7 +62,7 @@ PyAPI_FUNC(int) PyDict_SetDefaultRef(PyObject *mp, PyObject *key, PyObject *defa /* Get the number of items of a dictionary. */ static inline Py_ssize_t PyDict_GET_SIZE(PyObject *op) { PyDictObject *mp; - assert(PyDict_Check(op)); + assert(_PyAnyDict_Check(op)); mp = _Py_CAST(PyDictObject*, op); #ifdef Py_GIL_DISABLED return _Py_atomic_load_ssize_relaxed(&mp->ma_used); @@ -103,3 +113,6 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id); // Mark given dictionary as "watched" (callback will be called if it is modified) PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict); PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict); + +// Create a frozendict. Create an empty dictionary if iterable is NULL. +PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable); diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index b8fe360321d14b..757329d491cd4d 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -406,6 +406,15 @@ _Py_DECREF_BUILTINS(PyObject *op) } #endif +/* frozendict */ +typedef struct { + PyDictObject ob_base; + Py_hash_t ma_hash; +} PyFrozenDictObject; + +#define _PyFrozenDictObject_CAST(op) \ + (assert(PyFrozenDict_Check(op)), _Py_CAST(PyFrozenDictObject*, (op))) + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 980d6d7764bd2c..fb50acd62da5eb 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -863,8 +863,7 @@ static inline Py_hash_t _PyObject_HashFast(PyObject *op) { if (PyUnicode_CheckExact(op)) { - Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED( - _PyASCIIObject_CAST(op)->hash); + Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(op); if (hash != -1) { return hash; } diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 60b471317ce97c..23cc6d8faae2da 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -823,6 +823,7 @@ def __eq__(self, other): __reversed__ = None +Mapping.register(frozendict) Mapping.register(mappingproxy) Mapping.register(framelocalsproxy) diff --git a/Lib/_compat_pickle.py b/Lib/_compat_pickle.py index a981326432429b..6f868c9890e7c0 100644 --- a/Lib/_compat_pickle.py +++ b/Lib/_compat_pickle.py @@ -183,6 +183,7 @@ 'StringIO': 'io', 'cStringIO': 'io', }) +IMPORT_MAPPING = frozendict(IMPORT_MAPPING) REVERSE_IMPORT_MAPPING.update({ '_bz2': 'bz2', @@ -198,6 +199,7 @@ ('UserDict', 'UserDict'): ('collections', 'UserDict'), ('socket', '_socketobject'): ('socket', 'SocketType'), }) +NAME_MAPPING = frozendict(NAME_MAPPING) REVERSE_NAME_MAPPING.update({ ('_functools', 'reduce'): ('__builtin__', 'reduce'), diff --git a/Lib/_opcode_metadata.py b/Lib/_opcode_metadata.py index f168d169a32948..c0bb805f69ceba 100644 --- a/Lib/_opcode_metadata.py +++ b/Lib/_opcode_metadata.py @@ -2,11 +2,11 @@ # from: # Python/bytecodes.c # Do not edit! -_specializations = { - "RESUME": [ +_specializations = frozendict( + RESUME= [ "RESUME_CHECK", ], - "TO_BOOL": [ + TO_BOOL= [ "TO_BOOL_ALWAYS_TRUE", "TO_BOOL_BOOL", "TO_BOOL_INT", @@ -14,7 +14,7 @@ "TO_BOOL_NONE", "TO_BOOL_STR", ], - "BINARY_OP": [ + BINARY_OP= [ "BINARY_OP_MULTIPLY_INT", "BINARY_OP_ADD_INT", "BINARY_OP_SUBTRACT_INT", @@ -31,32 +31,32 @@ "BINARY_OP_EXTEND", "BINARY_OP_INPLACE_ADD_UNICODE", ], - "STORE_SUBSCR": [ + STORE_SUBSCR= [ "STORE_SUBSCR_DICT", "STORE_SUBSCR_LIST_INT", ], - "SEND": [ + SEND= [ "SEND_GEN", ], - "UNPACK_SEQUENCE": [ + UNPACK_SEQUENCE= [ "UNPACK_SEQUENCE_TWO_TUPLE", "UNPACK_SEQUENCE_TUPLE", "UNPACK_SEQUENCE_LIST", ], - "STORE_ATTR": [ + STORE_ATTR= [ "STORE_ATTR_INSTANCE_VALUE", "STORE_ATTR_SLOT", "STORE_ATTR_WITH_HINT", ], - "LOAD_GLOBAL": [ + LOAD_GLOBAL= [ "LOAD_GLOBAL_MODULE", "LOAD_GLOBAL_BUILTIN", ], - "LOAD_SUPER_ATTR": [ + LOAD_SUPER_ATTR= [ "LOAD_SUPER_ATTR_ATTR", "LOAD_SUPER_ATTR_METHOD", ], - "LOAD_ATTR": [ + LOAD_ATTR= [ "LOAD_ATTR_INSTANCE_VALUE", "LOAD_ATTR_MODULE", "LOAD_ATTR_WITH_HINT", @@ -71,26 +71,26 @@ "LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES", "LOAD_ATTR_NONDESCRIPTOR_NO_DICT", ], - "COMPARE_OP": [ + COMPARE_OP= [ "COMPARE_OP_FLOAT", "COMPARE_OP_INT", "COMPARE_OP_STR", ], - "CONTAINS_OP": [ + CONTAINS_OP= [ "CONTAINS_OP_SET", "CONTAINS_OP_DICT", ], - "JUMP_BACKWARD": [ + JUMP_BACKWARD= [ "JUMP_BACKWARD_NO_JIT", "JUMP_BACKWARD_JIT", ], - "FOR_ITER": [ + FOR_ITER= [ "FOR_ITER_LIST", "FOR_ITER_TUPLE", "FOR_ITER_RANGE", "FOR_ITER_GEN", ], - "CALL": [ + CALL= [ "CALL_BOUND_METHOD_EXACT_ARGS", "CALL_PY_EXACT_ARGS", "CALL_TYPE_1", @@ -112,254 +112,254 @@ "CALL_BOUND_METHOD_GENERAL", "CALL_NON_PY_GENERAL", ], - "CALL_KW": [ + CALL_KW= [ "CALL_KW_BOUND_METHOD", "CALL_KW_PY", "CALL_KW_NON_PY", ], -} +) -_specialized_opmap = { - 'BINARY_OP_ADD_FLOAT': 129, - 'BINARY_OP_ADD_INT': 130, - 'BINARY_OP_ADD_UNICODE': 131, - 'BINARY_OP_EXTEND': 132, - 'BINARY_OP_INPLACE_ADD_UNICODE': 3, - 'BINARY_OP_MULTIPLY_FLOAT': 133, - 'BINARY_OP_MULTIPLY_INT': 134, - 'BINARY_OP_SUBSCR_DICT': 135, - 'BINARY_OP_SUBSCR_GETITEM': 136, - 'BINARY_OP_SUBSCR_LIST_INT': 137, - 'BINARY_OP_SUBSCR_LIST_SLICE': 138, - 'BINARY_OP_SUBSCR_STR_INT': 139, - 'BINARY_OP_SUBSCR_TUPLE_INT': 140, - 'BINARY_OP_SUBTRACT_FLOAT': 141, - 'BINARY_OP_SUBTRACT_INT': 142, - 'CALL_ALLOC_AND_ENTER_INIT': 143, - 'CALL_BOUND_METHOD_EXACT_ARGS': 144, - 'CALL_BOUND_METHOD_GENERAL': 145, - 'CALL_BUILTIN_CLASS': 146, - 'CALL_BUILTIN_FAST': 147, - 'CALL_BUILTIN_FAST_WITH_KEYWORDS': 148, - 'CALL_BUILTIN_O': 149, - 'CALL_ISINSTANCE': 150, - 'CALL_KW_BOUND_METHOD': 151, - 'CALL_KW_NON_PY': 152, - 'CALL_KW_PY': 153, - 'CALL_LEN': 154, - 'CALL_LIST_APPEND': 155, - 'CALL_METHOD_DESCRIPTOR_FAST': 156, - 'CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS': 157, - 'CALL_METHOD_DESCRIPTOR_NOARGS': 158, - 'CALL_METHOD_DESCRIPTOR_O': 159, - 'CALL_NON_PY_GENERAL': 160, - 'CALL_PY_EXACT_ARGS': 161, - 'CALL_PY_GENERAL': 162, - 'CALL_STR_1': 163, - 'CALL_TUPLE_1': 164, - 'CALL_TYPE_1': 165, - 'COMPARE_OP_FLOAT': 166, - 'COMPARE_OP_INT': 167, - 'COMPARE_OP_STR': 168, - 'CONTAINS_OP_DICT': 169, - 'CONTAINS_OP_SET': 170, - 'FOR_ITER_GEN': 171, - 'FOR_ITER_LIST': 172, - 'FOR_ITER_RANGE': 173, - 'FOR_ITER_TUPLE': 174, - 'JUMP_BACKWARD_JIT': 175, - 'JUMP_BACKWARD_NO_JIT': 176, - 'LOAD_ATTR_CLASS': 177, - 'LOAD_ATTR_CLASS_WITH_METACLASS_CHECK': 178, - 'LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN': 179, - 'LOAD_ATTR_INSTANCE_VALUE': 180, - 'LOAD_ATTR_METHOD_LAZY_DICT': 181, - 'LOAD_ATTR_METHOD_NO_DICT': 182, - 'LOAD_ATTR_METHOD_WITH_VALUES': 183, - 'LOAD_ATTR_MODULE': 184, - 'LOAD_ATTR_NONDESCRIPTOR_NO_DICT': 185, - 'LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES': 186, - 'LOAD_ATTR_PROPERTY': 187, - 'LOAD_ATTR_SLOT': 188, - 'LOAD_ATTR_WITH_HINT': 189, - 'LOAD_GLOBAL_BUILTIN': 190, - 'LOAD_GLOBAL_MODULE': 191, - 'LOAD_SUPER_ATTR_ATTR': 192, - 'LOAD_SUPER_ATTR_METHOD': 193, - 'RESUME_CHECK': 194, - 'SEND_GEN': 195, - 'STORE_ATTR_INSTANCE_VALUE': 196, - 'STORE_ATTR_SLOT': 197, - 'STORE_ATTR_WITH_HINT': 198, - 'STORE_SUBSCR_DICT': 199, - 'STORE_SUBSCR_LIST_INT': 200, - 'TO_BOOL_ALWAYS_TRUE': 201, - 'TO_BOOL_BOOL': 202, - 'TO_BOOL_INT': 203, - 'TO_BOOL_LIST': 204, - 'TO_BOOL_NONE': 205, - 'TO_BOOL_STR': 206, - 'UNPACK_SEQUENCE_LIST': 207, - 'UNPACK_SEQUENCE_TUPLE': 208, - 'UNPACK_SEQUENCE_TWO_TUPLE': 209, -} +_specialized_opmap = frozendict( + BINARY_OP_ADD_FLOAT= 129, + BINARY_OP_ADD_INT= 130, + BINARY_OP_ADD_UNICODE= 131, + BINARY_OP_EXTEND= 132, + BINARY_OP_INPLACE_ADD_UNICODE= 3, + BINARY_OP_MULTIPLY_FLOAT= 133, + BINARY_OP_MULTIPLY_INT= 134, + BINARY_OP_SUBSCR_DICT= 135, + BINARY_OP_SUBSCR_GETITEM= 136, + BINARY_OP_SUBSCR_LIST_INT= 137, + BINARY_OP_SUBSCR_LIST_SLICE= 138, + BINARY_OP_SUBSCR_STR_INT= 139, + BINARY_OP_SUBSCR_TUPLE_INT= 140, + BINARY_OP_SUBTRACT_FLOAT= 141, + BINARY_OP_SUBTRACT_INT= 142, + CALL_ALLOC_AND_ENTER_INIT= 143, + CALL_BOUND_METHOD_EXACT_ARGS= 144, + CALL_BOUND_METHOD_GENERAL= 145, + CALL_BUILTIN_CLASS= 146, + CALL_BUILTIN_FAST= 147, + CALL_BUILTIN_FAST_WITH_KEYWORDS= 148, + CALL_BUILTIN_O= 149, + CALL_ISINSTANCE= 150, + CALL_KW_BOUND_METHOD= 151, + CALL_KW_NON_PY= 152, + CALL_KW_PY= 153, + CALL_LEN= 154, + CALL_LIST_APPEND= 155, + CALL_METHOD_DESCRIPTOR_FAST= 156, + CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS= 157, + CALL_METHOD_DESCRIPTOR_NOARGS= 158, + CALL_METHOD_DESCRIPTOR_O= 159, + CALL_NON_PY_GENERAL= 160, + CALL_PY_EXACT_ARGS= 161, + CALL_PY_GENERAL= 162, + CALL_STR_1= 163, + CALL_TUPLE_1= 164, + CALL_TYPE_1= 165, + COMPARE_OP_FLOAT= 166, + COMPARE_OP_INT= 167, + COMPARE_OP_STR= 168, + CONTAINS_OP_DICT= 169, + CONTAINS_OP_SET= 170, + FOR_ITER_GEN= 171, + FOR_ITER_LIST= 172, + FOR_ITER_RANGE= 173, + FOR_ITER_TUPLE= 174, + JUMP_BACKWARD_JIT= 175, + JUMP_BACKWARD_NO_JIT= 176, + LOAD_ATTR_CLASS= 177, + LOAD_ATTR_CLASS_WITH_METACLASS_CHECK= 178, + LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN= 179, + LOAD_ATTR_INSTANCE_VALUE= 180, + LOAD_ATTR_METHOD_LAZY_DICT= 181, + LOAD_ATTR_METHOD_NO_DICT= 182, + LOAD_ATTR_METHOD_WITH_VALUES= 183, + LOAD_ATTR_MODULE= 184, + LOAD_ATTR_NONDESCRIPTOR_NO_DICT= 185, + LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES= 186, + LOAD_ATTR_PROPERTY= 187, + LOAD_ATTR_SLOT= 188, + LOAD_ATTR_WITH_HINT= 189, + LOAD_GLOBAL_BUILTIN= 190, + LOAD_GLOBAL_MODULE= 191, + LOAD_SUPER_ATTR_ATTR= 192, + LOAD_SUPER_ATTR_METHOD= 193, + RESUME_CHECK= 194, + SEND_GEN= 195, + STORE_ATTR_INSTANCE_VALUE= 196, + STORE_ATTR_SLOT= 197, + STORE_ATTR_WITH_HINT= 198, + STORE_SUBSCR_DICT= 199, + STORE_SUBSCR_LIST_INT= 200, + TO_BOOL_ALWAYS_TRUE= 201, + TO_BOOL_BOOL= 202, + TO_BOOL_INT= 203, + TO_BOOL_LIST= 204, + TO_BOOL_NONE= 205, + TO_BOOL_STR= 206, + UNPACK_SEQUENCE_LIST= 207, + UNPACK_SEQUENCE_TUPLE= 208, + UNPACK_SEQUENCE_TWO_TUPLE= 209, +) -opmap = { - 'CACHE': 0, - 'RESERVED': 17, - 'RESUME': 128, - 'INSTRUMENTED_LINE': 254, - 'ENTER_EXECUTOR': 255, - 'BINARY_SLICE': 1, - 'BUILD_TEMPLATE': 2, - 'CALL_FUNCTION_EX': 4, - 'CHECK_EG_MATCH': 5, - 'CHECK_EXC_MATCH': 6, - 'CLEANUP_THROW': 7, - 'DELETE_SUBSCR': 8, - 'END_FOR': 9, - 'END_SEND': 10, - 'EXIT_INIT_CHECK': 11, - 'FORMAT_SIMPLE': 12, - 'FORMAT_WITH_SPEC': 13, - 'GET_AITER': 14, - 'GET_ANEXT': 15, - 'GET_ITER': 16, - 'GET_LEN': 18, - 'GET_YIELD_FROM_ITER': 19, - 'INTERPRETER_EXIT': 20, - 'LOAD_BUILD_CLASS': 21, - 'LOAD_LOCALS': 22, - 'MAKE_FUNCTION': 23, - 'MATCH_KEYS': 24, - 'MATCH_MAPPING': 25, - 'MATCH_SEQUENCE': 26, - 'NOP': 27, - 'NOT_TAKEN': 28, - 'POP_EXCEPT': 29, - 'POP_ITER': 30, - 'POP_TOP': 31, - 'PUSH_EXC_INFO': 32, - 'PUSH_NULL': 33, - 'RETURN_GENERATOR': 34, - 'RETURN_VALUE': 35, - 'SETUP_ANNOTATIONS': 36, - 'STORE_SLICE': 37, - 'STORE_SUBSCR': 38, - 'TO_BOOL': 39, - 'UNARY_INVERT': 40, - 'UNARY_NEGATIVE': 41, - 'UNARY_NOT': 42, - 'WITH_EXCEPT_START': 43, - 'BINARY_OP': 44, - 'BUILD_INTERPOLATION': 45, - 'BUILD_LIST': 46, - 'BUILD_MAP': 47, - 'BUILD_SET': 48, - 'BUILD_SLICE': 49, - 'BUILD_STRING': 50, - 'BUILD_TUPLE': 51, - 'CALL': 52, - 'CALL_INTRINSIC_1': 53, - 'CALL_INTRINSIC_2': 54, - 'CALL_KW': 55, - 'COMPARE_OP': 56, - 'CONTAINS_OP': 57, - 'CONVERT_VALUE': 58, - 'COPY': 59, - 'COPY_FREE_VARS': 60, - 'DELETE_ATTR': 61, - 'DELETE_DEREF': 62, - 'DELETE_FAST': 63, - 'DELETE_GLOBAL': 64, - 'DELETE_NAME': 65, - 'DICT_MERGE': 66, - 'DICT_UPDATE': 67, - 'END_ASYNC_FOR': 68, - 'EXTENDED_ARG': 69, - 'FOR_ITER': 70, - 'GET_AWAITABLE': 71, - 'IMPORT_FROM': 72, - 'IMPORT_NAME': 73, - 'IS_OP': 74, - 'JUMP_BACKWARD': 75, - 'JUMP_BACKWARD_NO_INTERRUPT': 76, - 'JUMP_FORWARD': 77, - 'LIST_APPEND': 78, - 'LIST_EXTEND': 79, - 'LOAD_ATTR': 80, - 'LOAD_COMMON_CONSTANT': 81, - 'LOAD_CONST': 82, - 'LOAD_DEREF': 83, - 'LOAD_FAST': 84, - 'LOAD_FAST_AND_CLEAR': 85, - 'LOAD_FAST_BORROW': 86, - 'LOAD_FAST_BORROW_LOAD_FAST_BORROW': 87, - 'LOAD_FAST_CHECK': 88, - 'LOAD_FAST_LOAD_FAST': 89, - 'LOAD_FROM_DICT_OR_DEREF': 90, - 'LOAD_FROM_DICT_OR_GLOBALS': 91, - 'LOAD_GLOBAL': 92, - 'LOAD_NAME': 93, - 'LOAD_SMALL_INT': 94, - 'LOAD_SPECIAL': 95, - 'LOAD_SUPER_ATTR': 96, - 'MAKE_CELL': 97, - 'MAP_ADD': 98, - 'MATCH_CLASS': 99, - 'POP_JUMP_IF_FALSE': 100, - 'POP_JUMP_IF_NONE': 101, - 'POP_JUMP_IF_NOT_NONE': 102, - 'POP_JUMP_IF_TRUE': 103, - 'RAISE_VARARGS': 104, - 'RERAISE': 105, - 'SEND': 106, - 'SET_ADD': 107, - 'SET_FUNCTION_ATTRIBUTE': 108, - 'SET_UPDATE': 109, - 'STORE_ATTR': 110, - 'STORE_DEREF': 111, - 'STORE_FAST': 112, - 'STORE_FAST_LOAD_FAST': 113, - 'STORE_FAST_STORE_FAST': 114, - 'STORE_GLOBAL': 115, - 'STORE_NAME': 116, - 'SWAP': 117, - 'UNPACK_EX': 118, - 'UNPACK_SEQUENCE': 119, - 'YIELD_VALUE': 120, - 'INSTRUMENTED_END_FOR': 234, - 'INSTRUMENTED_POP_ITER': 235, - 'INSTRUMENTED_END_SEND': 236, - 'INSTRUMENTED_FOR_ITER': 237, - 'INSTRUMENTED_INSTRUCTION': 238, - 'INSTRUMENTED_JUMP_FORWARD': 239, - 'INSTRUMENTED_NOT_TAKEN': 240, - 'INSTRUMENTED_POP_JUMP_IF_TRUE': 241, - 'INSTRUMENTED_POP_JUMP_IF_FALSE': 242, - 'INSTRUMENTED_POP_JUMP_IF_NONE': 243, - 'INSTRUMENTED_POP_JUMP_IF_NOT_NONE': 244, - 'INSTRUMENTED_RESUME': 245, - 'INSTRUMENTED_RETURN_VALUE': 246, - 'INSTRUMENTED_YIELD_VALUE': 247, - 'INSTRUMENTED_END_ASYNC_FOR': 248, - 'INSTRUMENTED_LOAD_SUPER_ATTR': 249, - 'INSTRUMENTED_CALL': 250, - 'INSTRUMENTED_CALL_KW': 251, - 'INSTRUMENTED_CALL_FUNCTION_EX': 252, - 'INSTRUMENTED_JUMP_BACKWARD': 253, - 'ANNOTATIONS_PLACEHOLDER': 256, - 'JUMP': 257, - 'JUMP_IF_FALSE': 258, - 'JUMP_IF_TRUE': 259, - 'JUMP_NO_INTERRUPT': 260, - 'LOAD_CLOSURE': 261, - 'POP_BLOCK': 262, - 'SETUP_CLEANUP': 263, - 'SETUP_FINALLY': 264, - 'SETUP_WITH': 265, - 'STORE_FAST_MAYBE_NULL': 266, -} +opmap = frozendict( + CACHE= 0, + RESERVED= 17, + RESUME= 128, + INSTRUMENTED_LINE= 254, + ENTER_EXECUTOR= 255, + BINARY_SLICE= 1, + BUILD_TEMPLATE= 2, + CALL_FUNCTION_EX= 4, + CHECK_EG_MATCH= 5, + CHECK_EXC_MATCH= 6, + CLEANUP_THROW= 7, + DELETE_SUBSCR= 8, + END_FOR= 9, + END_SEND= 10, + EXIT_INIT_CHECK= 11, + FORMAT_SIMPLE= 12, + FORMAT_WITH_SPEC= 13, + GET_AITER= 14, + GET_ANEXT= 15, + GET_ITER= 16, + GET_LEN= 18, + GET_YIELD_FROM_ITER= 19, + INTERPRETER_EXIT= 20, + LOAD_BUILD_CLASS= 21, + LOAD_LOCALS= 22, + MAKE_FUNCTION= 23, + MATCH_KEYS= 24, + MATCH_MAPPING= 25, + MATCH_SEQUENCE= 26, + NOP= 27, + NOT_TAKEN= 28, + POP_EXCEPT= 29, + POP_ITER= 30, + POP_TOP= 31, + PUSH_EXC_INFO= 32, + PUSH_NULL= 33, + RETURN_GENERATOR= 34, + RETURN_VALUE= 35, + SETUP_ANNOTATIONS= 36, + STORE_SLICE= 37, + STORE_SUBSCR= 38, + TO_BOOL= 39, + UNARY_INVERT= 40, + UNARY_NEGATIVE= 41, + UNARY_NOT= 42, + WITH_EXCEPT_START= 43, + BINARY_OP= 44, + BUILD_INTERPOLATION= 45, + BUILD_LIST= 46, + BUILD_MAP= 47, + BUILD_SET= 48, + BUILD_SLICE= 49, + BUILD_STRING= 50, + BUILD_TUPLE= 51, + CALL= 52, + CALL_INTRINSIC_1= 53, + CALL_INTRINSIC_2= 54, + CALL_KW= 55, + COMPARE_OP= 56, + CONTAINS_OP= 57, + CONVERT_VALUE= 58, + COPY= 59, + COPY_FREE_VARS= 60, + DELETE_ATTR= 61, + DELETE_DEREF= 62, + DELETE_FAST= 63, + DELETE_GLOBAL= 64, + DELETE_NAME= 65, + DICT_MERGE= 66, + DICT_UPDATE= 67, + END_ASYNC_FOR= 68, + EXTENDED_ARG= 69, + FOR_ITER= 70, + GET_AWAITABLE= 71, + IMPORT_FROM= 72, + IMPORT_NAME= 73, + IS_OP= 74, + JUMP_BACKWARD= 75, + JUMP_BACKWARD_NO_INTERRUPT= 76, + JUMP_FORWARD= 77, + LIST_APPEND= 78, + LIST_EXTEND= 79, + LOAD_ATTR= 80, + LOAD_COMMON_CONSTANT= 81, + LOAD_CONST= 82, + LOAD_DEREF= 83, + LOAD_FAST= 84, + LOAD_FAST_AND_CLEAR= 85, + LOAD_FAST_BORROW= 86, + LOAD_FAST_BORROW_LOAD_FAST_BORROW= 87, + LOAD_FAST_CHECK= 88, + LOAD_FAST_LOAD_FAST= 89, + LOAD_FROM_DICT_OR_DEREF= 90, + LOAD_FROM_DICT_OR_GLOBALS= 91, + LOAD_GLOBAL= 92, + LOAD_NAME= 93, + LOAD_SMALL_INT= 94, + LOAD_SPECIAL= 95, + LOAD_SUPER_ATTR= 96, + MAKE_CELL= 97, + MAP_ADD= 98, + MATCH_CLASS= 99, + POP_JUMP_IF_FALSE= 100, + POP_JUMP_IF_NONE= 101, + POP_JUMP_IF_NOT_NONE= 102, + POP_JUMP_IF_TRUE= 103, + RAISE_VARARGS= 104, + RERAISE= 105, + SEND= 106, + SET_ADD= 107, + SET_FUNCTION_ATTRIBUTE= 108, + SET_UPDATE= 109, + STORE_ATTR= 110, + STORE_DEREF= 111, + STORE_FAST= 112, + STORE_FAST_LOAD_FAST= 113, + STORE_FAST_STORE_FAST= 114, + STORE_GLOBAL= 115, + STORE_NAME= 116, + SWAP= 117, + UNPACK_EX= 118, + UNPACK_SEQUENCE= 119, + YIELD_VALUE= 120, + INSTRUMENTED_END_FOR= 234, + INSTRUMENTED_POP_ITER= 235, + INSTRUMENTED_END_SEND= 236, + INSTRUMENTED_FOR_ITER= 237, + INSTRUMENTED_INSTRUCTION= 238, + INSTRUMENTED_JUMP_FORWARD= 239, + INSTRUMENTED_NOT_TAKEN= 240, + INSTRUMENTED_POP_JUMP_IF_TRUE= 241, + INSTRUMENTED_POP_JUMP_IF_FALSE= 242, + INSTRUMENTED_POP_JUMP_IF_NONE= 243, + INSTRUMENTED_POP_JUMP_IF_NOT_NONE= 244, + INSTRUMENTED_RESUME= 245, + INSTRUMENTED_RETURN_VALUE= 246, + INSTRUMENTED_YIELD_VALUE= 247, + INSTRUMENTED_END_ASYNC_FOR= 248, + INSTRUMENTED_LOAD_SUPER_ATTR= 249, + INSTRUMENTED_CALL= 250, + INSTRUMENTED_CALL_KW= 251, + INSTRUMENTED_CALL_FUNCTION_EX= 252, + INSTRUMENTED_JUMP_BACKWARD= 253, + ANNOTATIONS_PLACEHOLDER= 256, + JUMP= 257, + JUMP_IF_FALSE= 258, + JUMP_IF_TRUE= 259, + JUMP_NO_INTERRUPT= 260, + LOAD_CLOSURE= 261, + POP_BLOCK= 262, + SETUP_CLEANUP= 263, + SETUP_FINALLY= 264, + SETUP_WITH= 265, + STORE_FAST_MAYBE_NULL= 266, +) HAVE_ARGUMENT = 43 MIN_INSTRUMENTED_OPCODE = 234 diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..1a93279286d080 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -164,13 +164,13 @@ def _build_struct_time(y, m, d, hh, mm, ss, dstflag): return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag)) def _format_time(hh, mm, ss, us, timespec='auto'): - specs = { - 'hours': '{:02d}', - 'minutes': '{:02d}:{:02d}', - 'seconds': '{:02d}:{:02d}:{:02d}', - 'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}', - 'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}' - } + specs = frozendict( + hours= '{:02d}', + minutes= '{:02d}:{:02d}', + seconds= '{:02d}:{:02d}:{:02d}', + milliseconds= '{:02d}:{:02d}:{:02d}.{:03d}', + microseconds= '{:02d}:{:02d}:{:02d}.{:06d}' + ) if timespec == 'auto': # Skip trailing microseconds when us==0. diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index ef889ea0cc834c..06e6d1df567f0c 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -331,10 +331,11 @@ class FloatOperation(DecimalException, TypeError): Underflow, InvalidOperation, Subnormal, FloatOperation] # Map conditions (per the spec) to signals -_condition_map = {ConversionSyntax:InvalidOperation, - DivisionImpossible:InvalidOperation, - DivisionUndefined:InvalidOperation, - InvalidContext:InvalidOperation} +_condition_map = frozendict({ + ConversionSyntax:InvalidOperation, + DivisionImpossible:InvalidOperation, + DivisionUndefined:InvalidOperation, + InvalidContext:InvalidOperation}) # Valid rounding modes _rounding_modes = (ROUND_DOWN, ROUND_HALF_UP, ROUND_HALF_EVEN, ROUND_CEILING, diff --git a/Lib/bdb.py b/Lib/bdb.py index efc3e0a235ac8e..2f406f1fced4b5 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -20,7 +20,7 @@ class BdbQuit(Exception): E = sys.monitoring.events class _MonitoringTracer: - EVENT_CALLBACK_MAP = { + EVENT_CALLBACK_MAP = frozendict({ E.PY_START: 'call', E.PY_RESUME: 'call', E.PY_THROW: 'call', @@ -32,7 +32,7 @@ class _MonitoringTracer: E.RAISE: 'exception', E.STOP_ITERATION: 'exception', E.INSTRUCTION: 'opcode', - } + }) GLOBAL_EVENTS = E.PY_START | E.PY_RESUME | E.PY_THROW | E.PY_UNWIND | E.RAISE LOCAL_EVENTS = E.LINE | E.JUMP | E.PY_RETURN | E.PY_YIELD | E.STOP_ITERATION diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 3ccb72469286eb..d83e19d77ecee6 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -957,7 +957,8 @@ def _hash_exception(cls, fields, func_builder): # | | | | +------- action # | | | | | # v v v v v -_hash_action = {(False, False, False, False): None, +_hash_action = frozendict( + {(False, False, False, False): None, (False, False, False, True ): None, (False, False, True, False): None, (False, False, True, True ): None, @@ -973,7 +974,7 @@ def _hash_exception(cls, fields, func_builder): (True, True, False, True ): _hash_exception, (True, True, True, False): _hash_add, (True, True, True, True ): _hash_exception, - } + }) # See https://bugs.python.org/issue32929#msg312829 for an if-statement # version of this table. diff --git a/Lib/dis.py b/Lib/dis.py index d6d2c1386dd785..4e7d64d9ed164c 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -65,9 +65,9 @@ _all_opname[op] = name _all_opmap[name] = op -deoptmap = { +deoptmap = frozendict({ specialized: base for base, family in _specializations.items() for specialized in family -} +}) def _try_compile(source, name): """Attempts to compile the given source, first as an expression and @@ -152,7 +152,7 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets # The inspect module interrogates this dictionary to build its # list of CO_* constants. It is also used by pretty_flags to # turn the co_flags field into a human readable list. -COMPILER_FLAG_NAMES = { +COMPILER_FLAG_NAMES = frozendict({ 1: "OPTIMIZED", 2: "NEWLOCALS", 4: "VARARGS", @@ -165,7 +165,7 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False, show_offsets 512: "ASYNC_GENERATOR", 0x4000000: "HAS_DOCSTRING", 0x8000000: "METHOD", -} +}) def pretty_flags(flags): """Return pretty representation of code flags.""" diff --git a/Lib/email/headerregistry.py b/Lib/email/headerregistry.py index 543141dc427ebe..d15df7cc2fbfb1 100644 --- a/Lib/email/headerregistry.py +++ b/Lib/email/headerregistry.py @@ -3,8 +3,6 @@ This module provides an implementation of the HeaderRegistry API. The implementation is designed to flexibly follow RFC5322 rules. """ -from types import MappingProxyType - from email import utils from email import errors from email import _header_value_parser as parser @@ -462,7 +460,7 @@ def init(self, *args, **kw): @property def params(self): - return MappingProxyType(self._params) + return frozendict(self._params) class ContentTypeHeader(ParameterizedMIMEHeader): diff --git a/Lib/enum.py b/Lib/enum.py index ad782b8c41e160..43b4982691e214 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1,6 +1,6 @@ import sys import builtins as bltns -from types import MappingProxyType, DynamicClassAttribute +from types import DynamicClassAttribute __all__ = [ @@ -817,7 +817,7 @@ def __members__(cls): This mapping lists all enum members, including aliases. Note that this is a read-only view of the internal mapping. """ - return MappingProxyType(cls._member_map_) + return frozendict(cls._member_map_) def __repr__(cls): if Flag is not None and issubclass(cls, Flag): diff --git a/Lib/functools.py b/Lib/functools.py index a92844ba7227b0..2e8565a660b293 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -170,7 +170,7 @@ def _lt_from_ge(self, other): return op_result return not op_result -_convert = { +_convert = frozendict({ '__lt__': [('__gt__', _gt_from_lt), ('__le__', _le_from_lt), ('__ge__', _ge_from_lt)], @@ -183,7 +183,7 @@ def _lt_from_ge(self, other): '__ge__': [('__le__', _le_from_ge), ('__gt__', _gt_from_ge), ('__lt__', _lt_from_ge)] -} +}) def total_ordering(cls): """Class decorator that fills in missing ordering methods""" diff --git a/Lib/gettext.py b/Lib/gettext.py index 6c11ab2b1eb570..2f77f0e849e9ae 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -111,8 +111,9 @@ def _error(value): ('+', '-'), ('*', '/', '%'), ) -_binary_ops = {op: i for i, ops in enumerate(_binary_ops, 1) for op in ops} -_c2py_ops = {'||': 'or', '&&': 'and', '/': '//'} +_binary_ops = frozendict({op: i for i, ops in enumerate(_binary_ops, 1) + for op in ops}) +_c2py_ops = frozendict({'||': 'or', '&&': 'and', '/': '//'}) def _parse(tokens, priority=-1): diff --git a/Lib/imaplib.py b/Lib/imaplib.py index c176736548188c..e5984b0446de39 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -54,7 +54,7 @@ # Commands -Commands = { +Commands = frozendict({ # name valid states 'APPEND': ('AUTH', 'SELECTED'), 'AUTHENTICATE': ('NONAUTH',), @@ -99,7 +99,7 @@ 'UID': ('SELECTED',), 'UNSUBSCRIBE': ('AUTH', 'SELECTED'), 'UNSELECT': ('SELECTED',), - } + }) # Patterns to match server responses @@ -1755,7 +1755,7 @@ def decode(self, inp): return binascii.a2b_base64(inp) Months = ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ') -Mon2num = {s.encode():n+1 for n, s in enumerate(Months[1:])} +Mon2num = frozendict({s.encode():n+1 for n, s in enumerate(Months[1:])}) def Internaldate2tuple(resp): """Parse an IMAP4 INTERNALDATE string. diff --git a/Lib/json/decoder.py b/Lib/json/decoder.py index 92ad6352557640..d1490491920845 100644 --- a/Lib/json/decoder.py +++ b/Lib/json/decoder.py @@ -43,19 +43,19 @@ def __reduce__(self): return self.__class__, (self.msg, self.doc, self.pos) -_CONSTANTS = { +_CONSTANTS = frozendict({ '-Infinity': NegInf, 'Infinity': PosInf, 'NaN': NaN, -} +}) HEXDIGITS = re.compile(r'[0-9A-Fa-f]{4}', FLAGS) STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) -BACKSLASH = { +BACKSLASH = frozendict({ '"': '"', '\\': '\\', '/': '/', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', -} +}) def _decode_uXXXX(s, pos, _m=HEXDIGITS.match): esc = _m(s, pos + 1) diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 5cf6d64f3eade6..285ad02ed9544f 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -30,6 +30,9 @@ for i in range(0x20): ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) #ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +# freeze the dict to prevent accidental modifications +ESCAPE_DCT = frozendict(ESCAPE_DCT) del i INFINITY = float('inf') @@ -79,7 +82,7 @@ class JSONEncoder(object): +-------------------+---------------+ | Python | JSON | +===================+===============+ - | dict | object | + | dict, frozendict | object | +-------------------+---------------+ | list, tuple | array | +-------------------+---------------+ @@ -267,6 +270,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, ## HACK: hand-optimized bytecode; turn globals into locals ValueError=ValueError, dict=dict, + frozendict=frozendict, float=float, id=id, int=int, @@ -319,7 +323,7 @@ def _iterencode_list(lst, _current_indent_level): yield buf if isinstance(value, (list, tuple)): chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): + elif isinstance(value, (dict, frozendict)): chunks = _iterencode_dict(value, _current_indent_level) else: chunks = _iterencode(value, _current_indent_level) @@ -406,7 +410,7 @@ def _iterencode_dict(dct, _current_indent_level): else: if isinstance(value, (list, tuple)): chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): + elif isinstance(value, (dict, frozendict)): chunks = _iterencode_dict(value, _current_indent_level) else: chunks = _iterencode(value, _current_indent_level) @@ -440,7 +444,7 @@ def _iterencode(o, _current_indent_level): yield _floatstr(o) elif isinstance(o, (list, tuple)): yield from _iterencode_list(o, _current_indent_level) - elif isinstance(o, dict): + elif isinstance(o, (dict, frozendict)): yield from _iterencode_dict(o, _current_indent_level) else: if markers is not None: diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 050c2fe2161e3e..14a5c9ecb74d13 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -22,13 +22,7 @@ (?Pnull) ''', re.VERBOSE) -_group_to_theme_color = { - "key": "definition", - "string": "string", - "number": "number", - "boolean": "keyword", - "null": "keyword", -} +_group_to_theme_color = frozendict(key="definition",string="string",number="number",boolean="keyword",null="keyword") def _colorize_json(json_str, theme): diff --git a/Lib/locale.py b/Lib/locale.py index 37cafb4a601b3c..a3aef3e421da87 100644 --- a/Lib/locale.py +++ b/Lib/locale.py @@ -757,6 +757,7 @@ def getpreferredencoding(do_setlocale=True): for k, v in sorted(locale_encoding_alias.items()): k = k.replace('_', '') locale_encoding_alias.setdefault(k, v) +locale_encoding_alias = frozendict(locale_encoding_alias) del k, v # @@ -909,7 +910,7 @@ def getpreferredencoding(do_setlocale=True): # removed 'el_gr@euro' # removed 'uz_uz@cyrillic' -locale_alias = { +locale_alias = frozendict({ 'a3': 'az_AZ.KOI8-C', 'a3_az': 'az_AZ.KOI8-C', 'a3_az.koic': 'az_AZ.KOI8-C', @@ -1509,7 +1510,7 @@ def getpreferredencoding(do_setlocale=True): 'zh_tw.euctw': 'zh_TW.eucTW', 'zu': 'zu_ZA.ISO8859-1', 'zu_za': 'zu_ZA.ISO8859-1', -} +}) # # This maps Windows language identifiers to locale strings. @@ -1525,7 +1526,7 @@ def getpreferredencoding(do_setlocale=True): # locale code. # -windows_locale = { +windows_locale = frozendict({ 0x0436: "af_ZA", # Afrikaans 0x041c: "sq_AL", # Albanian 0x0484: "gsw_FR",# Alsatian - France @@ -1736,7 +1737,7 @@ def getpreferredencoding(do_setlocale=True): 0x0478: "ii_CN", # Yi - PRC 0x046a: "yo_NG", # Yoruba - Nigeria 0x0435: "zu_ZA", # Zulu -} +}) def _print_locale(): diff --git a/Lib/opcode.py b/Lib/opcode.py index 0e9520b6832499..ef183475972053 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -45,78 +45,78 @@ hascompare = [opmap["COMPARE_OP"]] -_cache_format = { - "LOAD_GLOBAL": { +_cache_format = frozendict( + LOAD_GLOBAL= { "counter": 1, "index": 1, "module_keys_version": 1, "builtin_keys_version": 1, }, - "BINARY_OP": { + BINARY_OP= { "counter": 1, "descr": 4, }, - "UNPACK_SEQUENCE": { + UNPACK_SEQUENCE= { "counter": 1, }, - "COMPARE_OP": { + COMPARE_OP= { "counter": 1, }, - "CONTAINS_OP": { + CONTAINS_OP= { "counter": 1, }, - "FOR_ITER": { + FOR_ITER= { "counter": 1, }, - "LOAD_SUPER_ATTR": { + LOAD_SUPER_ATTR= { "counter": 1, }, - "LOAD_ATTR": { + LOAD_ATTR= { "counter": 1, "version": 2, "keys_version": 2, "descr": 4, }, - "STORE_ATTR": { + STORE_ATTR= { "counter": 1, "version": 2, "index": 1, }, - "CALL": { + CALL= { "counter": 1, "func_version": 2, }, - "CALL_KW": { + CALL_KW= { "counter": 1, "func_version": 2, }, - "STORE_SUBSCR": { + STORE_SUBSCR= { "counter": 1, }, - "SEND": { + SEND= { "counter": 1, }, - "JUMP_BACKWARD": { + JUMP_BACKWARD= { "counter": 1, }, - "TO_BOOL": { + TO_BOOL= { "counter": 1, "version": 2, }, - "POP_JUMP_IF_TRUE": { + POP_JUMP_IF_TRUE= { "counter": 1, }, - "POP_JUMP_IF_FALSE": { + POP_JUMP_IF_FALSE= { "counter": 1, }, - "POP_JUMP_IF_NONE": { + POP_JUMP_IF_NONE= { "counter": 1, }, - "POP_JUMP_IF_NOT_NONE": { + POP_JUMP_IF_NOT_NONE= { "counter": 1, }, -} +) -_inline_cache_entries = { +_inline_cache_entries = frozendict({ name : sum(value.values()) for (name, value) in _cache_format.items() -} +}) diff --git a/Lib/optparse.py b/Lib/optparse.py index 02ff7140882ed6..2f885876b0e691 100644 --- a/Lib/optparse.py +++ b/Lib/optparse.py @@ -407,10 +407,11 @@ def _parse_num(val, type): def _parse_int(val): return _parse_num(val, int) -_builtin_cvt = { "int" : (_parse_int, _("integer")), - "long" : (_parse_int, _("integer")), - "float" : (float, _("floating-point")), - "complex" : (complex, _("complex")) } +_builtin_cvt = frozendict( + { "int" : (_parse_int, _("integer")), + "long" : (_parse_int, _("integer")), + "float" : (float, _("floating-point")), + "complex" : (complex, _("complex")) }) def check_builtin(option, opt, value): (cvt, what) = _builtin_cvt[option.type] diff --git a/Lib/pickle.py b/Lib/pickle.py index 729c215514ad24..be346aacdf086a 100644 --- a/Lib/pickle.py +++ b/Lib/pickle.py @@ -185,6 +185,7 @@ def __init__(self, value): BYTEARRAY8 = b'\x96' # push bytearray NEXT_BUFFER = b'\x97' # push next out-of-band buffer READONLY_BUFFER = b'\x98' # make top of stack readonly +FROZENDICT = b'\x99' __all__.extend(x for x in dir() if x.isupper() and not x.startswith('_')) @@ -1064,6 +1065,31 @@ def save_dict(self, obj): dispatch[dict] = save_dict + def save_frozendict(self, obj): + save = self.save + write = self.write + + if self.proto < 5: + self.save_reduce(frozendict, (dict(obj),), obj=obj) + return + + write(MARK) + for k, v in obj.items(): + save(k) + save(v) + + if id(obj) in self.memo: + # If the object is already in the memo, this means it is + # recursive. In this case, throw away everything we put on the + # stack, and fetch the object back from the memo. + write(POP_MARK + self.get(self.memo[id(obj)][0])) + return + + write(FROZENDICT) + self.memoize(obj) + + dispatch[frozendict] = save_frozendict + def _batch_setitems(self, items, obj): # Helper to batch up SETITEMS sequences; proto >= 1 only save = self.save @@ -1589,6 +1615,13 @@ def load_dict(self): self.append(d) dispatch[DICT[0]] = load_dict + def load_frozendict(self): + items = self.pop_mark() + d = frozendict({items[i]: items[i+1] for i in range(0, len(items), 2)}) + self.append(d) + + dispatch[FROZENDICT[0]] = load_frozendict + # INST and OBJ differ only in how they get a class object. It's not # only sensible to do the rest in a common routine, the two routines # previously diverged and grew different bugs. diff --git a/Lib/pickletools.py b/Lib/pickletools.py index 254b6c7fcc9dd2..1dff5add96ebad 100644 --- a/Lib/pickletools.py +++ b/Lib/pickletools.py @@ -1035,6 +1035,11 @@ def __repr__(self): obtype=dict, doc="A Python dict object.") +pyfrozendict = StackObject( + name="frozendict", + obtype=frozendict, + doc="A Python frozendict object.") + pyset = StackObject( name="set", obtype=set, @@ -1384,6 +1389,23 @@ def __init__(self, name, code, arg, proto=5, doc="Make an out-of-band buffer object read-only."), + I(name='FROZENDICT', + code='\x99', + arg=None, + stack_before=[markobject, stackslice], + stack_after=[pyfrozendict], + proto=5, + doc="""Build a frozendict out of the topmost stack slice, after markobject. + + All the stack entries following the topmost markobject are placed into + a single Python dict, which single dict object replaces all of the + stack from the topmost markobject onward. The stack slice alternates + key, value, key, value, .... For example, + + Stack before: ... markobject 1 2 3 'abc' + Stack after: ... {1: 2, 3: 'abc'} + """), + # Ways to spell None. I(name='NONE', diff --git a/Lib/platform.py b/Lib/platform.py index 4db93bea2a39e1..d3db1933a672a5 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -127,7 +127,7 @@ # Based on the description of the PHP's version_compare(): # http://php.net/manual/en/function.version-compare.php -_ver_stages = { +_ver_stages = frozendict({ # any string not found in this dict, will get 0 assigned 'dev': 10, 'alpha': 20, 'a': 20, @@ -136,7 +136,7 @@ 'RC': 50, 'rc': 50, # number, will get 100 assigned 'pl': 200, 'p': 200, -} +}) def _comparable_version(version): @@ -706,11 +706,11 @@ def _syscmd_file(target, default=''): # Default values for architecture; non-empty strings override the # defaults given as parameters -_default_architecture = { +_default_architecture = frozendict({ 'win32': ('', 'WindowsPE'), 'win16': ('', 'Windows'), 'dos': ('', 'MSDOS'), -} +}) def architecture(executable=sys.executable, bits='', linkage=''): diff --git a/Lib/plistlib.py b/Lib/plistlib.py index 67e832db217319..952b0670cf77b1 100644 --- a/Lib/plistlib.py +++ b/Lib/plistlib.py @@ -457,7 +457,7 @@ class InvalidFileException (ValueError): def __init__(self, message="Invalid file"): ValueError.__init__(self, message) -_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} +_BINARY_FORMAT = frozendict({1: 'B', 2: 'H', 4: 'L', 8: 'Q'}) _undefined = object() diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 293c3189589e36..ef6cc1001542ac 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -3679,7 +3679,7 @@ def f() -> annotation: ... * a class that inherits from any of the above - The standard library classes "dict" and "types.MappingProxyType" + The standard library classes "dict" and "frozendict" are mappings. [4] A string literal appearing as the first statement in the function @@ -13174,8 +13174,7 @@ class dict(iterable, /, **kwargs) See also: - "types.MappingProxyType" can be used to create a read-only view of a - "dict". + "frozendict" can be used to create a read-only view of a "dict". Dictionary view objects diff --git a/Lib/ssl.py b/Lib/ssl.py index 7ad7969a8217f8..65cd19e47288ae 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -150,7 +150,8 @@ source=_ssl) PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS -_PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()} +_PROTOCOL_NAMES = frozendict({ + value: name for name, value in _SSLMethod.__members__.items()}) _SSLv2_IF_EXISTS = getattr(_SSLMethod, 'PROTOCOL_SSLv2', None) diff --git a/Lib/stringprep.py b/Lib/stringprep.py index 44ecdb266ce8b9..cb8f56efbc57cb 100644 --- a/Lib/stringprep.py +++ b/Lib/stringprep.py @@ -21,7 +21,7 @@ def in_table_b1(code): return ord(code) in b1_set -b3_exceptions = { +b3_exceptions = frozendict({ 0xb5:'\u03bc', 0xdf:'ss', 0x130:'i\u0307', 0x149:'\u02bcn', 0x17f:'s', 0x1f0:'j\u030c', 0x345:'\u03b9', 0x37a:' \u03b9', 0x390:'\u03b9\u0308\u0301', 0x3b0:'\u03c5\u0308\u0301', 0x3c2:'\u03c3', 0x3d0:'\u03b2', @@ -184,7 +184,7 @@ def in_table_b1(code): 0x1d79c:'\u03bd', 0x1d79d:'\u03be', 0x1d79e:'\u03bf', 0x1d79f:'\u03c0', 0x1d7a0:'\u03c1', 0x1d7a1:'\u03b8', 0x1d7a2:'\u03c3', 0x1d7a3:'\u03c4', 0x1d7a4:'\u03c5', 0x1d7a5:'\u03c6', 0x1d7a6:'\u03c7', 0x1d7a7:'\u03c8', -0x1d7a8:'\u03c9', 0x1d7bb:'\u03c3', } +0x1d7a8:'\u03c9', 0x1d7bb:'\u03c3', }) def map_table_b3(code): r = b3_exceptions.get(ord(code)) diff --git a/Lib/symtable.py b/Lib/symtable.py index 4c832e68f94cbd..de2cc4a9cf0cc0 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -401,7 +401,7 @@ def get_namespace(self): _flags = [('USE', USE)] _flags.extend(kv for kv in globals().items() if kv[0].startswith('DEF_')) _scopes_names = ('FREE', 'LOCAL', 'GLOBAL_IMPLICIT', 'GLOBAL_EXPLICIT', 'CELL') -_scopes_value_to_name = {globals()[n]: n for n in _scopes_names} +_scopes_value_to_name = frozendict({globals()[n]: n for n in _scopes_names}) def main(args): diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 7db3a40c9b33cf..54790cb6d96a97 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -133,14 +133,14 @@ # Fields in a pax header that are numbers, all other fields # are treated as strings. -PAX_NUMBER_FIELDS = { +PAX_NUMBER_FIELDS = frozendict({ "atime": float, "ctime": float, "mtime": float, "uid": int, "gid": int, "size": int -} +}) #--------------------------------------------------------- # initialization @@ -860,11 +860,11 @@ def data_filter(member, dest_path): return member.replace(**new_attrs, deep=False) return member -_NAMED_FILTERS = { +_NAMED_FILTERS = frozendict({ "fully_trusted": fully_trusted_filter, "tar": tar_filter, "data": data_filter, -} +}) #------------------ # Exported Classes diff --git a/Lib/test/mapping_tests.py b/Lib/test/mapping_tests.py index 20306e1526d7b8..ccc1268bdff1df 100644 --- a/Lib/test/mapping_tests.py +++ b/Lib/test/mapping_tests.py @@ -4,7 +4,7 @@ from test import support -class BasicTestMappingProtocol(unittest.TestCase): +class BasicTestImmutableMappingProtocol(unittest.TestCase): # This base class can be used to check that an object conforms to the # mapping protocol @@ -22,10 +22,7 @@ def _empty_mapping(self): def _full_mapping(self, data): """Return a mapping object with the value contained in data dictionary""" - x = self._empty_mapping() - for key, value in data.items(): - x[key] = value - return x + return self.type2test(data) def __init__(self, *args, **kw): unittest.TestCase.__init__(self, *args, **kw) @@ -88,6 +85,72 @@ def check_iterandlist(iter, lst, ref): self.assertEqual(d.get(knownkey, knownvalue), knownvalue) self.assertNotIn(knownkey, d) + def test_constructor(self): + self.assertEqual(self._empty_mapping(), self._empty_mapping()) + + def test_bool(self): + self.assertTrue(not self._empty_mapping()) + self.assertTrue(self.reference) + self.assertTrue(bool(self._empty_mapping()) is False) + self.assertTrue(bool(self.reference) is True) + + def test_keys(self): + d = self._empty_mapping() + self.assertEqual(list(d.keys()), []) + d = self.reference + self.assertIn(list(self.inmapping.keys())[0], d.keys()) + self.assertNotIn(list(self.other.keys())[0], d.keys()) + self.assertRaises(TypeError, d.keys, None) + + def test_values(self): + d = self._empty_mapping() + self.assertEqual(list(d.values()), []) + + self.assertRaises(TypeError, d.values, None) + + def test_items(self): + d = self._empty_mapping() + self.assertEqual(list(d.items()), []) + + self.assertRaises(TypeError, d.items, None) + + def test_len(self): + d = self._empty_mapping() + self.assertEqual(len(d), 0) + + def test_getitem(self): + d = self.reference + self.assertEqual(d[list(self.inmapping.keys())[0]], + list(self.inmapping.values())[0]) + + self.assertRaises(TypeError, d.__getitem__) + + # no test_fromkeys or test_copy as both os.environ and selves don't support it + + def test_get(self): + d = self._empty_mapping() + self.assertTrue(d.get(list(self.other.keys())[0]) is None) + self.assertEqual(d.get(list(self.other.keys())[0], 3), 3) + d = self.reference + self.assertTrue(d.get(list(self.other.keys())[0]) is None) + self.assertEqual(d.get(list(self.other.keys())[0], 3), 3) + self.assertEqual(d.get(list(self.inmapping.keys())[0]), + list(self.inmapping.values())[0]) + self.assertEqual(d.get(list(self.inmapping.keys())[0], 3), + list(self.inmapping.values())[0]) + self.assertRaises(TypeError, d.get) + self.assertRaises(TypeError, d.get, None, None, None) + + +class BasicTestMappingProtocol(BasicTestImmutableMappingProtocol): + def _full_mapping(self, data): + """Return a mapping object with the value contained in data + dictionary""" + x = self._empty_mapping() + for key, value in data.items(): + x[key] = value + return x + def test_write(self): # Test for write operations on mapping p = self._empty_mapping() @@ -130,46 +193,6 @@ def test_write(self): p=self._empty_mapping() self.assertRaises(KeyError, p.popitem) - def test_constructor(self): - self.assertEqual(self._empty_mapping(), self._empty_mapping()) - - def test_bool(self): - self.assertTrue(not self._empty_mapping()) - self.assertTrue(self.reference) - self.assertTrue(bool(self._empty_mapping()) is False) - self.assertTrue(bool(self.reference) is True) - - def test_keys(self): - d = self._empty_mapping() - self.assertEqual(list(d.keys()), []) - d = self.reference - self.assertIn(list(self.inmapping.keys())[0], d.keys()) - self.assertNotIn(list(self.other.keys())[0], d.keys()) - self.assertRaises(TypeError, d.keys, None) - - def test_values(self): - d = self._empty_mapping() - self.assertEqual(list(d.values()), []) - - self.assertRaises(TypeError, d.values, None) - - def test_items(self): - d = self._empty_mapping() - self.assertEqual(list(d.items()), []) - - self.assertRaises(TypeError, d.items, None) - - def test_len(self): - d = self._empty_mapping() - self.assertEqual(len(d), 0) - - def test_getitem(self): - d = self.reference - self.assertEqual(d[list(self.inmapping.keys())[0]], - list(self.inmapping.values())[0]) - - self.assertRaises(TypeError, d.__getitem__) - def test_update(self): # mapping argument d = self._empty_mapping() @@ -265,22 +288,6 @@ def __next__(self): self.assertRaises(ValueError, d.update, [(1, 2, 3)]) - # no test_fromkeys or test_copy as both os.environ and selves don't support it - - def test_get(self): - d = self._empty_mapping() - self.assertTrue(d.get(list(self.other.keys())[0]) is None) - self.assertEqual(d.get(list(self.other.keys())[0], 3), 3) - d = self.reference - self.assertTrue(d.get(list(self.other.keys())[0]) is None) - self.assertEqual(d.get(list(self.other.keys())[0], 3), 3) - self.assertEqual(d.get(list(self.inmapping.keys())[0]), - list(self.inmapping.values())[0]) - self.assertEqual(d.get(list(self.inmapping.keys())[0], 3), - list(self.inmapping.values())[0]) - self.assertRaises(TypeError, d.get) - self.assertRaises(TypeError, d.get, None, None, None) - def test_setdefault(self): d = self._empty_mapping() self.assertRaises(TypeError, d.setdefault) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 2e6c2bbdf19409..af3a390150a549 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1664,6 +1664,9 @@ class Dict(dict): class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol): type2test = Dict +class FrozenDictMappingTests(mapping_tests.BasicTestImmutableMappingProtocol): + type2test = frozendict + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 0fa74407e3c436..0429b1fd3c071a 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -742,7 +742,7 @@ def non_Python_modules(): r""" >>> import builtins >>> tests = doctest.DocTestFinder().find(builtins) - >>> 750 < len(tests) < 800 # approximate number of objects with docstrings + >>> 750 < len(tests) < 850 # approximate number of objects with docstrings True >>> real_tests = [t for t in tests if len(t.examples) > 0] >>> len(real_tests) # objects that actually have doctests diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index dd3b7d9c5b4b5b..04e38ab323aeca 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6103,7 +6103,8 @@ def _test_builtin_methods_have_signatures(self, cls, no_signature, unsupported_s self.assertRaises(ValueError, inspect.signature, getattr(cls, name)) def test_builtins_have_signatures(self): - no_signature = {'type', 'super', 'bytearray', 'bytes', 'dict', 'int', 'str'} + no_signature = {'type', 'super', 'bytearray', 'bytes', + 'dict', 'frozendict', 'int', 'str'} # These need PEP 457 groups needs_groups = {"range", "slice", "dir", "getattr", "next", "iter", "vars"} diff --git a/Lib/test/test_pickletools.py b/Lib/test/test_pickletools.py index cf990874621eae..f4a6e2e81b4c70 100644 --- a/Lib/test/test_pickletools.py +++ b/Lib/test/test_pickletools.py @@ -506,7 +506,7 @@ def test__all__(self): 'StackObject', 'pyint', 'pylong', 'pyinteger_or_bool', 'pybool', 'pyfloat', 'pybytes_or_str', 'pystring', 'pybytes', 'pybytearray', - 'pyunicode', 'pynone', 'pytuple', 'pylist', 'pydict', + 'pyunicode', 'pynone', 'pytuple', 'pylist', 'pydict', 'pyfrozendict', 'pyset', 'pyfrozenset', 'pybuffer', 'anyobject', 'markobject', 'stackslice', 'OpcodeInfo', 'opcodes', 'code2op', diff --git a/Lib/token.py b/Lib/token.py index f61723cc09da02..f76de6d24e84d8 100644 --- a/Lib/token.py +++ b/Lib/token.py @@ -78,12 +78,13 @@ # Special definitions for cooperation with parser NT_OFFSET = 256 -tok_name = {value: name - for name, value in globals().items() - if isinstance(value, int) and not name.startswith('_')} +tok_name = frozendict({ + value: name + for name, value in globals().items() + if isinstance(value, int) and not name.startswith('_')}) __all__.extend(tok_name.values()) -EXACT_TOKEN_TYPES = { +EXACT_TOKEN_TYPES = frozendict({ '!': EXCLAMATION, '!=': NOTEQUAL, '%': PERCENT, @@ -132,7 +133,7 @@ '|=': VBAREQUAL, '}': RBRACE, '~': TILDE, -} +}) def ISTERMINAL(x: int) -> bool: return x < NT_OFFSET diff --git a/Lib/tomllib/_parser.py b/Lib/tomllib/_parser.py index 3ee47aa9e0afba..35ce41be745dd2 100644 --- a/Lib/tomllib/_parser.py +++ b/Lib/tomllib/_parser.py @@ -4,8 +4,6 @@ from __future__ import annotations -from types import MappingProxyType - from ._re import ( RE_DATETIME, RE_LOCALTIME, @@ -42,7 +40,7 @@ KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'") HEXDIGIT_CHARS = frozenset("abcdef" "ABCDEF" "0123456789") -BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType( +BASIC_STR_ESCAPE_REPLACEMENTS = frozendict( { "\\b": "\u0008", # backspace "\\t": "\u0009", # tab diff --git a/Lib/typing.py b/Lib/typing.py index eb0519986a8952..189d320e382e83 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1915,7 +1915,7 @@ def _allow_reckless_class_checks(depth=2): return _caller(depth) in {'abc', '_py_abc', 'functools', None} -_PROTO_ALLOWLIST = { +_PROTO_ALLOWLIST = frozendict({ 'collections.abc': [ 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', 'Hashable', 'Sized', 'Container', 'Collection', @@ -1924,7 +1924,7 @@ def _allow_reckless_class_checks(depth=2): 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], 'io': ['Reader', 'Writer'], 'os': ['PathLike'], -} +}) @functools.cache diff --git a/Modules/_json.c b/Modules/_json.c index 14714d4b346546..34626dc82e7236 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1597,7 +1597,7 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer, _Py_LeaveRecursiveCall(); return rv; } - else if (PyDict_Check(obj)) { + else if (PyDict_Check(obj) || PyFrozenDict_Check(obj)) { if (_Py_EnterRecursiveCall(" while encoding a JSON object")) return -1; rv = encoder_listencode_dict(s, writer, obj, indent_level, indent_cache); diff --git a/Modules/_pickle.c b/Modules/_pickle.c index bfb2830f3893d6..13cace30863713 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -136,7 +136,8 @@ enum opcode { /* Protocol 5 */ BYTEARRAY8 = '\x96', NEXT_BUFFER = '\x97', - READONLY_BUFFER = '\x98' + READONLY_BUFFER = '\x98', + FROZENDICT = '\x99', }; enum { @@ -324,7 +325,7 @@ _Pickle_InitState(PickleState *st) PyObject_GetAttrString(compat_pickle, "NAME_MAPPING"); if (!st->name_mapping_2to3) goto error; - if (!PyDict_CheckExact(st->name_mapping_2to3)) { + if (!_PyAnyDict_CheckExact(st->name_mapping_2to3)) { PyErr_Format(PyExc_RuntimeError, "_compat_pickle.NAME_MAPPING should be a dict, not %.200s", Py_TYPE(st->name_mapping_2to3)->tp_name); @@ -334,7 +335,7 @@ _Pickle_InitState(PickleState *st) PyObject_GetAttrString(compat_pickle, "IMPORT_MAPPING"); if (!st->import_mapping_2to3) goto error; - if (!PyDict_CheckExact(st->import_mapping_2to3)) { + if (!_PyAnyDict_CheckExact(st->import_mapping_2to3)) { PyErr_Format(PyExc_RuntimeError, "_compat_pickle.IMPORT_MAPPING should be a dict, " "not %.200s", Py_TYPE(st->import_mapping_2to3)->tp_name); @@ -592,6 +593,36 @@ Pdata_poplist(Pdata *self, Py_ssize_t start) return list; } +static PyObject * +Pdata_poplist2(PickleState *state, Pdata *self, Py_ssize_t start) +{ + if (start < self->fence) { + Pdata_stack_underflow(state, self); + return NULL; + } + + Py_ssize_t len = (Py_SIZE(self) - start) >> 1; + + PyObject *list = PyList_New(len); + if (list == NULL) { + return NULL; + } + + for (Py_ssize_t i = start, j = 0; j < len; i+=2, j++) { + PyObject *subtuple = PyTuple_New(2); + if (subtuple == NULL) { + return NULL; + } + + PyTuple_SET_ITEM(subtuple, 0, self->data[i]); + PyTuple_SET_ITEM(subtuple, 1, self->data[i+1]); + PyList_SET_ITEM(list, j, subtuple); + } + + Py_SET_SIZE(self, start); + return list; +} + typedef struct { PyObject *me_key; Py_ssize_t me_value; @@ -3445,6 +3476,64 @@ save_dict(PickleState *state, PicklerObject *self, PyObject *obj) return status; } +static int +save_frozendict(PickleState *state, PicklerObject *self, PyObject *obj) +{ + const char mark_op = MARK; + const char frozendict_op = FROZENDICT; + + if (self->fast && !fast_save_enter(self, obj)) { + return -1; + } + + if (self->proto < 4) { + PyObject *items = PyDict_Items(obj); + if (items == NULL) { + return -1; + } + + PyObject *reduce_value; + reduce_value = Py_BuildValue("(O(O))", (PyObject*)&PyFrozenDict_Type, + items); + Py_DECREF(items); + if (reduce_value == NULL) { + return -1; + } + + /* save_reduce() will memoize the object automatically. */ + int status = save_reduce(state, self, reduce_value, obj); + Py_DECREF(reduce_value); + return status; + } + + if (_Pickler_Write(self, &mark_op, 1) < 0) { + return -1; + } + + PyObject *key = NULL, *value = NULL; + Py_ssize_t pos = 0; + while (PyDict_Next(obj, &pos, &key, &value)) { + int res = save(state, self, key, 0); + if (res < 0) { + return -1; + } + + res = save(state, self, value, 0); + if (res < 0) { + return -1; + } + } + + if (_Pickler_Write(self, &frozendict_op, 1) < 0) { + return -1; + } + + if (memo_put(state, self, obj) < 0) { + return -1; + } + return 0; +} + static int save_set(PickleState *state, PicklerObject *self, PyObject *obj) { @@ -4411,6 +4500,10 @@ save(PickleState *st, PicklerObject *self, PyObject *obj, int pers_save) status = save_dict(st, self, obj); goto done; } + else if (type == &PyFrozenDict_Type) { + status = save_frozendict(st, self, obj); + goto done; + } else if (type == &PySet_Type) { status = save_set(st, self, obj); goto done; @@ -5831,6 +5924,30 @@ load_dict(PickleState *st, UnpicklerObject *self) return 0; } + +static int +load_frozendict(PickleState *st, UnpicklerObject *self) +{ + Py_ssize_t i = marker(st, self); + if (i < 0) { + return -1; + } + + PyObject *items = Pdata_poplist2(st, self->stack, i); + if (items == NULL) { + return -1; + } + + PyObject *frozendict = PyFrozenDict_New(items); + Py_DECREF(items); + if (frozendict == NULL) { + return -1; + } + + PDATA_PUSH(self->stack, frozendict, -1); + return 0; +} + static int load_frozenset(PickleState *state, UnpicklerObject *self) { @@ -6946,6 +7063,7 @@ load(PickleState *st, UnpicklerObject *self) OP(LIST, load_list) OP(EMPTY_DICT, load_empty_dict) OP(DICT, load_dict) + OP(FROZENDICT, load_frozendict) OP(EMPTY_SET, load_empty_set) OP(ADDITEMS, load_additems) OP(FROZENSET, load_frozenset) diff --git a/Modules/errnomodule.c b/Modules/errnomodule.c index 9557d68e759497..509cb498d026b0 100644 --- a/Modules/errnomodule.c +++ b/Modules/errnomodule.c @@ -1,11 +1,5 @@ /* Errno module */ -// Need limited C API version 3.13 for Py_mod_gil -#include "pyconfig.h" // Py_GIL_DISABLED -#ifndef Py_GIL_DISABLED -# define Py_LIMITED_API 0x030d0000 -#endif - #include "Python.h" #include // EPIPE @@ -96,10 +90,6 @@ errno_exec(PyObject *module) if (error_dict == NULL) { return -1; } - if (PyDict_SetItemString(module_dict, "errorcode", error_dict) < 0) { - Py_DECREF(error_dict); - return -1; - } /* Macro so I don't have to edit each and every line below... */ #define add_errcode(name, code, comment) \ @@ -947,6 +937,18 @@ errno_exec(PyObject *module) add_errcode("ENOTCAPABLE", ENOTCAPABLE, "Capabilities insufficient"); #endif + PyObject *frozendict = PyFrozenDict_New(error_dict); + if (frozendict == NULL) { + Py_DECREF(error_dict); + return -1; + } + if (PyDict_SetItemString(module_dict, "errorcode", frozendict) < 0) { + Py_DECREF(error_dict); + Py_DECREF(frozendict); + return -1; + } + Py_DECREF(frozendict); + Py_DECREF(error_dict); return 0; } diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 65eed151c2829d..f91f7b3263efb9 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -135,6 +135,10 @@ As a consequence of this, split keys have a maximum size of 16. #include "stringlib/eq.h" // unicode_eq() #include +// Forward declarations +static PyObject* frozendict_new(PyTypeObject *type, PyObject *args, + PyObject *kwds); + /*[clinic input] class dict "PyDictObject *" "&PyDict_Type" @@ -655,7 +659,7 @@ _PyDict_CheckConsistency(PyObject *op, int check_content) do { if (!(expr)) { _PyObject_ASSERT_FAILED_MSG(op, Py_STRINGIFY(expr)); } } while (0) assert(op != NULL); - CHECK(PyDict_Check(op)); + CHECK(_PyAnyDict_Check(op)); PyDictObject *mp = (PyDictObject *)op; PyDictKeysObject *keys = mp->ma_keys; @@ -2277,7 +2281,7 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset, static PyObject * dict_getitem(PyObject *op, PyObject *key, const char *warnmsg) { - if (!PyDict_Check(op)) { + if (!_PyAnyDict_Check(op)) { return NULL; } PyDictObject *mp = (PyDictObject *)op; @@ -2441,7 +2445,7 @@ _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, Py int PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result) { - if (!PyDict_Check(op)) { + if (!_PyAnyDict_Check(op)) { PyErr_BadInternalCall(); *result = NULL; return -1; @@ -2497,7 +2501,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key) PyDictObject*mp = (PyDictObject *)op; PyObject *value; - if (!PyDict_Check(op)) { + if (!_PyAnyDict_Check(op)) { PyErr_BadInternalCall(); return NULL; } @@ -2658,7 +2662,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) assert(key); assert(value); - assert(PyDict_Check(mp)); + assert(_PyAnyDict_Check(mp)); Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { dict_unhashable_type(key); @@ -2705,6 +2709,16 @@ PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value) Py_NewRef(key), Py_NewRef(value)); } +static int +_PyAnyDict_SetItem(PyObject *op, PyObject *key, PyObject *value) +{ + assert(_PyAnyDict_Check(op)); + assert(key); + assert(value); + return _PyDict_SetItem_Take2((PyDictObject *)op, + Py_NewRef(key), Py_NewRef(value)); +} + static int setitem_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) { @@ -2992,7 +3006,7 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject *key, *value; Py_hash_t hash; - if (!PyDict_Check(op)) + if (!_PyAnyDict_Check(op)) return 0; mp = (PyDictObject *)op; @@ -3291,7 +3305,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return NULL; } - if (PyDict_CheckExact(d)) { + if (_PyAnyDict_CheckExact(d)) { Py_BEGIN_CRITICAL_SECTION(d); while ((key = PyIter_Next(it)) != NULL) { status = setitem_lock_held((PyDictObject *)d, key, value); @@ -3764,7 +3778,7 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, int override) PyObject *fast; /* item as a 2-tuple or 2-list */ assert(d != NULL); - assert(PyDict_Check(d)); + assert(_PyAnyDict_Check(d)); assert(seq2 != NULL); it = PyObject_GetIter(seq2); @@ -3962,7 +3976,7 @@ dict_merge(PyInterpreterState *interp, PyObject *a, PyObject *b, int override) * things quite efficiently. For the latter, we only require that * PyMapping_Keys() and PyObject_GetItem() be supported. */ - if (a == NULL || !PyDict_Check(a) || b == NULL) { + if (a == NULL || !_PyAnyDict_Check(a) || b == NULL) { PyErr_BadInternalCall(); return -1; } @@ -4112,13 +4126,19 @@ copy_lock_held(PyObject *o) PyObject *copy; PyDictObject *mp; PyInterpreterState *interp = _PyInterpreterState_GET(); + int frozendict = PyFrozenDict_Check(o); ASSERT_DICT_LOCKED(o); mp = (PyDictObject *)o; if (mp->ma_used == 0) { /* The dict is empty; just return a new dict. */ - return PyDict_New(); + if (frozendict) { + return PyFrozenDict_New(NULL); + } + else { + return PyDict_New(); + } } if (_PyDict_HasSplitTable(mp)) { @@ -4127,7 +4147,13 @@ copy_lock_held(PyObject *o) if (newvalues == NULL) { return PyErr_NoMemory(); } - split_copy = PyObject_GC_New(PyDictObject, &PyDict_Type); + if (frozendict) { + split_copy = (PyDictObject *)PyObject_GC_New(PyFrozenDictObject, + &PyFrozenDict_Type); + } + else { + split_copy = PyObject_GC_New(PyDictObject, &PyDict_Type); + } if (split_copy == NULL) { free_values(newvalues, false); return NULL; @@ -4140,13 +4166,18 @@ copy_lock_held(PyObject *o) split_copy->ma_used = mp->ma_used; split_copy->_ma_watcher_tag = 0; dictkeys_incref(mp->ma_keys); + if (frozendict) { + PyFrozenDictObject *frozen = (PyFrozenDictObject *)split_copy; + frozen->ma_hash = -1; + } _PyObject_GC_TRACK(split_copy); return (PyObject *)split_copy; } if (Py_TYPE(mp)->tp_iter == dict_iter && mp->ma_values == NULL && - (mp->ma_used >= (mp->ma_keys->dk_nentries * 2) / 3)) + (mp->ma_used >= (mp->ma_keys->dk_nentries * 2) / 3) && + !frozendict) { /* Use fast-copy if: @@ -4178,7 +4209,12 @@ copy_lock_held(PyObject *o) return (PyObject *)new; } - copy = PyDict_New(); + if (frozendict) { + copy = PyFrozenDict_New(NULL); + } + else { + copy = PyDict_New(); + } if (copy == NULL) return NULL; if (dict_merge(interp, copy, o, 1) == 0) @@ -4190,7 +4226,7 @@ copy_lock_held(PyObject *o) PyObject * PyDict_Copy(PyObject *o) { - if (o == NULL || !PyDict_Check(o)) { + if (o == NULL || !_PyAnyDict_Check(o)) { PyErr_BadInternalCall(); return NULL; } @@ -4297,7 +4333,7 @@ dict_richcompare(PyObject *v, PyObject *w, int op) int cmp; PyObject *res; - if (!PyDict_Check(v) || !PyDict_Check(w)) { + if (!_PyAnyDict_Check(v) || !_PyAnyDict_Check(w)) { res = Py_NotImplemented; } else if (op == Py_EQ || op == Py_NE) { @@ -4741,7 +4777,7 @@ dict___sizeof___impl(PyDictObject *self) static PyObject * dict_or(PyObject *self, PyObject *other) { - if (!PyDict_Check(self) || !PyDict_Check(other)) { + if (!_PyAnyDict_Check(self) || !_PyAnyDict_Check(other)) { Py_RETURN_NOTIMPLEMENTED; } PyObject *new = PyDict_Copy(self); @@ -4917,7 +4953,15 @@ dict_vectorcall(PyObject *type, PyObject * const*args, return NULL; } - PyObject *self = dict_new(_PyType_CAST(type), NULL, NULL); + PyObject *self; + if (Py_Is((PyTypeObject*)type, &PyFrozenDict_Type) + || PyType_IsSubtype((PyTypeObject*)type, &PyFrozenDict_Type)) + { + self = frozendict_new(_PyType_CAST(type), NULL, NULL); + } + else { + self = dict_new(_PyType_CAST(type), NULL, NULL); + } if (self == NULL) { return NULL; } @@ -4930,7 +4974,8 @@ dict_vectorcall(PyObject *type, PyObject * const*args, } if (kwnames != NULL) { for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwnames); i++) { - if (PyDict_SetItem(self, PyTuple_GET_ITEM(kwnames, i), args[i]) < 0) { + PyObject *key = PyTuple_GET_ITEM(kwnames, i); // borrowed + if (_PyAnyDict_SetItem(self, key, args[i]) < 0) { Py_DECREF(self); return NULL; } @@ -5003,6 +5048,7 @@ PyTypeObject PyDict_Type = { .tp_version_tag = _Py_TYPE_VERSION_DICT, }; + /* For backward compatibility with old dictionary interface */ PyObject * @@ -5197,7 +5243,7 @@ dictiter_iternextkey_lock_held(PyDictObject *d, PyObject *self) Py_ssize_t i; PyDictKeysObject *k; - assert (PyDict_Check(d)); + assert (_PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5321,7 +5367,7 @@ dictiter_iternextvalue_lock_held(PyDictObject *d, PyObject *self) PyObject *value; Py_ssize_t i; - assert (PyDict_Check(d)); + assert (_PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5443,7 +5489,7 @@ dictiter_iternextitem_lock_held(PyDictObject *d, PyObject *self, PyObject *key, *value; Py_ssize_t i; - assert (PyDict_Check(d)); + assert (_PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5958,7 +6004,7 @@ _PyDictView_New(PyObject *dict, PyTypeObject *type) PyErr_BadInternalCall(); return NULL; } - if (!PyDict_Check(dict)) { + if (!_PyAnyDict_Check(dict)) { /* XXX Get rid of this restriction later */ PyErr_Format(PyExc_TypeError, "%s() requires a dict argument, not '%s'", @@ -6878,6 +6924,11 @@ _PyObject_MaterializeManagedDict(PyObject *obj) int _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value) { + if (!PyDict_Check(dict)) { + PyErr_BadInternalCall(); + return -1; + } + if (value == NULL) { Py_hash_t hash = _PyObject_HashFast(name); if (hash == -1) { @@ -7775,3 +7826,166 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj) return 0; } #endif + +// --- frozendict implementation --------------------------------------------- + +static PyNumberMethods frozendict_as_number = { + .nb_or = dict_or, +}; + +static PyMappingMethods frozendict_as_mapping = { + dict_length, /*mp_length*/ + dict_subscript, /*mp_subscript*/ +}; + +static PyMethodDef frozendict_methods[] = { + DICT___CONTAINS___METHODDEF + {"__getitem__", dict_subscript, METH_O | METH_COEXIST, + getitem__doc__}, + DICT___SIZEOF___METHODDEF + DICT_GET_METHODDEF + DICT_KEYS_METHODDEF + DICT_ITEMS_METHODDEF + DICT_VALUES_METHODDEF + DICT_FROMKEYS_METHODDEF + DICT_COPY_METHODDEF + DICT___REVERSED___METHODDEF + {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, + {NULL, NULL} /* sentinel */ +}; + + +static PyObject * +frozendict_repr(PyObject *self) +{ + PyObject *repr = dict_repr(self); + if (repr == NULL) { + return NULL; + } + assert(PyUnicode_Check(repr)); + + PyObject *res = PyUnicode_FromFormat("%s(%U)", + Py_TYPE(self)->tp_name, + repr); + Py_DECREF(repr); + return res; +} + +static Py_hash_t +frozendict_hash(PyObject *op) +{ + PyFrozenDictObject *self = _PyFrozenDictObject_CAST(op); + Py_hash_t hash = self->ma_hash; + if (hash != -1) { + return hash; + } + + PyObject *items = _PyDictView_New(op, &PyDictItems_Type); + if (items == NULL) { + return -1; + } + PyObject *frozenset = PyFrozenSet_New(items); + Py_DECREF(items); + if (frozenset == NULL) { + return -1; + } + + hash = PyObject_Hash(frozenset); + Py_DECREF(frozenset); + if (hash == -1) { + return -1; + } + + self->ma_hash = hash; + return hash; +} + + +static PyObject * +frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyObject *d = dict_new(type, args, kwds); + if (d == NULL) { + return NULL; + } + PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d); + self->ma_hash = -1; + + if (args != NULL) { + if (dict_update_common(d, args, kwds, "frozendict") < 0) { + Py_DECREF(d); + return NULL; + } + } + else { + assert(kwds == NULL); + } + + return d; +} + + +PyObject* +PyFrozenDict_New(PyObject *iterable) +{ + if (iterable != NULL) { + PyObject *args = PyTuple_Pack(1, iterable); + if (args == NULL) { + return NULL; + } + PyObject *frozendict = frozendict_new(&PyFrozenDict_Type, args, NULL); + Py_DECREF(args); + return frozendict; + } + else { + PyObject *args = Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_TUPLE); + return frozendict_new(&PyFrozenDict_Type, args, NULL); + } +} + + +PyTypeObject PyFrozenDict_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "frozendict", + sizeof(PyFrozenDictObject), + 0, + dict_dealloc, /* tp_dealloc */ + 0, /* tp_vectorcall_offset */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + frozendict_repr, /* tp_repr */ + &frozendict_as_number, /* tp_as_number */ + &dict_as_sequence, /* tp_as_sequence */ + &frozendict_as_mapping, /* tp_as_mapping */ + frozendict_hash, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_BASETYPE | + _Py_TPFLAGS_MATCH_SELF | Py_TPFLAGS_MAPPING, /* tp_flags */ + dictionary_doc, /* tp_doc */ + dict_traverse, /* tp_traverse */ + dict_tp_clear, /* tp_clear */ + dict_richcompare, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + dict_iter, /* tp_iter */ + 0, /* tp_iternext */ + frozendict_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + dict_init, /* tp_init */ + _PyType_AllocNoTrack, /* tp_alloc */ + frozendict_new, /* tp_new */ + PyObject_GC_Del, /* tp_free */ + .tp_vectorcall = dict_vectorcall, + .tp_version_tag = _Py_TYPE_VERSION_DICT, +}; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 9dee03bdb5ee55..1346e7598247cb 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1473,6 +1473,7 @@ module_dir(PyObject *self, PyObject *args) return result; } + static PyMethodDef module_methods[] = { {"__dir__", module_dir, METH_NOARGS, PyDoc_STR("__dir__() -> list\nspecialized dir() implementation")}, diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 58228d6248522e..c47dcf54ec3eeb 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3998,7 +3998,7 @@ subtype_dict(PyObject *obj, void *context) int _PyObject_SetDict(PyObject *obj, PyObject *value) { - if (value != NULL && !PyDict_Check(value)) { + if (value != NULL && !_PyAnyDict_Check(value)) { PyErr_Format(PyExc_TypeError, "__dict__ must be set to a dictionary, " "not a '%.200s'", Py_TYPE(value)->tp_name); @@ -5999,7 +5999,7 @@ find_name_in_mro(PyTypeObject *type, PyObject *name, int *error) for (Py_ssize_t i = 0; i < n; i++) { PyObject *base = PyTuple_GET_ITEM(mro, i); PyObject *dict = lookup_tp_dict(_PyType_CAST(base)); - assert(dict && PyDict_Check(dict)); + assert(dict && _PyAnyDict_Check(dict)); if (_PyDict_GetItemRef_KnownHash((PyDictObject *)dict, name, hash, &res) < 0) { *error = -1; goto done; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index c2d780ac9b9270..c45e61846794dd 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -3479,6 +3479,7 @@ _PyBuiltin_Init(PyInterpreterState *interp) SETBUILTIN("enumerate", &PyEnum_Type); SETBUILTIN("filter", &PyFilter_Type); SETBUILTIN("float", &PyFloat_Type); + SETBUILTIN("frozendict", &PyFrozenDict_Type); SETBUILTIN("frozenset", &PyFrozenSet_Type); SETBUILTIN("property", &PyProperty_Type); SETBUILTIN("int", &PyLong_Type); diff --git a/Python/marshal.c b/Python/marshal.c index 8b56de6575559c..e3fef318fa142a 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -67,6 +67,7 @@ module marshal #define TYPE_TUPLE '(' // See also TYPE_SMALL_TUPLE. #define TYPE_LIST '[' #define TYPE_DICT '{' +#define TYPE_FROZENDICT '}' #define TYPE_CODE 'c' #define TYPE_UNICODE 'u' #define TYPE_UNKNOWN '?' @@ -571,10 +572,15 @@ w_complex_object(PyObject *v, char flag, WFILE *p) w_object(PyList_GET_ITEM(v, i), p); } } - else if (PyDict_CheckExact(v)) { + else if (_PyAnyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; - W_TYPE(TYPE_DICT, p); + if (PyFrozenDict_CheckExact(v)) { + W_TYPE(TYPE_FROZENDICT, p); + } + else { + W_TYPE(TYPE_DICT, p); + } /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { @@ -1416,6 +1422,7 @@ r_object(RFILE *p) break; case TYPE_DICT: + case TYPE_FROZENDICT: v = PyDict_New(); R_REF(v); if (v == NULL) @@ -1439,7 +1446,16 @@ r_object(RFILE *p) Py_DECREF(val); } if (PyErr_Occurred()) { - Py_SETREF(v, NULL); + Py_CLEAR(v); + } + if (type == TYPE_FROZENDICT && v != NULL) { + PyObject *frozendict = PyFrozenDict_New(v); + if (frozendict != NULL) { + Py_SETREF(v, frozendict); + } + else { + Py_CLEAR(v); + } } retval = v; break; diff --git a/Tools/cases_generator/py_metadata_generator.py b/Tools/cases_generator/py_metadata_generator.py index 3ec06faf338488..73cdc9f3b31350 100644 --- a/Tools/cases_generator/py_metadata_generator.py +++ b/Tools/cases_generator/py_metadata_generator.py @@ -30,17 +30,17 @@ def get_specialized(analysis: Analysis) -> set[str]: def generate_specializations(analysis: Analysis, out: CWriter) -> None: - out.emit("_specializations = {\n") + out.emit("_specializations = frozendict(\n") for family in analysis.families.values(): - out.emit(f'"{family.name}": [\n') + out.emit(f'{family.name}= [\n') for member in family.members: out.emit(f' "{member.name}",\n') out.emit("],\n") - out.emit("}\n\n") + out.emit(")\n\n") def generate_specialized_opmap(analysis: Analysis, out: CWriter) -> None: - out.emit("_specialized_opmap = {\n") + out.emit("_specialized_opmap = frozendict(\n") names = [] for family in analysis.families.values(): for member in family.members: @@ -48,17 +48,17 @@ def generate_specialized_opmap(analysis: Analysis, out: CWriter) -> None: continue names.append(member.name) for name in sorted(names): - out.emit(f"'{name}': {analysis.opmap[name]},\n") - out.emit("}\n\n") + out.emit(f"{name}= {analysis.opmap[name]},\n") + out.emit(")\n\n") def generate_opmap(analysis: Analysis, out: CWriter) -> None: specialized = get_specialized(analysis) - out.emit("opmap = {\n") + out.emit("opmap = frozendict(\n") for inst, op in analysis.opmap.items(): if inst not in specialized: - out.emit(f"'{inst}': {analysis.opmap[inst]},\n") - out.emit("}\n\n") + out.emit(f"{inst}= {analysis.opmap[inst]},\n") + out.emit(")\n\n") def generate_py_metadata( From 49aca48431fb881811a9d2220f616cf249d30e59 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 14:21:30 +0100 Subject: [PATCH 02/22] Update Tools/build/generate_token.py for token.py --- Tools/build/generate_token.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Tools/build/generate_token.py b/Tools/build/generate_token.py index 9ee5ec86e75d47..1ff7a7c95f6af4 100755 --- a/Tools/build/generate_token.py +++ b/Tools/build/generate_token.py @@ -269,14 +269,15 @@ def make_rst(infile, outfile='Doc/library/token-list.inc', # Special definitions for cooperation with parser NT_OFFSET = %d -tok_name = {value: name - for name, value in globals().items() - if isinstance(value, int) and not name.startswith('_')} +tok_name = frozendict({ + value: name + for name, value in globals().items() + if isinstance(value, int) and not name.startswith('_')}) __all__.extend(tok_name.values()) -EXACT_TOKEN_TYPES = { +EXACT_TOKEN_TYPES = frozendict({ %s -} +}) def ISTERMINAL(x: int) -> bool: return x < NT_OFFSET From 3299299ffaa0c8fbe6c2a5298af59524f20a5900 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 14:22:50 +0100 Subject: [PATCH 03/22] Try to fix build on Ubuntu and macOS --- Objects/dictobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index f91f7b3263efb9..292d8e2f6e0824 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -5595,7 +5595,7 @@ dictiter_iternext_threadsafe(PyDictObject *d, PyObject *self, Py_ssize_t i; PyDictKeysObject *k; - assert (PyDict_Check(d)); + assert (_PyAnyDict_Check(d)); if (di->di_used != _Py_atomic_load_ssize_relaxed(&d->ma_used)) { PyErr_SetString(PyExc_RuntimeError, From 18ce520e8bbbdc05249adc8c063c62f246b83067 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 14:39:22 +0100 Subject: [PATCH 04/22] Fix make check-c-globals --- Tools/c-analyzer/cpython/ignored.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 4621ad250f4633..8811e0e458186f 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -760,6 +760,7 @@ Modules/clinic/md5module.c.h _md5_md5 _keywords - Modules/clinic/grpmodule.c.h grp_getgrgid _keywords - Modules/clinic/grpmodule.c.h grp_getgrnam _keywords - Objects/object.c - constants static PyObject*[] +Objects/dictobject.c - PyFrozenDict_Type - ## False positives From 1a72eec987345c43e59a31930cf3f8626e520d6c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 15:37:59 +0100 Subject: [PATCH 05/22] Add PyFrozenDict_Type to static_types --- Include/internal/pycore_typeobject.h | 1 + Objects/dictobject.c | 2 +- Objects/object.c | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h index 3661f171e2b013..a9039cfcd45473 100644 --- a/Include/internal/pycore_typeobject.h +++ b/Include/internal/pycore_typeobject.h @@ -25,6 +25,7 @@ extern "C" { #define _Py_TYPE_VERSION_BYTEARRAY 9 #define _Py_TYPE_VERSION_BYTES 10 #define _Py_TYPE_VERSION_COMPLEX 11 +#define _Py_TYPE_VERSION_FROZENDICT 12 #define _Py_TYPE_VERSION_NEXT 16 diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 292d8e2f6e0824..13f810d1af12c6 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7987,5 +7987,5 @@ PyTypeObject PyFrozenDict_Type = { frozendict_new, /* tp_new */ PyObject_GC_Del, /* tp_free */ .tp_vectorcall = dict_vectorcall, - .tp_version_tag = _Py_TYPE_VERSION_DICT, + .tp_version_tag = _Py_TYPE_VERSION_FROZENDICT, }; diff --git a/Objects/object.c b/Objects/object.c index 0540112d7d2acf..98eb0ab5a20f7a 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2476,8 +2476,9 @@ static PyTypeObject* static_types[] = { &PyEnum_Type, &PyFilter_Type, &PyFloat_Type, - &PyFrame_Type, &PyFrameLocalsProxy_Type, + &PyFrame_Type, + &PyFrozenDict_Type, &PyFrozenSet_Type, &PyFunction_Type, &PyGen_Type, From 9d6ca58b0de297a9aaa3fdef849717936a201311 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 16:28:07 +0100 Subject: [PATCH 06/22] Update Doc/library/stdtypes.rst Co-authored-by: Nice Zombies --- Doc/library/stdtypes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index d74345416f3644..c6a9893f5b57a8 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4926,8 +4926,8 @@ Mapping Types --- :class:`dict`, :class:`frozendict` pair: built-in function; len A :term:`mapping` object maps :term:`hashable` values to arbitrary objects. -Mappings are mutable objects. There is currently two standard mapping -types, the :dfn:`dictionary` and :class:`frozendict`. +There are currently two standard mapping types, the :dfn:`dictionary` and +:class:`frozendict`. (For other containers see the built-in :class:`list`, :class:`set`, and :class:`tuple` classes, and the :mod:`collections` module.) From 439c98c307e0ce3d62b0eabf0afe065a0bbe4c07 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 21:06:43 +0100 Subject: [PATCH 07/22] Replace PyDict_Check() with _PyAnyDict_Check() --- Modules/_collectionsmodule.c | 2 +- Objects/abstract.c | 10 +++++----- Objects/descrobject.c | 2 +- Objects/dictobject.c | 32 ++++++++++++++++---------------- Objects/listobject.c | 2 +- Objects/object.c | 2 +- Objects/odictobject.c | 2 +- Objects/setobject.c | 14 +++++++------- Objects/unicodeobject.c | 2 +- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 3ba48d5d9d3c64..298ae3d6a58320 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2403,7 +2403,7 @@ defdict_or(PyObject* left, PyObject* right) self = right; other = left; } - if (!PyDict_Check(other)) { + if (!_PyAnyDict_Check(other)) { Py_RETURN_NOTIMPLEMENTED; } // Like copy(), this calls the object's class. diff --git a/Objects/abstract.c b/Objects/abstract.c index 8adad8407d04d4..ca22d5298d3d3f 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -208,7 +208,7 @@ PyObject_GetItem(PyObject *o, PyObject *key) int PyMapping_GetOptionalItem(PyObject *obj, PyObject *key, PyObject **result) { - if (PyDict_CheckExact(obj)) { + if (_PyAnyDict_CheckExact(obj)) { return PyDict_GetItemRef(obj, key, result); } @@ -1672,7 +1672,7 @@ PyNumber_ToBase(PyObject *n, int base) int PySequence_Check(PyObject *s) { - if (PyDict_Check(s)) + if (_PyAnyDict_Check(s)) return 0; return Py_TYPE(s)->tp_as_sequence && Py_TYPE(s)->tp_as_sequence->sq_item != NULL; @@ -2454,7 +2454,7 @@ PyMapping_Keys(PyObject *o) if (o == NULL) { return null_error(); } - if (PyDict_CheckExact(o)) { + if (_PyAnyDict_CheckExact(o)) { return PyDict_Keys(o); } return method_output_as_list(o, &_Py_ID(keys)); @@ -2466,7 +2466,7 @@ PyMapping_Items(PyObject *o) if (o == NULL) { return null_error(); } - if (PyDict_CheckExact(o)) { + if (_PyAnyDict_CheckExact(o)) { return PyDict_Items(o); } return method_output_as_list(o, &_Py_ID(items)); @@ -2478,7 +2478,7 @@ PyMapping_Values(PyObject *o) if (o == NULL) { return null_error(); } - if (PyDict_CheckExact(o)) { + if (_PyAnyDict_CheckExact(o)) { return PyDict_Values(o); } return method_output_as_list(o, &_Py_ID(values)); diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 5ac4fbd812924c..4da0c7f241d96d 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1089,7 +1089,7 @@ static int mappingproxy_contains(PyObject *self, PyObject *key) { mappingproxyobject *pp = (mappingproxyobject *)self; - if (PyDict_CheckExact(pp->mapping)) + if (_PyAnyDict_CheckExact(pp->mapping)) return PyDict_Contains(pp->mapping, key); else return PySequence_Contains(pp->mapping, key); diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 13f810d1af12c6..758f68fdfa6547 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -914,7 +914,7 @@ new_dict_with_shared_keys(PyDictKeysObject *keys) static PyDictKeysObject * clone_combined_dict_keys(PyDictObject *orig) { - assert(PyDict_Check(orig)); + assert(_PyAnyDict_Check(orig)); assert(Py_TYPE(orig)->tp_iter == dict_iter); assert(orig->ma_values == NULL); assert(orig->ma_keys != Py_EMPTY_KEYS); @@ -2374,7 +2374,7 @@ _PyDict_GetItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash) PyDictObject *mp = (PyDictObject *)op; PyObject *value; - if (!PyDict_Check(op)) { + if (!_PyAnyDict_Check(op)) { PyErr_BadInternalCall(); return NULL; } @@ -3280,8 +3280,8 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return NULL; - if (PyDict_CheckExact(d)) { - if (PyDict_CheckExact(iterable)) { + if (_PyAnyDict_CheckExact(d)) { + if (_PyAnyDict_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; Py_BEGIN_CRITICAL_SECTION2(d, iterable); @@ -3496,7 +3496,7 @@ dict_subscript(PyObject *self, PyObject *key) if (ix == DKIX_ERROR) return NULL; if (ix == DKIX_EMPTY || value == NULL) { - if (!PyDict_CheckExact(mp)) { + if (!_PyAnyDict_CheckExact(mp)) { /* Look up __missing__ method if we're a subclass. */ PyObject *missing, *res; missing = _PyObject_LookupSpecial( @@ -3535,7 +3535,7 @@ keys_lock_held(PyObject *dict) { ASSERT_DICT_LOCKED(dict); - if (dict == NULL || !PyDict_Check(dict)) { + if (dict == NULL || !_PyAnyDict_Check(dict)) { PyErr_BadInternalCall(); return NULL; } @@ -3584,7 +3584,7 @@ values_lock_held(PyObject *dict) { ASSERT_DICT_LOCKED(dict); - if (dict == NULL || !PyDict_Check(dict)) { + if (dict == NULL || !_PyAnyDict_Check(dict)) { PyErr_BadInternalCall(); return NULL; } @@ -3632,7 +3632,7 @@ items_lock_held(PyObject *dict) { ASSERT_DICT_LOCKED(dict); - if (dict == NULL || !PyDict_Check(dict)) { + if (dict == NULL || !_PyAnyDict_Check(dict)) { PyErr_BadInternalCall(); return NULL; } @@ -3712,7 +3712,7 @@ dict_fromkeys_impl(PyTypeObject *type, PyObject *iterable, PyObject *value) static int dict_update_arg(PyObject *self, PyObject *arg) { - if (PyDict_CheckExact(arg)) { + if (_PyAnyDict_CheckExact(arg)) { return PyDict_Merge(self, arg, 1); } int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys)); @@ -3982,7 +3982,7 @@ dict_merge(PyInterpreterState *interp, PyObject *a, PyObject *b, int override) } mp = (PyDictObject*)a; int res = 0; - if (PyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) { + if (_PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) { other = (PyDictObject*)b; int res; Py_BEGIN_CRITICAL_SECTION2(a, b); @@ -4243,7 +4243,7 @@ PyDict_Copy(PyObject *o) Py_ssize_t PyDict_Size(PyObject *mp) { - if (mp == NULL || !PyDict_Check(mp)) { + if (mp == NULL || !_PyAnyDict_Check(mp)) { PyErr_BadInternalCall(); return -1; } @@ -5789,7 +5789,7 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self) { dictiterobject *di = (dictiterobject *)self; - assert (PyDict_Check(d)); + assert (_PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5919,7 +5919,7 @@ static PyObject * dict___reversed___impl(PyDictObject *self) /*[clinic end generated code: output=e674483336d1ed51 input=23210ef3477d8c4d]*/ { - assert (PyDict_Check(self)); + assert (_PyAnyDict_Check(self)); return dictiter_new(self, &PyDictRevIterKey_Type); } @@ -6194,7 +6194,7 @@ dictviews_to_set(PyObject *self) if (PyDictKeys_Check(self)) { // PySet_New() has fast path for the dict object. PyObject *dict = (PyObject *)((_PyDictViewObject *)self)->dv_dict; - if (PyDict_CheckExact(dict)) { + if (_PyAnyDict_CheckExact(dict)) { left = dict; } } @@ -7714,7 +7714,7 @@ validate_watcher_id(PyInterpreterState *interp, int watcher_id) int PyDict_Watch(int watcher_id, PyObject* dict) { - if (!PyDict_Check(dict)) { + if (!_PyAnyDict_Check(dict)) { PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary"); return -1; } @@ -7729,7 +7729,7 @@ PyDict_Watch(int watcher_id, PyObject* dict) int PyDict_Unwatch(int watcher_id, PyObject* dict) { - if (!PyDict_Check(dict)) { + if (!_PyAnyDict_Check(dict)) { PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary"); return -1; } diff --git a/Objects/listobject.c b/Objects/listobject.c index 1722ea60cdc68f..fd3a66deff5044 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -1424,7 +1424,7 @@ _list_extend(PyListObject *self, PyObject *iterable) res = list_extend_set(self, (PySetObject *)iterable); Py_END_CRITICAL_SECTION2(); } - else if (PyDict_CheckExact(iterable)) { + else if (_PyAnyDict_CheckExact(iterable)) { Py_BEGIN_CRITICAL_SECTION2(self, iterable); res = list_extend_dict(self, (PyDictObject *)iterable, 0 /*keys*/); Py_END_CRITICAL_SECTION2(); diff --git a/Objects/object.c b/Objects/object.c index 98eb0ab5a20f7a..8d7e23351e1ae1 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -57,7 +57,7 @@ _PyObject_CheckConsistency(PyObject *op, int check_content) if (PyUnicode_Check(op)) { _PyUnicode_CheckConsistency(op, check_content); } - else if (PyDict_Check(op)) { + else if (_PyAnyDict_Check(op)) { _PyDict_CheckConsistency(op, check_content); } return 1; diff --git a/Objects/odictobject.c b/Objects/odictobject.c index 45d2ea0203a9ff..177a8b8ddc8429 100644 --- a/Objects/odictobject.c +++ b/Objects/odictobject.c @@ -2272,7 +2272,7 @@ static int mutablemapping_update_arg(PyObject *self, PyObject *arg) { int res = 0; - if (PyDict_CheckExact(arg)) { + if (_PyAnyDict_CheckExact(arg)) { PyObject *items = PyDict_Items(arg); if (items == NULL) { return -1; diff --git a/Objects/setobject.c b/Objects/setobject.c index 85f4d7d403178a..e8ff12c71ef74f 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -989,7 +989,7 @@ set_iter(PyObject *so) static int set_update_dict_lock_held(PySetObject *so, PyObject *other) { - assert(PyDict_CheckExact(other)); + assert(_PyAnyDict_CheckExact(other)); _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(so); _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(other); @@ -1048,7 +1048,7 @@ set_update_lock_held(PySetObject *so, PyObject *other) if (PyAnySet_Check(other)) { return set_merge_lock_held(so, other); } - else if (PyDict_CheckExact(other)) { + else if (_PyAnyDict_CheckExact(other)) { return set_update_dict_lock_held(so, other); } return set_update_iterable_lock_held(so, other); @@ -1066,7 +1066,7 @@ set_update_local(PySetObject *so, PyObject *other) Py_END_CRITICAL_SECTION(); return rv; } - else if (PyDict_CheckExact(other)) { + else if (_PyAnyDict_CheckExact(other)) { int rv; Py_BEGIN_CRITICAL_SECTION(other); rv = set_update_dict_lock_held(so, other); @@ -1089,7 +1089,7 @@ set_update_internal(PySetObject *so, PyObject *other) Py_END_CRITICAL_SECTION2(); return rv; } - else if (PyDict_CheckExact(other)) { + else if (_PyAnyDict_CheckExact(other)) { int rv; Py_BEGIN_CRITICAL_SECTION2(so, other); rv = set_update_dict_lock_held(so, other); @@ -1781,7 +1781,7 @@ set_difference(PySetObject *so, PyObject *other) if (PyAnySet_Check(other)) { other_size = PySet_GET_SIZE(other); } - else if (PyDict_CheckExact(other)) { + else if (_PyAnyDict_CheckExact(other)) { other_size = PyDict_GET_SIZE(other); } else { @@ -1798,7 +1798,7 @@ set_difference(PySetObject *so, PyObject *other) if (result == NULL) return NULL; - if (PyDict_CheckExact(other)) { + if (_PyAnyDict_CheckExact(other)) { while (set_next(so, &pos, &entry)) { key = entry->key; hash = entry->hash; @@ -1989,7 +1989,7 @@ set_symmetric_difference_update_impl(PySetObject *so, PyObject *other) } int rv; - if (PyDict_CheckExact(other)) { + if (_PyAnyDict_CheckExact(other)) { Py_BEGIN_CRITICAL_SECTION2(so, other); rv = set_symmetric_difference_update_dict(so, other); Py_END_CRITICAL_SECTION2(); diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 4e8c132327b7d0..c71324f206ab3b 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -13189,7 +13189,7 @@ unicode_maketrans_impl(PyObject *x, PyObject *y, PyObject *z) const void *data; /* x must be a dict */ - if (!PyDict_CheckExact(x)) { + if (!_PyAnyDict_CheckExact(x)) { PyErr_SetString(PyExc_TypeError, "if you give only one argument " "to maketrans it must be a dict"); goto err; From 4a1a5046735c8cd23382ed70dded2e9cfb4dd1ce Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 21:35:43 +0100 Subject: [PATCH 08/22] copy: support frozendict --- Lib/copy.py | 7 ++++++- Lib/test/test_copy.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/copy.py b/Lib/copy.py index fff7e93c2a1b89..76004de347277d 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -105,7 +105,7 @@ def copy(x): types.BuiltinFunctionType, types.EllipsisType, types.NotImplementedType, types.FunctionType, types.CodeType, weakref.ref, super}) -_copy_builtin_containers = frozenset({list, dict, set, bytearray}) +_copy_builtin_containers = frozenset({list, dict, frozendict, set, bytearray}) def deepcopy(x, memo=None): """Deep copy operation on arbitrary Python objects. @@ -203,6 +203,11 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy): return y d[dict] = _deepcopy_dict +def _deepcopy_frozendict(x, memo, deepcopy=deepcopy): + y = _deepcopy_dict(x, memo, deepcopy) + return frozendict(y) +d[frozendict] = _deepcopy_frozendict + def _deepcopy_method(x, memo): # Copy instance methods return type(x)(x.__func__, deepcopy(x.__self__, memo)) d[types.MethodType] = _deepcopy_method diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 467ec09d99e462..fc61107a12ed5d 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -133,6 +133,18 @@ def test_copy_dict(self): self.assertEqual(y, x) self.assertIsNot(y, x) + def test_copy_frozendict(self): + x = frozendict(foo=1, bar=2) + y = copy.copy(x) + self.assertEqual(y, x) + self.assertEqual(type(y), frozendict) + self.assertIsNot(y, x) + x = frozendict() + y = copy.copy(x) + self.assertEqual(y, x) + self.assertEqual(type(y), frozendict) + self.assertIsNot(y, x) + def test_copy_set(self): x = {1, 2, 3} y = copy.copy(x) From 302767b9218bed97024e92b0ef4275961df0c70b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 22:08:01 +0100 Subject: [PATCH 09/22] Fix frozendict.__reduce_ex__() --- Objects/typeobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index c47dcf54ec3eeb..46a611aa868646 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -8010,7 +8010,7 @@ reduce_newobj(PyObject *obj) return NULL; } - state = object_getstate(obj, !(hasargs || PyList_Check(obj) || PyDict_Check(obj))); + state = object_getstate(obj, !(hasargs || PyList_Check(obj) || _PyAnyDict_Check(obj))); if (state == NULL) { Py_DECREF(newobj); Py_DECREF(newargs); From 07b351098a67b9e722dc3857fff66ab154f4eb52 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 22:32:16 +0100 Subject: [PATCH 10/22] exec(): accept frozendict for globals --- Objects/dictobject.c | 2 +- Python/bltinmodule.c | 2 +- Python/pythonrun.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 758f68fdfa6547..a5b084cdf2235c 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2623,7 +2623,7 @@ _PyDict_LoadGlobalStackRef(PyDictObject *globals, PyDictObject *builtins, PyObje PyObject * _PyDict_LoadBuiltinsFromGlobals(PyObject *globals) { - if (!PyDict_Check(globals)) { + if (!_PyAnyDict_Check(globals)) { PyErr_BadInternalCall(); return NULL; } diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index c45e61846794dd..3ba4f2d1271edd 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1135,7 +1135,7 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, locals = Py_NewRef(globals); } - if (!PyDict_Check(globals)) { + if (!_PyAnyDict_Check(globals)) { PyErr_Format(PyExc_TypeError, "exec() globals must be a dict, not %.100s", Py_TYPE(globals)->tp_name); goto error; diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 49ce0a97d4742f..601e813f2855e9 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1353,7 +1353,7 @@ static PyObject * run_eval_code_obj(PyThreadState *tstate, PyCodeObject *co, PyObject *globals, PyObject *locals) { /* Set globals['__builtins__'] if it doesn't exist */ - if (!globals || !PyDict_Check(globals)) { + if (!globals || !_PyAnyDict_Check(globals)) { PyErr_SetString(PyExc_SystemError, "globals must be a real dict"); return NULL; } From 04f66ad6eade13f97a7be3e064527f916ef5e6cf Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 13 Nov 2025 22:38:22 +0100 Subject: [PATCH 11/22] Make PyAnyDict_Check() public --- Include/cpython/dictobject.h | 6 ++-- Modules/_collectionsmodule.c | 2 +- Modules/_pickle.c | 4 +-- Objects/abstract.c | 10 +++--- Objects/descrobject.c | 2 +- Objects/dictobject.c | 70 ++++++++++++++++++------------------ Objects/listobject.c | 2 +- Objects/object.c | 2 +- Objects/odictobject.c | 2 +- Objects/setobject.c | 14 ++++---- Objects/typeobject.c | 6 ++-- Objects/unicodeobject.c | 2 +- Python/bltinmodule.c | 2 +- Python/marshal.c | 2 +- Python/pythonrun.c | 2 +- 15 files changed, 64 insertions(+), 64 deletions(-) diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h index 17bbed74cda862..57faea3eacf50c 100644 --- a/Include/cpython/dictobject.h +++ b/Include/cpython/dictobject.h @@ -37,9 +37,9 @@ PyAPI_DATA(PyTypeObject) PyFrozenDict_Type; #define PyFrozenDict_Check(op) PyObject_TypeCheck((op), &PyFrozenDict_Type) #define PyFrozenDict_CheckExact(op) Py_IS_TYPE((op), &PyFrozenDict_Type) -#define _PyAnyDict_CheckExact(ob) \ +#define PyAnyDict_CheckExact(ob) \ (PyDict_CheckExact(ob) || PyFrozenDict_CheckExact(ob)) -#define _PyAnyDict_Check(ob) \ +#define PyAnyDict_Check(ob) \ (PyDict_Check(ob) || PyFrozenDict_Check(ob)) PyAPI_FUNC(PyObject *) _PyDict_GetItem_KnownHash(PyObject *mp, PyObject *key, @@ -62,7 +62,7 @@ PyAPI_FUNC(int) PyDict_SetDefaultRef(PyObject *mp, PyObject *key, PyObject *defa /* Get the number of items of a dictionary. */ static inline Py_ssize_t PyDict_GET_SIZE(PyObject *op) { PyDictObject *mp; - assert(_PyAnyDict_Check(op)); + assert(PyAnyDict_Check(op)); mp = _Py_CAST(PyDictObject*, op); #ifdef Py_GIL_DISABLED return _Py_atomic_load_ssize_relaxed(&mp->ma_used); diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 298ae3d6a58320..386734c3da0f0b 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2403,7 +2403,7 @@ defdict_or(PyObject* left, PyObject* right) self = right; other = left; } - if (!_PyAnyDict_Check(other)) { + if (!PyAnyDict_Check(other)) { Py_RETURN_NOTIMPLEMENTED; } // Like copy(), this calls the object's class. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 13cace30863713..105312e2e4b75b 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -325,7 +325,7 @@ _Pickle_InitState(PickleState *st) PyObject_GetAttrString(compat_pickle, "NAME_MAPPING"); if (!st->name_mapping_2to3) goto error; - if (!_PyAnyDict_CheckExact(st->name_mapping_2to3)) { + if (!PyAnyDict_CheckExact(st->name_mapping_2to3)) { PyErr_Format(PyExc_RuntimeError, "_compat_pickle.NAME_MAPPING should be a dict, not %.200s", Py_TYPE(st->name_mapping_2to3)->tp_name); @@ -335,7 +335,7 @@ _Pickle_InitState(PickleState *st) PyObject_GetAttrString(compat_pickle, "IMPORT_MAPPING"); if (!st->import_mapping_2to3) goto error; - if (!_PyAnyDict_CheckExact(st->import_mapping_2to3)) { + if (!PyAnyDict_CheckExact(st->import_mapping_2to3)) { PyErr_Format(PyExc_RuntimeError, "_compat_pickle.IMPORT_MAPPING should be a dict, " "not %.200s", Py_TYPE(st->import_mapping_2to3)->tp_name); diff --git a/Objects/abstract.c b/Objects/abstract.c index ca22d5298d3d3f..2e06f0de2dd900 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -208,7 +208,7 @@ PyObject_GetItem(PyObject *o, PyObject *key) int PyMapping_GetOptionalItem(PyObject *obj, PyObject *key, PyObject **result) { - if (_PyAnyDict_CheckExact(obj)) { + if (PyAnyDict_CheckExact(obj)) { return PyDict_GetItemRef(obj, key, result); } @@ -1672,7 +1672,7 @@ PyNumber_ToBase(PyObject *n, int base) int PySequence_Check(PyObject *s) { - if (_PyAnyDict_Check(s)) + if (PyAnyDict_Check(s)) return 0; return Py_TYPE(s)->tp_as_sequence && Py_TYPE(s)->tp_as_sequence->sq_item != NULL; @@ -2454,7 +2454,7 @@ PyMapping_Keys(PyObject *o) if (o == NULL) { return null_error(); } - if (_PyAnyDict_CheckExact(o)) { + if (PyAnyDict_CheckExact(o)) { return PyDict_Keys(o); } return method_output_as_list(o, &_Py_ID(keys)); @@ -2466,7 +2466,7 @@ PyMapping_Items(PyObject *o) if (o == NULL) { return null_error(); } - if (_PyAnyDict_CheckExact(o)) { + if (PyAnyDict_CheckExact(o)) { return PyDict_Items(o); } return method_output_as_list(o, &_Py_ID(items)); @@ -2478,7 +2478,7 @@ PyMapping_Values(PyObject *o) if (o == NULL) { return null_error(); } - if (_PyAnyDict_CheckExact(o)) { + if (PyAnyDict_CheckExact(o)) { return PyDict_Values(o); } return method_output_as_list(o, &_Py_ID(values)); diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 4da0c7f241d96d..0baf7f2b832af4 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1089,7 +1089,7 @@ static int mappingproxy_contains(PyObject *self, PyObject *key) { mappingproxyobject *pp = (mappingproxyobject *)self; - if (_PyAnyDict_CheckExact(pp->mapping)) + if (PyAnyDict_CheckExact(pp->mapping)) return PyDict_Contains(pp->mapping, key); else return PySequence_Contains(pp->mapping, key); diff --git a/Objects/dictobject.c b/Objects/dictobject.c index a5b084cdf2235c..724610d6d797fd 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -659,7 +659,7 @@ _PyDict_CheckConsistency(PyObject *op, int check_content) do { if (!(expr)) { _PyObject_ASSERT_FAILED_MSG(op, Py_STRINGIFY(expr)); } } while (0) assert(op != NULL); - CHECK(_PyAnyDict_Check(op)); + CHECK(PyAnyDict_Check(op)); PyDictObject *mp = (PyDictObject *)op; PyDictKeysObject *keys = mp->ma_keys; @@ -914,7 +914,7 @@ new_dict_with_shared_keys(PyDictKeysObject *keys) static PyDictKeysObject * clone_combined_dict_keys(PyDictObject *orig) { - assert(_PyAnyDict_Check(orig)); + assert(PyAnyDict_Check(orig)); assert(Py_TYPE(orig)->tp_iter == dict_iter); assert(orig->ma_values == NULL); assert(orig->ma_keys != Py_EMPTY_KEYS); @@ -2281,7 +2281,7 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset, static PyObject * dict_getitem(PyObject *op, PyObject *key, const char *warnmsg) { - if (!_PyAnyDict_Check(op)) { + if (!PyAnyDict_Check(op)) { return NULL; } PyDictObject *mp = (PyDictObject *)op; @@ -2374,7 +2374,7 @@ _PyDict_GetItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash) PyDictObject *mp = (PyDictObject *)op; PyObject *value; - if (!_PyAnyDict_Check(op)) { + if (!PyAnyDict_Check(op)) { PyErr_BadInternalCall(); return NULL; } @@ -2445,7 +2445,7 @@ _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, Py int PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result) { - if (!_PyAnyDict_Check(op)) { + if (!PyAnyDict_Check(op)) { PyErr_BadInternalCall(); *result = NULL; return -1; @@ -2501,7 +2501,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key) PyDictObject*mp = (PyDictObject *)op; PyObject *value; - if (!_PyAnyDict_Check(op)) { + if (!PyAnyDict_Check(op)) { PyErr_BadInternalCall(); return NULL; } @@ -2623,7 +2623,7 @@ _PyDict_LoadGlobalStackRef(PyDictObject *globals, PyDictObject *builtins, PyObje PyObject * _PyDict_LoadBuiltinsFromGlobals(PyObject *globals) { - if (!_PyAnyDict_Check(globals)) { + if (!PyAnyDict_Check(globals)) { PyErr_BadInternalCall(); return NULL; } @@ -2662,7 +2662,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) assert(key); assert(value); - assert(_PyAnyDict_Check(mp)); + assert(PyAnyDict_Check(mp)); Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { dict_unhashable_type(key); @@ -2712,7 +2712,7 @@ PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value) static int _PyAnyDict_SetItem(PyObject *op, PyObject *key, PyObject *value) { - assert(_PyAnyDict_Check(op)); + assert(PyAnyDict_Check(op)); assert(key); assert(value); return _PyDict_SetItem_Take2((PyDictObject *)op, @@ -3006,7 +3006,7 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject *key, *value; Py_hash_t hash; - if (!_PyAnyDict_Check(op)) + if (!PyAnyDict_Check(op)) return 0; mp = (PyDictObject *)op; @@ -3280,8 +3280,8 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return NULL; - if (_PyAnyDict_CheckExact(d)) { - if (_PyAnyDict_CheckExact(iterable)) { + if (PyAnyDict_CheckExact(d)) { + if (PyAnyDict_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; Py_BEGIN_CRITICAL_SECTION2(d, iterable); @@ -3305,7 +3305,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return NULL; } - if (_PyAnyDict_CheckExact(d)) { + if (PyAnyDict_CheckExact(d)) { Py_BEGIN_CRITICAL_SECTION(d); while ((key = PyIter_Next(it)) != NULL) { status = setitem_lock_held((PyDictObject *)d, key, value); @@ -3496,7 +3496,7 @@ dict_subscript(PyObject *self, PyObject *key) if (ix == DKIX_ERROR) return NULL; if (ix == DKIX_EMPTY || value == NULL) { - if (!_PyAnyDict_CheckExact(mp)) { + if (!PyAnyDict_CheckExact(mp)) { /* Look up __missing__ method if we're a subclass. */ PyObject *missing, *res; missing = _PyObject_LookupSpecial( @@ -3535,7 +3535,7 @@ keys_lock_held(PyObject *dict) { ASSERT_DICT_LOCKED(dict); - if (dict == NULL || !_PyAnyDict_Check(dict)) { + if (dict == NULL || !PyAnyDict_Check(dict)) { PyErr_BadInternalCall(); return NULL; } @@ -3584,7 +3584,7 @@ values_lock_held(PyObject *dict) { ASSERT_DICT_LOCKED(dict); - if (dict == NULL || !_PyAnyDict_Check(dict)) { + if (dict == NULL || !PyAnyDict_Check(dict)) { PyErr_BadInternalCall(); return NULL; } @@ -3632,7 +3632,7 @@ items_lock_held(PyObject *dict) { ASSERT_DICT_LOCKED(dict); - if (dict == NULL || !_PyAnyDict_Check(dict)) { + if (dict == NULL || !PyAnyDict_Check(dict)) { PyErr_BadInternalCall(); return NULL; } @@ -3712,7 +3712,7 @@ dict_fromkeys_impl(PyTypeObject *type, PyObject *iterable, PyObject *value) static int dict_update_arg(PyObject *self, PyObject *arg) { - if (_PyAnyDict_CheckExact(arg)) { + if (PyAnyDict_CheckExact(arg)) { return PyDict_Merge(self, arg, 1); } int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys)); @@ -3778,7 +3778,7 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, int override) PyObject *fast; /* item as a 2-tuple or 2-list */ assert(d != NULL); - assert(_PyAnyDict_Check(d)); + assert(PyAnyDict_Check(d)); assert(seq2 != NULL); it = PyObject_GetIter(seq2); @@ -3976,13 +3976,13 @@ dict_merge(PyInterpreterState *interp, PyObject *a, PyObject *b, int override) * things quite efficiently. For the latter, we only require that * PyMapping_Keys() and PyObject_GetItem() be supported. */ - if (a == NULL || !_PyAnyDict_Check(a) || b == NULL) { + if (a == NULL || !PyAnyDict_Check(a) || b == NULL) { PyErr_BadInternalCall(); return -1; } mp = (PyDictObject*)a; int res = 0; - if (_PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) { + if (PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) { other = (PyDictObject*)b; int res; Py_BEGIN_CRITICAL_SECTION2(a, b); @@ -4226,7 +4226,7 @@ copy_lock_held(PyObject *o) PyObject * PyDict_Copy(PyObject *o) { - if (o == NULL || !_PyAnyDict_Check(o)) { + if (o == NULL || !PyAnyDict_Check(o)) { PyErr_BadInternalCall(); return NULL; } @@ -4243,7 +4243,7 @@ PyDict_Copy(PyObject *o) Py_ssize_t PyDict_Size(PyObject *mp) { - if (mp == NULL || !_PyAnyDict_Check(mp)) { + if (mp == NULL || !PyAnyDict_Check(mp)) { PyErr_BadInternalCall(); return -1; } @@ -4333,7 +4333,7 @@ dict_richcompare(PyObject *v, PyObject *w, int op) int cmp; PyObject *res; - if (!_PyAnyDict_Check(v) || !_PyAnyDict_Check(w)) { + if (!PyAnyDict_Check(v) || !PyAnyDict_Check(w)) { res = Py_NotImplemented; } else if (op == Py_EQ || op == Py_NE) { @@ -4777,7 +4777,7 @@ dict___sizeof___impl(PyDictObject *self) static PyObject * dict_or(PyObject *self, PyObject *other) { - if (!_PyAnyDict_Check(self) || !_PyAnyDict_Check(other)) { + if (!PyAnyDict_Check(self) || !PyAnyDict_Check(other)) { Py_RETURN_NOTIMPLEMENTED; } PyObject *new = PyDict_Copy(self); @@ -5243,7 +5243,7 @@ dictiter_iternextkey_lock_held(PyDictObject *d, PyObject *self) Py_ssize_t i; PyDictKeysObject *k; - assert (_PyAnyDict_Check(d)); + assert (PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5367,7 +5367,7 @@ dictiter_iternextvalue_lock_held(PyDictObject *d, PyObject *self) PyObject *value; Py_ssize_t i; - assert (_PyAnyDict_Check(d)); + assert (PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5489,7 +5489,7 @@ dictiter_iternextitem_lock_held(PyDictObject *d, PyObject *self, PyObject *key, *value; Py_ssize_t i; - assert (_PyAnyDict_Check(d)); + assert (PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5595,7 +5595,7 @@ dictiter_iternext_threadsafe(PyDictObject *d, PyObject *self, Py_ssize_t i; PyDictKeysObject *k; - assert (_PyAnyDict_Check(d)); + assert (PyAnyDict_Check(d)); if (di->di_used != _Py_atomic_load_ssize_relaxed(&d->ma_used)) { PyErr_SetString(PyExc_RuntimeError, @@ -5789,7 +5789,7 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self) { dictiterobject *di = (dictiterobject *)self; - assert (_PyAnyDict_Check(d)); + assert (PyAnyDict_Check(d)); ASSERT_DICT_LOCKED(d); if (di->di_used != d->ma_used) { @@ -5919,7 +5919,7 @@ static PyObject * dict___reversed___impl(PyDictObject *self) /*[clinic end generated code: output=e674483336d1ed51 input=23210ef3477d8c4d]*/ { - assert (_PyAnyDict_Check(self)); + assert (PyAnyDict_Check(self)); return dictiter_new(self, &PyDictRevIterKey_Type); } @@ -6004,7 +6004,7 @@ _PyDictView_New(PyObject *dict, PyTypeObject *type) PyErr_BadInternalCall(); return NULL; } - if (!_PyAnyDict_Check(dict)) { + if (!PyAnyDict_Check(dict)) { /* XXX Get rid of this restriction later */ PyErr_Format(PyExc_TypeError, "%s() requires a dict argument, not '%s'", @@ -6194,7 +6194,7 @@ dictviews_to_set(PyObject *self) if (PyDictKeys_Check(self)) { // PySet_New() has fast path for the dict object. PyObject *dict = (PyObject *)((_PyDictViewObject *)self)->dv_dict; - if (_PyAnyDict_CheckExact(dict)) { + if (PyAnyDict_CheckExact(dict)) { left = dict; } } @@ -7714,7 +7714,7 @@ validate_watcher_id(PyInterpreterState *interp, int watcher_id) int PyDict_Watch(int watcher_id, PyObject* dict) { - if (!_PyAnyDict_Check(dict)) { + if (!PyAnyDict_Check(dict)) { PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary"); return -1; } @@ -7729,7 +7729,7 @@ PyDict_Watch(int watcher_id, PyObject* dict) int PyDict_Unwatch(int watcher_id, PyObject* dict) { - if (!_PyAnyDict_Check(dict)) { + if (!PyAnyDict_Check(dict)) { PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary"); return -1; } diff --git a/Objects/listobject.c b/Objects/listobject.c index fd3a66deff5044..04b8e46d1e9859 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -1424,7 +1424,7 @@ _list_extend(PyListObject *self, PyObject *iterable) res = list_extend_set(self, (PySetObject *)iterable); Py_END_CRITICAL_SECTION2(); } - else if (_PyAnyDict_CheckExact(iterable)) { + else if (PyAnyDict_CheckExact(iterable)) { Py_BEGIN_CRITICAL_SECTION2(self, iterable); res = list_extend_dict(self, (PyDictObject *)iterable, 0 /*keys*/); Py_END_CRITICAL_SECTION2(); diff --git a/Objects/object.c b/Objects/object.c index 8d7e23351e1ae1..4ad57a67cbb573 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -57,7 +57,7 @@ _PyObject_CheckConsistency(PyObject *op, int check_content) if (PyUnicode_Check(op)) { _PyUnicode_CheckConsistency(op, check_content); } - else if (_PyAnyDict_Check(op)) { + else if (PyAnyDict_Check(op)) { _PyDict_CheckConsistency(op, check_content); } return 1; diff --git a/Objects/odictobject.c b/Objects/odictobject.c index 177a8b8ddc8429..1a8fa792864106 100644 --- a/Objects/odictobject.c +++ b/Objects/odictobject.c @@ -2272,7 +2272,7 @@ static int mutablemapping_update_arg(PyObject *self, PyObject *arg) { int res = 0; - if (_PyAnyDict_CheckExact(arg)) { + if (PyAnyDict_CheckExact(arg)) { PyObject *items = PyDict_Items(arg); if (items == NULL) { return -1; diff --git a/Objects/setobject.c b/Objects/setobject.c index e8ff12c71ef74f..8b14e4d87e3682 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -989,7 +989,7 @@ set_iter(PyObject *so) static int set_update_dict_lock_held(PySetObject *so, PyObject *other) { - assert(_PyAnyDict_CheckExact(other)); + assert(PyAnyDict_CheckExact(other)); _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(so); _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(other); @@ -1048,7 +1048,7 @@ set_update_lock_held(PySetObject *so, PyObject *other) if (PyAnySet_Check(other)) { return set_merge_lock_held(so, other); } - else if (_PyAnyDict_CheckExact(other)) { + else if (PyAnyDict_CheckExact(other)) { return set_update_dict_lock_held(so, other); } return set_update_iterable_lock_held(so, other); @@ -1066,7 +1066,7 @@ set_update_local(PySetObject *so, PyObject *other) Py_END_CRITICAL_SECTION(); return rv; } - else if (_PyAnyDict_CheckExact(other)) { + else if (PyAnyDict_CheckExact(other)) { int rv; Py_BEGIN_CRITICAL_SECTION(other); rv = set_update_dict_lock_held(so, other); @@ -1089,7 +1089,7 @@ set_update_internal(PySetObject *so, PyObject *other) Py_END_CRITICAL_SECTION2(); return rv; } - else if (_PyAnyDict_CheckExact(other)) { + else if (PyAnyDict_CheckExact(other)) { int rv; Py_BEGIN_CRITICAL_SECTION2(so, other); rv = set_update_dict_lock_held(so, other); @@ -1781,7 +1781,7 @@ set_difference(PySetObject *so, PyObject *other) if (PyAnySet_Check(other)) { other_size = PySet_GET_SIZE(other); } - else if (_PyAnyDict_CheckExact(other)) { + else if (PyAnyDict_CheckExact(other)) { other_size = PyDict_GET_SIZE(other); } else { @@ -1798,7 +1798,7 @@ set_difference(PySetObject *so, PyObject *other) if (result == NULL) return NULL; - if (_PyAnyDict_CheckExact(other)) { + if (PyAnyDict_CheckExact(other)) { while (set_next(so, &pos, &entry)) { key = entry->key; hash = entry->hash; @@ -1989,7 +1989,7 @@ set_symmetric_difference_update_impl(PySetObject *so, PyObject *other) } int rv; - if (_PyAnyDict_CheckExact(other)) { + if (PyAnyDict_CheckExact(other)) { Py_BEGIN_CRITICAL_SECTION2(so, other); rv = set_symmetric_difference_update_dict(so, other); Py_END_CRITICAL_SECTION2(); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 46a611aa868646..30b1856c71dad7 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3998,7 +3998,7 @@ subtype_dict(PyObject *obj, void *context) int _PyObject_SetDict(PyObject *obj, PyObject *value) { - if (value != NULL && !_PyAnyDict_Check(value)) { + if (value != NULL && !PyAnyDict_Check(value)) { PyErr_Format(PyExc_TypeError, "__dict__ must be set to a dictionary, " "not a '%.200s'", Py_TYPE(value)->tp_name); @@ -5999,7 +5999,7 @@ find_name_in_mro(PyTypeObject *type, PyObject *name, int *error) for (Py_ssize_t i = 0; i < n; i++) { PyObject *base = PyTuple_GET_ITEM(mro, i); PyObject *dict = lookup_tp_dict(_PyType_CAST(base)); - assert(dict && _PyAnyDict_Check(dict)); + assert(dict && PyAnyDict_Check(dict)); if (_PyDict_GetItemRef_KnownHash((PyDictObject *)dict, name, hash, &res) < 0) { *error = -1; goto done; @@ -8010,7 +8010,7 @@ reduce_newobj(PyObject *obj) return NULL; } - state = object_getstate(obj, !(hasargs || PyList_Check(obj) || _PyAnyDict_Check(obj))); + state = object_getstate(obj, !(hasargs || PyList_Check(obj) || PyAnyDict_Check(obj))); if (state == NULL) { Py_DECREF(newobj); Py_DECREF(newargs); diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index c71324f206ab3b..55ceea8c96e8c9 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -13189,7 +13189,7 @@ unicode_maketrans_impl(PyObject *x, PyObject *y, PyObject *z) const void *data; /* x must be a dict */ - if (!_PyAnyDict_CheckExact(x)) { + if (!PyAnyDict_CheckExact(x)) { PyErr_SetString(PyExc_TypeError, "if you give only one argument " "to maketrans it must be a dict"); goto err; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 3ba4f2d1271edd..f35cdab9dcb2df 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1135,7 +1135,7 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, locals = Py_NewRef(globals); } - if (!_PyAnyDict_Check(globals)) { + if (!PyAnyDict_Check(globals)) { PyErr_Format(PyExc_TypeError, "exec() globals must be a dict, not %.100s", Py_TYPE(globals)->tp_name); goto error; diff --git a/Python/marshal.c b/Python/marshal.c index e3fef318fa142a..9e664bd39cc181 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -572,7 +572,7 @@ w_complex_object(PyObject *v, char flag, WFILE *p) w_object(PyList_GET_ITEM(v, i), p); } } - else if (_PyAnyDict_CheckExact(v)) { + else if (PyAnyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; if (PyFrozenDict_CheckExact(v)) { diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 601e813f2855e9..ca11b69a3a8acd 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1353,7 +1353,7 @@ static PyObject * run_eval_code_obj(PyThreadState *tstate, PyCodeObject *co, PyObject *globals, PyObject *locals) { /* Set globals['__builtins__'] if it doesn't exist */ - if (!globals || !_PyAnyDict_Check(globals)) { + if (!globals || !PyAnyDict_Check(globals)) { PyErr_SetString(PyExc_SystemError, "globals must be a real dict"); return NULL; } From 6fca6d1aa2d92dada90aa4137a40ceb041dd2e9e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 10:34:48 +0100 Subject: [PATCH 12/22] Optimize frozendict.copy() --- Objects/dictobject.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 724610d6d797fd..982d81ffb7566a 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4231,6 +4231,10 @@ PyDict_Copy(PyObject *o) return NULL; } + if (PyFrozenDict_CheckExact(o)) { + return Py_NewRef(o); + } + PyObject *res; Py_BEGIN_CRITICAL_SECTION(o); From 6727967522b2b664fd1b8ed4bea5e3ead1c93938 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 11:18:54 +0100 Subject: [PATCH 13/22] Fix frozendict merge ("|=" operator); add tests --- Lib/test/test_dict.py | 36 ++++++++++++++++++++++++++++++++++++ Objects/dictobject.c | 7 +++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index af3a390150a549..b95872549fff5b 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1668,5 +1668,41 @@ class FrozenDictMappingTests(mapping_tests.BasicTestImmutableMappingProtocol): type2test = frozendict +class FrozenDict(frozendict): + pass + + +class FrozenDictTests(unittest.TestCase): + def test_copy(self): + d = frozendict(x=1, y=2) + d2 = d.copy() + self.assertIs(d2, d) + + d = FrozenDict(x=1, y=2) + d2 = d.copy() + self.assertIsNot(d2, d) + self.assertEqual(d2, frozendict(x=1, y=2)) + self.assertEqual(type(d2), frozendict) + + def test_merge(self): + # test "a | b" operator + self.assertEqual(frozendict(x=1) | frozendict(y=2), + frozendict({'x': 1, 'y': 2})) + self.assertEqual(frozendict(x=1) | dict(y=2), + frozendict({'x': 1, 'y': 2})) + self.assertEqual(frozendict(x=1, y=2) | frozendict(y=5), + frozendict({'x': 1, 'y': 5})) + + def test_update(self): + # test "a |= b" operator + d = frozendict(x=1) + copy = d + self.assertIs(copy, d) + d |= frozendict(y=2) + self.assertIsNot(copy, d) + self.assertEqual(d, frozendict({'x': 1, 'y': 2})) + self.assertEqual(copy, frozendict({'x': 1})) + + if __name__ == "__main__": unittest.main() diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 982d81ffb7566a..4406f136f21b7b 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4096,6 +4096,9 @@ static PyObject * dict_copy_impl(PyDictObject *self) /*[clinic end generated code: output=ffb782cf970a5c39 input=73935f042b639de4]*/ { + if (PyFrozenDict_CheckExact(self)) { + return Py_NewRef(self); + } return PyDict_Copy((PyObject *)self); } @@ -4231,10 +4234,6 @@ PyDict_Copy(PyObject *o) return NULL; } - if (PyFrozenDict_CheckExact(o)) { - return Py_NewRef(o); - } - PyObject *res; Py_BEGIN_CRITICAL_SECTION(o); From d231cdb942a2a9ba36362f4ab1d8ee9a609aac2e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 11:46:39 +0100 Subject: [PATCH 14/22] copy: use _copy_atomic_types instead; add tests --- Lib/copy.py | 4 ++-- Lib/test/test_copy.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/copy.py b/Lib/copy.py index 76004de347277d..899c8c2a355d95 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -101,11 +101,11 @@ def copy(x): _copy_atomic_types = frozenset({types.NoneType, int, float, bool, complex, str, tuple, - bytes, frozenset, type, range, slice, property, + bytes, frozenset, frozendict, type, range, slice, property, types.BuiltinFunctionType, types.EllipsisType, types.NotImplementedType, types.FunctionType, types.CodeType, weakref.ref, super}) -_copy_builtin_containers = frozenset({list, dict, frozendict, set, bytearray}) +_copy_builtin_containers = frozenset({list, dict, set, bytearray}) def deepcopy(x, memo=None): """Deep copy operation on arbitrary Python objects. diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index fc61107a12ed5d..5e7c885b00cbe0 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -161,6 +161,12 @@ def test_copy_frozenset(self): x = frozenset() self.assertIs(copy.copy(x), x) + def test_copy_frozendict(self): + x = frozendict(x=1, y=2) + self.assertIs(copy.copy(x), x) + x = frozendict() + self.assertIs(copy.copy(x), x) + def test_copy_bytearray(self): x = bytearray(b'abc') y = copy.copy(x) From 3a07c46bed4211856d6715c0111cae6999093bcb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 15:26:53 +0100 Subject: [PATCH 15/22] frozendict --- Lib/pprint.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 92a2c543ac279c..5aef27a806c4bc 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -220,7 +220,14 @@ def _pprint_dataclass(self, object, stream, indent, allowance, context, level): def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write - write('{') + typ = object.__class__ + if typ is dict: + write('{') + end = '}' + else: + stream.write(typ.__name__ + '({') + end = '})' + indent += len(typ.__name__) + 1 if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) @@ -231,9 +238,10 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): items = object.items() self._format_dict_items(items, stream, indent, allowance + 1, context, level) - write('}') + write(end) _dispatch[dict.__repr__] = _pprint_dict + _dispatch[frozendict.__repr__] = _pprint_dict def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): if not len(object): From bc25bb201b64e0f20ce25bbbfc902c02097b6650 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 15:28:56 +0100 Subject: [PATCH 16/22] pprint supports frozendict --- Lib/pprint.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 5aef27a806c4bc..ffba173c2bb24a 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -221,13 +221,13 @@ def _pprint_dataclass(self, object, stream, indent, allowance, context, level): def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write typ = object.__class__ - if typ is dict: - write('{') - end = '}' - else: + if typ is frozendict: stream.write(typ.__name__ + '({') end = '})' indent += len(typ.__name__) + 1 + else: + write('{') + end = '}' if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) From 0acff5eb535c18b1d63202bafa076b28ed7aac77 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 20:48:20 +0100 Subject: [PATCH 17/22] change json.tool._group_to_theme_color formatting --- Lib/json/tool.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 14a5c9ecb74d13..e0b944b197d38b 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -22,7 +22,13 @@ (?Pnull) ''', re.VERBOSE) -_group_to_theme_color = frozendict(key="definition",string="string",number="number",boolean="keyword",null="keyword") +_group_to_theme_color = frozendict({ + "key": "definition", + "string": "string", + "number": "number", + "boolean": "keyword", + "null": "keyword", +}) def _colorize_json(json_str, theme): From e90156e1e5e2d5e30486aa0632ba7c4fd3e30087 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 20:57:30 +0100 Subject: [PATCH 18/22] json: use PyAnyDict_Check() --- Modules/_json.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_json.c b/Modules/_json.c index 34626dc82e7236..524db53f7487e0 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1597,7 +1597,7 @@ encoder_listencode_obj(PyEncoderObject *s, PyUnicodeWriter *writer, _Py_LeaveRecursiveCall(); return rv; } - else if (PyDict_Check(obj) || PyFrozenDict_Check(obj)) { + else if (PyAnyDict_Check(obj)) { if (_Py_EnterRecursiveCall(" while encoding a JSON object")) return -1; rv = encoder_listencode_dict(s, writer, obj, indent_level, indent_cache); From fe7e7f5ecfaa4a2183888984200dd538c1d381a3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 21:02:05 +0100 Subject: [PATCH 19/22] _pickle: fix refleak --- Modules/_pickle.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 105312e2e4b75b..d92a644b9e63b8 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -611,6 +611,7 @@ Pdata_poplist2(PickleState *state, Pdata *self, Py_ssize_t start) for (Py_ssize_t i = start, j = 0; j < len; i+=2, j++) { PyObject *subtuple = PyTuple_New(2); if (subtuple == NULL) { + Py_DECREF(list); return NULL; } From 4c5c8d2b7b1324649b8296601151dfba2f89ebb8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 21:28:34 +0100 Subject: [PATCH 20/22] frozendict_hash: use atomic load/store --- Objects/dictobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 4406f136f21b7b..7f54c64e224fa9 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7878,7 +7878,7 @@ static Py_hash_t frozendict_hash(PyObject *op) { PyFrozenDictObject *self = _PyFrozenDictObject_CAST(op); - Py_hash_t hash = self->ma_hash; + Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash); if (hash != -1) { return hash; } @@ -7899,7 +7899,7 @@ frozendict_hash(PyObject *op) return -1; } - self->ma_hash = hash; + FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, hash); return hash; } From 0a57448485eaf85526ecd53687bf337c1f60269d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Nov 2025 23:20:52 +0100 Subject: [PATCH 21/22] Update Lib/pydoc_data/topics.py Co-authored-by: kalvdans --- Lib/pydoc_data/topics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index ef6cc1001542ac..0010d8a4211309 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -13174,7 +13174,7 @@ class dict(iterable, /, **kwargs) See also: - "frozendict" can be used to create a read-only view of a "dict". + "frozendict" can be used to create a read-only copy of a "dict". Dictionary view objects From fc66056604279850537f1bc4841a143b6f399400 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 17 Nov 2025 16:31:18 +0100 Subject: [PATCH 22/22] Fix test_copy: remove duplicated test --- Lib/test/test_copy.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 5e7c885b00cbe0..971b3b4c2f56f8 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -134,16 +134,10 @@ def test_copy_dict(self): self.assertIsNot(y, x) def test_copy_frozendict(self): - x = frozendict(foo=1, bar=2) - y = copy.copy(x) - self.assertEqual(y, x) - self.assertEqual(type(y), frozendict) - self.assertIsNot(y, x) + x = frozendict(x=1, y=2) + self.assertIs(copy.copy(x), x) x = frozendict() - y = copy.copy(x) - self.assertEqual(y, x) - self.assertEqual(type(y), frozendict) - self.assertIsNot(y, x) + self.assertIs(copy.copy(x), x) def test_copy_set(self): x = {1, 2, 3} @@ -161,12 +155,6 @@ def test_copy_frozenset(self): x = frozenset() self.assertIs(copy.copy(x), x) - def test_copy_frozendict(self): - x = frozendict(x=1, y=2) - self.assertIs(copy.copy(x), x) - x = frozendict() - self.assertIs(copy.copy(x), x) - def test_copy_bytearray(self): x = bytearray(b'abc') y = copy.copy(x)