Skip to content

Commit bde4379

Browse files
committed
gh-141510, PEP 814: Add built-in frozendict type
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.
1 parent e66f4a5 commit bde4379

File tree

16 files changed

+523
-117
lines changed

16 files changed

+523
-117
lines changed

Doc/c-api/dict.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,46 @@ Dictionary View Objects
490490
always succeeds.
491491
492492
493+
Frozen Dictionary Objects
494+
^^^^^^^^^^^^^^^^^^^^^^^^^
495+
496+
.. versionadded:: next
497+
498+
499+
.. c:function:: int PyAnyDict_Check(PyObject *p)
500+
501+
Return true if *p* is a dict object, a frozendict object, or an instance of
502+
a subtype of the dict or frozendict type.
503+
This function always succeeds.
504+
505+
506+
.. c:function:: int PyAnyDict_CheckExact(PyObject *p)
507+
508+
Return true if *p* is a dict object or a frozendict object, but not an
509+
instance of a subtype of the dict or frozendict type.
510+
This function always succeeds.
511+
512+
513+
.. c:function:: int PyFrozenDict_Check(PyObject *p)
514+
515+
Return true if *p* is a frozendict object or an instance of a subtype of the
516+
frozendict type.
517+
This function always succeeds.
518+
519+
520+
.. c:function:: int PyFrozenDict_CheckExact(PyObject *p)
521+
522+
Return true if *p* is a frozendict object, but not an instance of a subtype
523+
of the frozendict type.
524+
This function always succeeds.
525+
526+
527+
.. c:function:: PyObject* PyFrozenDict_New(PyObject *iterable)
528+
529+
Return a new frozendict from an iterable, or ``NULL`` on failure with an
530+
exception set.
531+
532+
493533
Ordered Dictionaries
494534
^^^^^^^^^^^^^^^^^^^^
495535

Doc/library/stdtypes.rst

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5305,8 +5305,8 @@ frozenset, a temporary one is created from *elem*.
53055305

53065306
.. _typesmapping:
53075307

5308-
Mapping Types --- :class:`dict`
5309-
===============================
5308+
Mapping Types --- :class:`dict`, :class:`frozendict`
5309+
====================================================
53105310

53115311
.. index::
53125312
pair: object; mapping
@@ -5317,8 +5317,9 @@ Mapping Types --- :class:`dict`
53175317
pair: built-in function; len
53185318

53195319
A :term:`mapping` object maps :term:`hashable` values to arbitrary objects.
5320-
Mappings are mutable objects. There is currently only one standard mapping
5321-
type, the :dfn:`dictionary`. (For other containers see the built-in
5320+
There are currently two standard mapping types, the :dfn:`dictionary` and
5321+
:class:`frozendict`.
5322+
(For other containers see the built-in
53225323
:class:`list`, :class:`set`, and :class:`tuple` classes, and the
53235324
:mod:`collections` module.)
53245325

@@ -5587,6 +5588,15 @@ can be used interchangeably to index the same dictionary entry.
55875588
.. versionchanged:: 3.8
55885589
Dictionaries are now reversible.
55895590

5591+
.. class:: frozendict(**kwargs)
5592+
frozendict(mapping, /, **kwargs)
5593+
frozendict(iterable, /, **kwargs)
5594+
5595+
Return a new frozen dictionary initialized from an optional positional
5596+
argument and a possibly empty set of keyword arguments.
5597+
5598+
.. versionadded:: next
5599+
55905600

55915601
.. seealso::
55925602
:class:`types.MappingProxyType` can be used to create a read-only view
@@ -6062,6 +6072,7 @@ list is non-exhaustive.
60626072
* :class:`list`
60636073
* :class:`dict`
60646074
* :class:`set`
6075+
* :class:`frozendict`
60656076
* :class:`frozenset`
60666077
* :class:`type`
60676078
* :class:`asyncio.Future`

Doc/whatsnew/3.15.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Summary -- Release highlights
6565
6666
.. PEP-sized items next.
6767
68+
* :pep:`814`: :ref:`Add frozendict built-in type
69+
<whatsnew315-pep814>`
6870
* :pep:`810`: :ref:`Explicit lazy imports for faster startup times
6971
<whatsnew315-pep810>`
7072
* :pep:`799`: :ref:`A dedicated profiling package for organizing Python
@@ -84,6 +86,20 @@ Summary -- Release highlights
8486
New features
8587
============
8688

89+
.. _whatsnew315-pep814:
90+
91+
:pep:`814`: Add frozendict built-in type
92+
----------------------------------------
93+
94+
A new public immutable type :class:`frozendict` is added to the :mod:`builtins`
95+
module. It is not a ``dict`` subclass but inherits directly from ``object``.
96+
97+
A ``frozendict`` can be hashed with ``hash(frozendict)`` if all keys and values
98+
can be hashed.
99+
100+
.. seealso:: :pep:`814` for the full specification and rationale.
101+
102+
87103
.. _whatsnew315-pep810:
88104

89105
:pep:`810`: Explicit lazy imports
@@ -1512,6 +1528,16 @@ C API changes
15121528
New features
15131529
------------
15141530

1531+
* Add the following functions for the new :class:`frozendict` type:
1532+
1533+
* :c:func:`PyAnyDict_Check`
1534+
* :c:func:`PyAnyDict_CheckExact`
1535+
* :c:func:`PyFrozenDict_Check`
1536+
* :c:func:`PyFrozenDict_CheckExact`
1537+
* :c:func:`PyFrozenDict_New`
1538+
1539+
(Contributed by Victor Stinner in :gh:`141510`.)
1540+
15151541
* Add :c:func:`PySys_GetAttr`, :c:func:`PySys_GetAttrString`,
15161542
:c:func:`PySys_GetOptionalAttr`, and :c:func:`PySys_GetOptionalAttrString`
15171543
functions as replacements for :c:func:`PySys_GetObject`.

Include/cpython/dictobject.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ typedef struct {
3232
PyDictValues *ma_values;
3333
} PyDictObject;
3434

35+
// frozendict
36+
PyAPI_DATA(PyTypeObject) PyFrozenDict_Type;
37+
#define PyFrozenDict_Check(op) PyObject_TypeCheck((op), &PyFrozenDict_Type)
38+
#define PyFrozenDict_CheckExact(op) Py_IS_TYPE((op), &PyFrozenDict_Type)
39+
40+
#define PyAnyDict_CheckExact(ob) \
41+
(PyDict_CheckExact(ob) || PyFrozenDict_CheckExact(ob))
42+
#define PyAnyDict_Check(ob) \
43+
(PyDict_Check(ob) || PyFrozenDict_Check(ob))
44+
3545
PyAPI_FUNC(PyObject *) _PyDict_GetItem_KnownHash(PyObject *mp, PyObject *key,
3646
Py_hash_t hash);
3747
// PyDict_GetItemStringRef() can be used instead
@@ -42,7 +52,7 @@ PyAPI_FUNC(PyObject *) PyDict_SetDefault(
4252
/* Get the number of items of a dictionary. */
4353
static inline Py_ssize_t PyDict_GET_SIZE(PyObject *op) {
4454
PyDictObject *mp;
45-
assert(PyDict_Check(op));
55+
assert(PyAnyDict_Check(op));
4656
mp = _Py_CAST(PyDictObject*, op);
4757
#ifdef Py_GIL_DISABLED
4858
return _Py_atomic_load_ssize_relaxed(&mp->ma_used);
@@ -93,3 +103,6 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);
93103
// Mark given dictionary as "watched" (callback will be called if it is modified)
94104
PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
95105
PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict);
106+
107+
// Create a frozendict. Create an empty dictionary if iterable is NULL.
108+
PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable);

Include/internal/pycore_dict.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,15 @@ _Py_DECREF_BUILTINS(PyObject *op)
408408
}
409409
#endif
410410

411+
/* frozendict */
412+
typedef struct {
413+
PyDictObject ob_base;
414+
Py_hash_t ma_hash;
415+
} PyFrozenDictObject;
416+
417+
#define _PyFrozenDictObject_CAST(op) \
418+
(assert(PyFrozenDict_Check(op)), _Py_CAST(PyFrozenDictObject*, (op)))
419+
411420
#ifdef __cplusplus
412421
}
413422
#endif

Include/internal/pycore_typeobject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extern "C" {
2626
#define _Py_TYPE_VERSION_BYTEARRAY 9
2727
#define _Py_TYPE_VERSION_BYTES 10
2828
#define _Py_TYPE_VERSION_COMPLEX 11
29+
#define _Py_TYPE_VERSION_FROZENDICT 12
2930

3031
#define _Py_TYPE_VERSION_NEXT 16
3132

Lib/_collections_abc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ def __eq__(self, other):
823823

824824
__reversed__ = None
825825

826+
Mapping.register(frozendict)
826827
Mapping.register(mappingproxy)
827828
Mapping.register(framelocalsproxy)
828829

Lib/test/mapping_tests.py

Lines changed: 68 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from test import support
55

66

7-
class BasicTestMappingProtocol(unittest.TestCase):
7+
class BasicTestImmutableMappingProtocol(unittest.TestCase):
88
# This base class can be used to check that an object conforms to the
99
# mapping protocol
1010

@@ -22,10 +22,7 @@ def _empty_mapping(self):
2222
def _full_mapping(self, data):
2323
"""Return a mapping object with the value contained in data
2424
dictionary"""
25-
x = self._empty_mapping()
26-
for key, value in data.items():
27-
x[key] = value
28-
return x
25+
return self.type2test(data)
2926

3027
def __init__(self, *args, **kw):
3128
unittest.TestCase.__init__(self, *args, **kw)
@@ -88,6 +85,72 @@ def check_iterandlist(iter, lst, ref):
8885
self.assertEqual(d.get(knownkey, knownvalue), knownvalue)
8986
self.assertNotIn(knownkey, d)
9087

88+
def test_constructor(self):
89+
self.assertEqual(self._empty_mapping(), self._empty_mapping())
90+
91+
def test_bool(self):
92+
self.assertTrue(not self._empty_mapping())
93+
self.assertTrue(self.reference)
94+
self.assertTrue(bool(self._empty_mapping()) is False)
95+
self.assertTrue(bool(self.reference) is True)
96+
97+
def test_keys(self):
98+
d = self._empty_mapping()
99+
self.assertEqual(list(d.keys()), [])
100+
d = self.reference
101+
self.assertIn(list(self.inmapping.keys())[0], d.keys())
102+
self.assertNotIn(list(self.other.keys())[0], d.keys())
103+
self.assertRaises(TypeError, d.keys, None)
104+
105+
def test_values(self):
106+
d = self._empty_mapping()
107+
self.assertEqual(list(d.values()), [])
108+
109+
self.assertRaises(TypeError, d.values, None)
110+
111+
def test_items(self):
112+
d = self._empty_mapping()
113+
self.assertEqual(list(d.items()), [])
114+
115+
self.assertRaises(TypeError, d.items, None)
116+
117+
def test_len(self):
118+
d = self._empty_mapping()
119+
self.assertEqual(len(d), 0)
120+
121+
def test_getitem(self):
122+
d = self.reference
123+
self.assertEqual(d[list(self.inmapping.keys())[0]],
124+
list(self.inmapping.values())[0])
125+
126+
self.assertRaises(TypeError, d.__getitem__)
127+
128+
# no test_fromkeys or test_copy as both os.environ and selves don't support it
129+
130+
def test_get(self):
131+
d = self._empty_mapping()
132+
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
133+
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
134+
d = self.reference
135+
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
136+
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
137+
self.assertEqual(d.get(list(self.inmapping.keys())[0]),
138+
list(self.inmapping.values())[0])
139+
self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
140+
list(self.inmapping.values())[0])
141+
self.assertRaises(TypeError, d.get)
142+
self.assertRaises(TypeError, d.get, None, None, None)
143+
144+
145+
class BasicTestMappingProtocol(BasicTestImmutableMappingProtocol):
146+
def _full_mapping(self, data):
147+
"""Return a mapping object with the value contained in data
148+
dictionary"""
149+
x = self._empty_mapping()
150+
for key, value in data.items():
151+
x[key] = value
152+
return x
153+
91154
def test_write(self):
92155
# Test for write operations on mapping
93156
p = self._empty_mapping()
@@ -130,46 +193,6 @@ def test_write(self):
130193
p=self._empty_mapping()
131194
self.assertRaises(KeyError, p.popitem)
132195

133-
def test_constructor(self):
134-
self.assertEqual(self._empty_mapping(), self._empty_mapping())
135-
136-
def test_bool(self):
137-
self.assertTrue(not self._empty_mapping())
138-
self.assertTrue(self.reference)
139-
self.assertTrue(bool(self._empty_mapping()) is False)
140-
self.assertTrue(bool(self.reference) is True)
141-
142-
def test_keys(self):
143-
d = self._empty_mapping()
144-
self.assertEqual(list(d.keys()), [])
145-
d = self.reference
146-
self.assertIn(list(self.inmapping.keys())[0], d.keys())
147-
self.assertNotIn(list(self.other.keys())[0], d.keys())
148-
self.assertRaises(TypeError, d.keys, None)
149-
150-
def test_values(self):
151-
d = self._empty_mapping()
152-
self.assertEqual(list(d.values()), [])
153-
154-
self.assertRaises(TypeError, d.values, None)
155-
156-
def test_items(self):
157-
d = self._empty_mapping()
158-
self.assertEqual(list(d.items()), [])
159-
160-
self.assertRaises(TypeError, d.items, None)
161-
162-
def test_len(self):
163-
d = self._empty_mapping()
164-
self.assertEqual(len(d), 0)
165-
166-
def test_getitem(self):
167-
d = self.reference
168-
self.assertEqual(d[list(self.inmapping.keys())[0]],
169-
list(self.inmapping.values())[0])
170-
171-
self.assertRaises(TypeError, d.__getitem__)
172-
173196
def test_update(self):
174197
# mapping argument
175198
d = self._empty_mapping()
@@ -265,22 +288,6 @@ def __next__(self):
265288

266289
self.assertRaises(ValueError, d.update, [(1, 2, 3)])
267290

268-
# no test_fromkeys or test_copy as both os.environ and selves don't support it
269-
270-
def test_get(self):
271-
d = self._empty_mapping()
272-
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
273-
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
274-
d = self.reference
275-
self.assertTrue(d.get(list(self.other.keys())[0]) is None)
276-
self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
277-
self.assertEqual(d.get(list(self.inmapping.keys())[0]),
278-
list(self.inmapping.values())[0])
279-
self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
280-
list(self.inmapping.values())[0])
281-
self.assertRaises(TypeError, d.get)
282-
self.assertRaises(TypeError, d.get, None, None, None)
283-
284291
def test_setdefault(self):
285292
d = self._empty_mapping()
286293
self.assertRaises(TypeError, d.setdefault)

0 commit comments

Comments
 (0)