Skip to content

Commit 792bd2a

Browse files
committed
Merge branch 'main' into frozendict_dict_contains
2 parents 01cf357 + 6940c1d commit 792bd2a

7 files changed

Lines changed: 255 additions & 31 deletions

File tree

Lib/copy.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,17 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy):
204204
d[dict] = _deepcopy_dict
205205

206206
def _deepcopy_frozendict(x, memo, deepcopy=deepcopy):
207-
y = _deepcopy_dict(x, memo, deepcopy)
207+
y = {}
208+
for key, value in x.items():
209+
y[deepcopy(key, memo)] = deepcopy(value, memo)
210+
211+
# We're not going to put the frozendict in the memo, but it's still
212+
# important we check for it, in case the frozendict contains recursive
213+
# mutable structures.
214+
try:
215+
return memo[id(x)]
216+
except KeyError:
217+
pass
208218
return frozendict(y)
209219
d[frozendict] = _deepcopy_frozendict
210220

Lib/pprint.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,20 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level):
235235

236236
_dispatch[dict.__repr__] = _pprint_dict
237237

238+
def _pprint_frozendict(self, object, stream, indent, allowance, context, level):
239+
write = stream.write
240+
cls = object.__class__
241+
stream.write(cls.__name__ + '(')
242+
length = len(object)
243+
if length:
244+
self._pprint_dict(object, stream,
245+
indent + len(cls.__name__) + 1,
246+
allowance + 1,
247+
context, level)
248+
write(')')
249+
250+
_dispatch[frozendict.__repr__] = _pprint_frozendict
251+
238252
def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level):
239253
if not len(object):
240254
stream.write(repr(object))
@@ -623,12 +637,21 @@ def _safe_repr(self, object, context, maxlevels, level):
623637
else:
624638
return repr(object), True, False
625639

626-
if issubclass(typ, dict) and r is dict.__repr__:
640+
if ((issubclass(typ, dict) and r is dict.__repr__)
641+
or (issubclass(typ, frozendict) and r is frozendict.__repr__)):
642+
is_frozendict = issubclass(typ, frozendict)
627643
if not object:
628-
return "{}", True, False
644+
if is_frozendict:
645+
rep = f"{object.__class__.__name__}()"
646+
else:
647+
rep = "{}"
648+
return rep, True, False
629649
objid = id(object)
630650
if maxlevels and level >= maxlevels:
631-
return "{...}", False, objid in context
651+
rep = "{...}"
652+
if is_frozendict:
653+
rep = f"{object.__class__.__name__}({rep})"
654+
return rep, False, objid in context
632655
if objid in context:
633656
return _recursion(object), False, True
634657
context[objid] = 1
@@ -651,7 +674,10 @@ def _safe_repr(self, object, context, maxlevels, level):
651674
if krecur or vrecur:
652675
recursive = True
653676
del context[objid]
654-
return "{%s}" % ", ".join(components), readable, recursive
677+
rep = "{%s}" % ", ".join(components)
678+
if is_frozendict:
679+
rep = f"{object.__class__.__name__}({rep})"
680+
return rep, readable, recursive
655681

656682
if (issubclass(typ, list) and r is list.__repr__) or \
657683
(issubclass(typ, tuple) and r is tuple.__repr__):

Lib/test/test_capi/test_dict.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ def gen():
2626
yield 'c'
2727

2828

29+
class FrozenDictSubclass(frozendict):
30+
pass
31+
32+
33+
DICT_TYPES = (dict, DictSubclass, OrderedDict)
34+
FROZENDICT_TYPES = (frozendict, FrozenDictSubclass)
35+
ANYDICT_TYPES = DICT_TYPES + FROZENDICT_TYPES
36+
MAPPING_TYPES = (UserDict,)
37+
NOT_FROZENDICT_TYPES = DICT_TYPES + MAPPING_TYPES
38+
NOT_ANYDICT_TYPES = MAPPING_TYPES
39+
OTHER_TYPES = (lambda: [1], lambda: 42, object) # (list, int, object)
40+
41+
2942
class CAPITest(unittest.TestCase):
3043

3144
def test_dict_check(self):
@@ -410,6 +423,7 @@ def test_dict_next(self):
410423
# CRASHES dict_next(NULL, 0)
411424

412425
def test_dict_update(self):
426+
# Test PyDict_Update()
413427
update = _testlimitedcapi.dict_update
414428
for cls1 in dict, DictSubclass:
415429
for cls2 in dict, DictSubclass, UserDict:
@@ -420,11 +434,13 @@ def test_dict_update(self):
420434
self.assertRaises(AttributeError, update, {}, [])
421435
self.assertRaises(AttributeError, update, {}, 42)
422436
self.assertRaises(SystemError, update, UserDict(), {})
437+
self.assertRaises(SystemError, update, frozendict(), {})
423438
self.assertRaises(SystemError, update, 42, {})
424439
self.assertRaises(SystemError, update, {}, NULL)
425440
self.assertRaises(SystemError, update, NULL, {})
426441

427442
def test_dict_merge(self):
443+
# Test PyDict_Merge()
428444
merge = _testlimitedcapi.dict_merge
429445
for cls1 in dict, DictSubclass:
430446
for cls2 in dict, DictSubclass, UserDict:
@@ -438,11 +454,13 @@ def test_dict_merge(self):
438454
self.assertRaises(AttributeError, merge, {}, [], 0)
439455
self.assertRaises(AttributeError, merge, {}, 42, 0)
440456
self.assertRaises(SystemError, merge, UserDict(), {}, 0)
457+
self.assertRaises(SystemError, merge, frozendict(), {}, 0)
441458
self.assertRaises(SystemError, merge, 42, {}, 0)
442459
self.assertRaises(SystemError, merge, {}, NULL, 0)
443460
self.assertRaises(SystemError, merge, NULL, {}, 0)
444461

445462
def test_dict_mergefromseq2(self):
463+
# Test PyDict_MergeFromSeq2()
446464
mergefromseq2 = _testlimitedcapi.dict_mergefromseq2
447465
for cls1 in dict, DictSubclass:
448466
for cls2 in list, iter:
@@ -457,8 +475,8 @@ def test_dict_mergefromseq2(self):
457475
self.assertRaises(ValueError, mergefromseq2, {}, [(1, 2, 3)], 0)
458476
self.assertRaises(TypeError, mergefromseq2, {}, [1], 0)
459477
self.assertRaises(TypeError, mergefromseq2, {}, 42, 0)
460-
# CRASHES mergefromseq2(UserDict(), [], 0)
461-
# CRASHES mergefromseq2(42, [], 0)
478+
self.assertRaises(SystemError, mergefromseq2, UserDict(), [], 0)
479+
self.assertRaises(SystemError, mergefromseq2, 42, [], 0)
462480
# CRASHES mergefromseq2({}, NULL, 0)
463481
# CRASHES mergefromseq2(NULL, {}, 0)
464482

@@ -549,6 +567,61 @@ def test_dict_popstring(self):
549567
# CRASHES dict_popstring({}, NULL)
550568
# CRASHES dict_popstring({"a": 1}, NULL)
551569

570+
def test_frozendict_check(self):
571+
# Test PyFrozenDict_Check()
572+
check = _testcapi.frozendict_check
573+
for dict_type in FROZENDICT_TYPES:
574+
self.assertTrue(check(dict_type(x=1)))
575+
for dict_type in NOT_FROZENDICT_TYPES + OTHER_TYPES:
576+
self.assertFalse(check(dict_type()))
577+
# CRASHES check(NULL)
578+
579+
def test_frozendict_checkexact(self):
580+
# Test PyFrozenDict_CheckExact()
581+
check = _testcapi.frozendict_checkexact
582+
for dict_type in FROZENDICT_TYPES:
583+
self.assertEqual(check(dict_type(x=1)), dict_type == frozendict)
584+
for dict_type in NOT_FROZENDICT_TYPES + OTHER_TYPES:
585+
self.assertFalse(check(dict_type()))
586+
# CRASHES check(NULL)
587+
588+
def test_anydict_check(self):
589+
# Test PyAnyDict_Check()
590+
check = _testcapi.anydict_check
591+
for dict_type in ANYDICT_TYPES:
592+
self.assertTrue(check(dict_type({1: 2})))
593+
for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
594+
self.assertFalse(check(test_type()))
595+
# CRASHES check(NULL)
596+
597+
def test_anydict_checkexact(self):
598+
# Test PyAnyDict_CheckExact()
599+
check = _testcapi.anydict_checkexact
600+
for dict_type in ANYDICT_TYPES:
601+
self.assertEqual(check(dict_type(x=1)),
602+
dict_type in (dict, frozendict))
603+
for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
604+
self.assertFalse(check(test_type()))
605+
# CRASHES check(NULL)
606+
607+
def test_frozendict_new(self):
608+
# Test PyFrozenDict_New()
609+
frozendict_new = _testcapi.frozendict_new
610+
611+
for dict_type in ANYDICT_TYPES:
612+
dct = frozendict_new(dict_type({'x': 1}))
613+
self.assertEqual(dct, frozendict(x=1))
614+
self.assertIs(type(dct), frozendict)
615+
616+
dct = frozendict_new([('x', 1), ('y', 2)])
617+
self.assertEqual(dct, frozendict(x=1, y=2))
618+
self.assertIs(type(dct), frozendict)
619+
620+
# PyFrozenDict_New(NULL) creates an empty dictionary
621+
dct = frozendict_new(NULL)
622+
self.assertEqual(dct, frozendict())
623+
self.assertIs(type(dct), frozendict)
624+
552625

553626
if __name__ == "__main__":
554627
unittest.main()

Lib/test/test_copy.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,23 @@ def test_deepcopy_frozendict(self):
432432
self.assertIsNot(x, y)
433433
self.assertIsNot(x["foo"], y["foo"])
434434

435+
# recursive frozendict
436+
x = frozendict(foo=[])
437+
x['foo'].append(x)
438+
y = copy.deepcopy(x)
439+
self.assertEqual(y.keys(), x.keys())
440+
self.assertIsNot(x, y)
441+
self.assertIsNot(x["foo"], y["foo"])
442+
self.assertIs(y['foo'][0], y)
443+
444+
x = frozendict(foo=[])
445+
x['foo'].append(x)
446+
x = x['foo']
447+
y = copy.deepcopy(x)
448+
self.assertIsNot(x, y)
449+
self.assertIsNot(x[0], y[0])
450+
self.assertIs(y[0]['foo'], y)
451+
435452
@support.skip_emscripten_stack_overflow()
436453
@support.skip_wasi_stack_overflow()
437454
def test_deepcopy_reflexive_dict(self):

Lib/test/test_pprint.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ class dict3(dict):
6767
def __repr__(self):
6868
return dict.__repr__(self)
6969

70+
class frozendict2(frozendict):
71+
pass
72+
73+
class frozendict3(frozendict):
74+
def __repr__(self):
75+
return frozendict.__repr__(self)
76+
7077
class dict_custom_repr(dict):
7178
def __repr__(self):
7279
return '*'*len(dict.__repr__(self))
@@ -254,18 +261,22 @@ def test_same_as_repr(self):
254261
set(), set2(), set3(),
255262
frozenset(), frozenset2(), frozenset3(),
256263
{}, dict2(), dict3(),
264+
frozendict(), frozendict2(), frozendict3(),
257265
{}.keys(), {}.values(), {}.items(),
258266
MappingView({}), KeysView({}), ItemsView({}), ValuesView({}),
259267
self.assertTrue, pprint,
260268
-6, -6, -6-6j, -1.5, "x", b"x", bytearray(b"x"),
261269
(3,), [3], {3: 6},
262-
(1,2), [3,4], {5: 6},
270+
(1,2), [3,4],
263271
tuple2((1,2)), tuple3((1,2)), tuple3(range(100)),
264272
[3,4], list2([3,4]), list3([3,4]), list3(range(100)),
265273
set({7}), set2({7}), set3({7}),
266274
frozenset({8}), frozenset2({8}), frozenset3({8}),
267-
dict2({5: 6}), dict3({5: 6}),
275+
{5: 6}, dict2({5: 6}), dict3({5: 6}),
276+
frozendict({5: 6}), frozendict2({5: 6}), frozendict3({5: 6}),
268277
{5: 6}.keys(), {5: 6}.values(), {5: 6}.items(),
278+
frozendict({5: 6}).keys(), frozendict({5: 6}).values(),
279+
frozendict({5: 6}).items(),
269280
MappingView({5: 6}), KeysView({5: 6}),
270281
ItemsView({5: 6}), ValuesView({5: 6}),
271282
range(10, -11, -1),
@@ -330,20 +341,45 @@ def test_basic_line_wrap(self):
330341
for type in [dict, dict2]:
331342
self.assertEqual(pprint.pformat(type(o)), exp)
332343

344+
exp = """\
345+
frozendict({'RPM_cal': 0,
346+
'RPM_cal2': 48059,
347+
'Speed_cal': 0,
348+
'controldesk_runtime_us': 0,
349+
'main_code_runtime_us': 0,
350+
'read_io_runtime_us': 0,
351+
'write_io_runtime_us': 43690})"""
352+
self.assertEqual(pprint.pformat(frozendict(o)), exp)
353+
exp = """\
354+
frozendict2({'RPM_cal': 0,
355+
'RPM_cal2': 48059,
356+
'Speed_cal': 0,
357+
'controldesk_runtime_us': 0,
358+
'main_code_runtime_us': 0,
359+
'read_io_runtime_us': 0,
360+
'write_io_runtime_us': 43690})"""
361+
self.assertEqual(pprint.pformat(frozendict2(o)), exp)
362+
333363
o = range(100)
334364
exp = 'dict_keys([%s])' % ',\n '.join(map(str, o))
335365
keys = dict.fromkeys(o).keys()
336366
self.assertEqual(pprint.pformat(keys), exp)
367+
keys = frozendict.fromkeys(o).keys()
368+
self.assertEqual(pprint.pformat(keys), exp)
337369

338370
o = range(100)
339371
exp = 'dict_values([%s])' % ',\n '.join(map(str, o))
340372
values = {v: v for v in o}.values()
341373
self.assertEqual(pprint.pformat(values), exp)
374+
values = frozendict({v: v for v in o}).values()
375+
self.assertEqual(pprint.pformat(values), exp)
342376

343377
o = range(100)
344378
exp = 'dict_items([%s])' % ',\n '.join("(%s, %s)" % (i, i) for i in o)
345379
items = {v: v for v in o}.items()
346380
self.assertEqual(pprint.pformat(items), exp)
381+
items = frozendict({v: v for v in o}).items()
382+
self.assertEqual(pprint.pformat(items), exp)
347383

348384
o = range(100)
349385
exp = 'odict_keys([%s])' % ',\n '.join(map(str, o))

Modules/_testcapi/dict.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,43 @@ test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored))
258258
}
259259

260260

261+
static PyObject *
262+
frozendict_check(PyObject *self, PyObject *obj)
263+
{
264+
NULLABLE(obj);
265+
return PyLong_FromLong(PyFrozenDict_Check(obj));
266+
}
267+
268+
static PyObject *
269+
frozendict_checkexact(PyObject *self, PyObject *obj)
270+
{
271+
NULLABLE(obj);
272+
return PyLong_FromLong(PyFrozenDict_CheckExact(obj));
273+
}
274+
275+
static PyObject *
276+
anydict_check(PyObject *self, PyObject *obj)
277+
{
278+
NULLABLE(obj);
279+
return PyLong_FromLong(PyAnyDict_Check(obj));
280+
}
281+
282+
static PyObject *
283+
anydict_checkexact(PyObject *self, PyObject *obj)
284+
{
285+
NULLABLE(obj);
286+
return PyLong_FromLong(PyAnyDict_CheckExact(obj));
287+
}
288+
289+
290+
static PyObject *
291+
frozendict_new(PyObject *self, PyObject *obj)
292+
{
293+
NULLABLE(obj);
294+
return PyFrozenDict_New(obj);
295+
}
296+
297+
261298
static PyMethodDef test_methods[] = {
262299
{"dict_containsstring", dict_containsstring, METH_VARARGS},
263300
{"dict_getitemref", dict_getitemref, METH_VARARGS},
@@ -269,6 +306,11 @@ static PyMethodDef test_methods[] = {
269306
{"dict_popstring", dict_popstring, METH_VARARGS},
270307
{"dict_popstring_null", dict_popstring_null, METH_VARARGS},
271308
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
309+
{"frozendict_check", frozendict_check, METH_O},
310+
{"frozendict_checkexact", frozendict_checkexact, METH_O},
311+
{"anydict_check", anydict_check, METH_O},
312+
{"anydict_checkexact", anydict_checkexact, METH_O},
313+
{"frozendict_new", frozendict_new, METH_O},
272314
{NULL},
273315
};
274316

0 commit comments

Comments
 (0)