gh-141510, PEP 814: Add built-in frozendict type (#144757)

Add TYPE_FROZENDICT to the marshal module.

Add C API functions:

* PyAnyDict_Check()
* PyAnyDict_CheckExact()
* PyFrozenDict_Check()
* PyFrozenDict_CheckExact()
* PyFrozenDict_New()

Add PyFrozenDict_Type C type.

Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: Adam Johnson <me@adamj.eu>
Co-authored-by: Benedikt Johannes <benedikt.johannes.hofer@gmail.com>
This commit is contained in:
Victor Stinner 2026-02-17 10:54:41 +01:00 committed by GitHub
parent 63531a3867
commit 696cdfc0a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 579 additions and 126 deletions

View file

@ -2,7 +2,7 @@
.. _dictobjects:
Dictionary Objects
Dictionary objects
------------------
.. index:: pair: object; dictionary
@ -444,7 +444,7 @@ Dictionary Objects
.. versionadded:: 3.12
Dictionary View Objects
Dictionary view objects
^^^^^^^^^^^^^^^^^^^^^^^
.. c:function:: int PyDictViewSet_Check(PyObject *op)
@ -490,7 +490,58 @@ Dictionary View Objects
always succeeds.
Ordered Dictionaries
Frozen dictionary objects
^^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: next
.. c:var:: PyTypeObject PyFrozenDict_Type
This instance of :c:type:`PyTypeObject` represents the Python frozen
dictionary type.
This is the same object as :class:`frozendict` in the Python layer.
.. c:function:: int PyAnyDict_Check(PyObject *p)
Return true if *p* is a :class:`dict` object, a :class:`frozendict` object,
or an instance of a subtype of the :class:`!dict` or :class:`!frozendict`
type.
This function always succeeds.
.. c:function:: int PyAnyDict_CheckExact(PyObject *p)
Return true if *p* is a :class:`dict` object or a :class:`frozendict` object,
but not an instance of a subtype of the :class:`!dict` or
:class:`!frozendict` type.
This function always succeeds.
.. c:function:: int PyFrozenDict_Check(PyObject *p)
Return true if *p* is a :class:`frozendict` object or an instance of a
subtype of the :class:`!frozendict` type.
This function always succeeds.
.. c:function:: int PyFrozenDict_CheckExact(PyObject *p)
Return true if *p* is a :class:`frozendict` object, but not an instance of a
subtype of the :class:`!frozendict` type.
This function always succeeds.
.. c:function:: PyObject* PyFrozenDict_New(PyObject *iterable)
Return a new :class:`frozendict` from an iterable, or ``NULL`` on failure
with an exception set.
Create an empty dictionary if *iterable* is ``NULL``.
Ordered dictionaries
^^^^^^^^^^^^^^^^^^^^
Python's C API provides interface for :class:`collections.OrderedDict` from C.

View file

@ -5305,8 +5305,8 @@ frozenset, a temporary one is created from *elem*.
.. _typesmapping:
Mapping Types --- :class:`dict`
===============================
Mapping types --- :class:`!dict`, :class:`!frozendict`
======================================================
.. index::
pair: object; mapping
@ -5317,8 +5317,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
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.)
@ -5588,10 +5589,9 @@ can be used interchangeably to index the same dictionary entry.
Dictionaries are now reversible.
.. seealso::
:class:`types.MappingProxyType` can be used to create a read-only view
of a :class:`dict`.
.. seealso::
:class:`types.MappingProxyType` can be used to create a read-only view
of a :class:`dict`.
.. _thread-safety-dict:
@ -5839,6 +5839,41 @@ An example of dictionary view usage::
500
Frozen dictionaries
-------------------
.. 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.
A :class:`!frozendict` has a similar API to the :class:`dict` API, with the
following differences:
* :class:`!dict` has more methods than :class:`!frozendict`:
* :meth:`!__delitem__`
* :meth:`!__setitem__`
* :meth:`~dict.clear`
* :meth:`~dict.pop`
* :meth:`~dict.popitem`
* :meth:`~dict.setdefault`
* :meth:`~dict.update`
* A :class:`!frozendict` can be hashed with ``hash(frozendict)`` if all keys and
values can be hashed.
* ``frozendict |= other`` does not modify the :class:`!frozendict` in-place but
creates a new frozen dictionary.
:class:`!frozendict` is not a :class:`!dict` subclass but inherits directly
from ``object``.
.. versionadded:: next
.. _typecontextmanager:
Context Manager Types
@ -6062,6 +6097,7 @@ list is non-exhaustive.
* :class:`list`
* :class:`dict`
* :class:`set`
* :class:`frozendict`
* :class:`frozenset`
* :class:`type`
* :class:`asyncio.Future`

View file

@ -67,6 +67,8 @@ Summary -- Release highlights
* :pep:`810`: :ref:`Explicit lazy imports for faster startup times
<whatsnew315-pep810>`
* :pep:`814`: :ref:`Add frozendict built-in type
<whatsnew315-frozendict>`
* :pep:`799`: :ref:`A dedicated profiling package for organizing Python
profiling tools <whatsnew315-profiling-package>`
* :pep:`799`: :ref:`Tachyon: High frequency statistical sampling profiler
@ -180,6 +182,21 @@ raise :exc:`SyntaxError`).
(Contributed by Pablo Galindo Salgado and Dino Viehland in :gh:`142349`.)
.. _whatsnew315-frozendict:
:pep:`814`: Add frozendict built-in type
----------------------------------------
A new public immutable type :class:`frozendict` is added to the :mod:`builtins`
module. It is not a ``dict`` subclass but inherits directly from ``object``.
A ``frozendict`` can be hashed with ``hash(frozendict)`` if all keys and values
can be hashed.
.. seealso:: :pep:`814` for the full specification and rationale.
.. _whatsnew315-profiling-package:
:pep:`799`: A dedicated profiling package
@ -1525,6 +1542,16 @@ C API changes
New features
------------
* Add the following functions for the new :class:`frozendict` type:
* :c:func:`PyAnyDict_Check`
* :c:func:`PyAnyDict_CheckExact`
* :c:func:`PyFrozenDict_Check`
* :c:func:`PyFrozenDict_CheckExact`
* :c:func:`PyFrozenDict_New`
(Contributed by Victor Stinner in :gh:`141510`.)
* Add :c:func:`PySys_GetAttr`, :c:func:`PySys_GetAttrString`,
:c:func:`PySys_GetOptionalAttr`, and :c:func:`PySys_GetOptionalAttrString`
functions as replacements for :c:func:`PySys_GetObject`.

View file

@ -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
@ -42,7 +52,7 @@ PyAPI_FUNC(PyObject *) PyDict_SetDefault(
/* 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);
@ -93,3 +103,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);

View file

@ -408,6 +408,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

View file

@ -26,6 +26,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

View file

@ -823,6 +823,7 @@ def __eq__(self, other):
__reversed__ = None
Mapping.register(frozendict)
Mapping.register(mappingproxy)
Mapping.register(framelocalsproxy)

View file

@ -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
@ -20,12 +20,9 @@ def _empty_mapping(self):
"""Return an empty mapping object"""
return self.type2test()
def _full_mapping(self, data):
"""Return a mapping object with the value contained in data
"""Return a mapping object with the values 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.assertFalse(bool(self._empty_mapping()))
self.assertTrue(bool(self.reference))
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.assertIsNone(d.get(list(self.other.keys())[0]))
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
d = self.reference
self.assertIsNone(d.get(list(self.other.keys())[0]))
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 values 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)

View file

@ -1723,6 +1723,58 @@ class Dict(dict):
class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol):
type2test = Dict
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}))
fd = frozendict(x=1, y=2)
self.assertIs(fd | frozendict(), fd)
self.assertIs(fd | {}, fd)
self.assertIs(frozendict() | fd, fd)
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}))
def test_repr(self):
d = frozendict(x=1, y=2)
self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})")
class MyFrozenDict(frozendict):
pass
d = MyFrozenDict(x=1, y=2)
self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})")
if __name__ == "__main__":
unittest.main()

View file

@ -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

View file

@ -6127,7 +6127,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"}

View file

@ -0,0 +1,9 @@
Add the following functions for the new :class:`frozendict` type:
* :c:func:`PyAnyDict_Check`
* :c:func:`PyAnyDict_CheckExact`
* :c:func:`PyFrozenDict_Check`
* :c:func:`PyFrozenDict_CheckExact`
* :c:func:`PyFrozenDict_New`
Patch by Victor Stinner.

View file

@ -0,0 +1 @@
Add built-in :class:`frozendict` type. Patch by Victor Stinner.

View file

@ -135,6 +135,10 @@ As a consequence of this, split keys have a maximum size of 16.
#include "stringlib/eq.h" // unicode_eq()
#include <stdbool.h>
// Forward declarations
static PyObject* frozendict_new(PyTypeObject *type, PyObject *args,
PyObject *kwds);
/*[clinic input]
class dict "PyDictObject *" "&PyDict_Type"
@ -278,6 +282,11 @@ load_keys_nentries(PyDictObject *mp)
#endif
#define _PyAnyDict_CAST(op) \
(assert(PyAnyDict_Check(op)), _Py_CAST(PyDictObject*, op))
#define GET_USED(ep) FT_ATOMIC_LOAD_SSIZE_RELAXED((ep)->ma_used)
#define STORE_KEY(ep, key) FT_ATOMIC_STORE_PTR_RELEASE((ep)->me_key, key)
#define STORE_VALUE(ep, value) FT_ATOMIC_STORE_PTR_RELEASE((ep)->me_value, value)
#define STORE_SPLIT_VALUE(mp, idx, value) FT_ATOMIC_STORE_PTR_RELEASE(mp->ma_values->values[idx], value)
@ -654,7 +663,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;
@ -909,7 +918,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);
@ -2293,7 +2302,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;
@ -2392,7 +2401,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;
}
@ -2463,7 +2472,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;
@ -2519,7 +2528,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;
}
@ -2668,7 +2677,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);
@ -2713,6 +2722,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)
{
@ -2996,7 +3015,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;
@ -3265,8 +3284,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);
@ -3290,7 +3309,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);
@ -3460,7 +3479,7 @@ dict_repr(PyObject *self)
static Py_ssize_t
dict_length(PyObject *self)
{
return FT_ATOMIC_LOAD_SSIZE_RELAXED(((PyDictObject *)self)->ma_used);
return GET_USED(_PyAnyDict_CAST(self));
}
static PyObject *
@ -3480,7 +3499,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(
@ -3519,7 +3538,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;
}
@ -3568,7 +3587,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;
}
@ -3616,7 +3635,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;
}
@ -3696,7 +3715,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));
@ -3762,7 +3781,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);
@ -3958,13 +3977,13 @@ dict_merge(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;
}
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);
@ -4075,6 +4094,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);
}
@ -4104,13 +4126,19 @@ copy_lock_held(PyObject *o)
{
PyObject *copy;
PyDictObject *mp;
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)) {
@ -4119,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;
@ -4132,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:
@ -4170,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(copy, o, 1) == 0)
@ -4182,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;
}
@ -4199,11 +4243,11 @@ 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;
}
return FT_ATOMIC_LOAD_SSIZE_RELAXED(((PyDictObject *)mp)->ma_used);
return GET_USED((PyDictObject *)mp);
}
/* Return 1 if dicts equal, 0 if not, -1 if error.
@ -4289,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) {
@ -4739,7 +4783,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);
@ -4753,6 +4797,29 @@ dict_or(PyObject *self, PyObject *other)
return new;
}
static PyObject *
frozendict_or(PyObject *self, PyObject *other)
{
if (PyFrozenDict_CheckExact(self)) {
// frozendict() | frozendict(...) => frozendict(...)
if (GET_USED((PyDictObject *)self) == 0
&& PyFrozenDict_CheckExact(other))
{
return Py_NewRef(other);
}
// frozendict(...) | frozendict() => frozendict(...)
if (PyAnyDict_CheckExact(other)
&& GET_USED((PyDictObject *)other) == 0)
{
return Py_NewRef(self);
}
}
return dict_or(self, other);
}
static PyObject *
dict_ior(PyObject *self, PyObject *other)
{
@ -4905,7 +4972,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;
}
@ -4918,7 +4993,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;
}
@ -4991,6 +5067,7 @@ PyTypeObject PyDict_Type = {
.tp_version_tag = _Py_TYPE_VERSION_DICT,
};
/* For backward compatibility with old dictionary interface */
PyObject *
@ -5073,7 +5150,7 @@ dictiter_new(PyDictObject *dict, PyTypeObject *itertype)
return NULL;
}
di->di_dict = (PyDictObject*)Py_NewRef(dict);
used = FT_ATOMIC_LOAD_SSIZE_RELAXED(dict->ma_used);
used = GET_USED(dict);
di->di_used = used;
di->len = used;
if (itertype == &PyDictRevIterKey_Type ||
@ -5129,7 +5206,7 @@ dictiter_len(PyObject *self, PyObject *Py_UNUSED(ignored))
{
dictiterobject *di = (dictiterobject *)self;
Py_ssize_t len = 0;
if (di->di_dict != NULL && di->di_used == FT_ATOMIC_LOAD_SSIZE_RELAXED(di->di_dict->ma_used))
if (di->di_dict != NULL && di->di_used == GET_USED(di->di_dict))
len = FT_ATOMIC_LOAD_SSIZE_RELAXED(di->len);
return PyLong_FromSize_t(len);
}
@ -5166,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) {
@ -5290,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) {
@ -5412,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) {
@ -5518,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,
@ -5712,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) {
@ -5842,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);
}
@ -5915,7 +5992,7 @@ dictview_len(PyObject *self)
_PyDictViewObject *dv = (_PyDictViewObject *)self;
Py_ssize_t len = 0;
if (dv->dv_dict != NULL)
len = FT_ATOMIC_LOAD_SSIZE_RELAXED(dv->dv_dict->ma_used);
len = GET_USED(dv->dv_dict);
return len;
}
@ -5927,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'",
@ -6117,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;
}
}
@ -6847,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) {
@ -7169,7 +7251,7 @@ _PyObject_IsInstanceDictEmpty(PyObject *obj)
if (dict == NULL) {
return 1;
}
return FT_ATOMIC_LOAD_SSIZE_RELAXED(((PyDictObject *)dict)->ma_used) == 0;
return GET_USED((PyDictObject *)dict) == 0;
}
int
@ -7631,7 +7713,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;
}
@ -7646,7 +7728,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;
}
@ -7743,3 +7825,147 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj)
return 0;
}
#endif
// --- frozendict implementation ---------------------------------------------
static PyNumberMethods frozendict_as_number = {
.nb_or = frozendict_or,
};
static PyMappingMethods frozendict_as_mapping = {
.mp_length = dict_length,
.mp_subscript = dict_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 = FT_ATOMIC_LOAD_SSIZE_RELAXED(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;
}
FT_ATOMIC_STORE_SSIZE_RELAXED(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)
.tp_name = "frozendict",
.tp_basicsize = sizeof(PyFrozenDictObject),
.tp_dealloc = dict_dealloc,
.tp_repr = frozendict_repr,
.tp_as_number = &frozendict_as_number,
.tp_as_sequence = &dict_as_sequence,
.tp_as_mapping = &frozendict_as_mapping,
.tp_hash = frozendict_hash,
.tp_getattro = PyObject_GenericGetAttr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC
| Py_TPFLAGS_BASETYPE
| _Py_TPFLAGS_MATCH_SELF | Py_TPFLAGS_MAPPING,
.tp_doc = dictionary_doc,
.tp_traverse = dict_traverse,
.tp_clear = dict_tp_clear,
.tp_richcompare = dict_richcompare,
.tp_iter = dict_iter,
.tp_methods = frozendict_methods,
.tp_init = dict_init,
.tp_alloc = _PyType_AllocNoTrack,
.tp_new = frozendict_new,
.tp_free = PyObject_GC_Del,
.tp_vectorcall = dict_vectorcall,
.tp_version_tag = _Py_TYPE_VERSION_FROZENDICT,
};

View file

@ -58,7 +58,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;
@ -2532,8 +2532,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,

View file

@ -3536,6 +3536,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);

View file

@ -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 '?'
@ -575,10 +576,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)) {
@ -1420,6 +1426,7 @@ r_object(RFILE *p)
break;
case TYPE_DICT:
case TYPE_FROZENDICT:
v = PyDict_New();
R_REF(v);
if (v == NULL)
@ -1443,7 +1450,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;

View file

@ -769,6 +769,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

Can't render this file because it has a wrong number of fields in line 4.