From f7835caceaebfa97c88bb5d4b58b11370c005b60 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Thu, 16 Dec 2021 06:42:53 -0300 Subject: [PATCH 01/28] Teach pprint about dict views with PrettyPrinter._pprint_dict_view and ._pprint_dict_items_view. --- Lib/pprint.py | 28 ++++++++++++++++++++ Lib/test/test_pprint.py | 58 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/Lib/pprint.py b/Lib/pprint.py index 575688d8eb6f4a..edfb0c701feb78 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -233,6 +233,34 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict + def _pprint_dict_view(self, object, stream, indent, allowance, context, level, items=False): + key = _safe_tuple if items else _safe_key + write = stream.write + write(object.__class__.__name__ + '([') + if self._indent_per_level > 1: + write((self._indent_per_level - 1) * ' ') + length = len(object) + if length: + if self._sort_dicts: + entries = sorted(object, key=key) + else: + entries = object + self._format_items(entries, stream, indent, allowance + 1, + context, level) + write('])') + + dict_keys_view = type({}.keys()) + _dispatch[dict_keys_view.__repr__] = _pprint_dict_view + + dict_values_view = type({}.values()) + _dispatch[dict_values_view.__repr__] = _pprint_dict_view + + def _pprint_dict_items_view(self, object, stream, indent, allowance, context, level): + self._pprint_dict_view(object, stream, indent, allowance, context, level, True) + + dict_items_view = type({}.items()) + _dispatch[dict_items_view.__repr__] = _pprint_dict_items_view + def _pprint_list(self, object, stream, indent, allowance, context, level): stream.write('[') self._format_items(object, stream, indent, allowance + 1, diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index c7b9893943471f..008ccd7167b75f 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -231,6 +231,7 @@ def test_same_as_repr(self): set(), set2(), set3(), frozenset(), frozenset2(), frozenset3(), {}, dict2(), dict3(), + {}.keys(), {}.values(), {}.items(), self.assertTrue, pprint, -6, -6, -6-6j, -1.5, "x", b"x", bytearray(b"x"), (3,), [3], {3: 6}, @@ -240,6 +241,7 @@ def test_same_as_repr(self): set({7}), set2({7}), set3({7}), frozenset({8}), frozenset2({8}), frozenset3({8}), dict2({5: 6}), dict3({5: 6}), + {5: 6}.keys(), {5: 6}.values(), {5: 6}.items(), range(10, -11, -1), True, False, None, ..., ): @@ -296,6 +298,36 @@ def test_basic_line_wrap(self): for type in [dict, dict2]: self.assertEqual(pprint.pformat(type(o)), exp) + o = range(100) + exp = 'dict_keys([%s])' % ',\n '.join(map(str, o)) + keys = dict.fromkeys(o).keys() + self.assertEqual(pprint.pformat(keys), exp) + + o = range(100) + exp = 'dict_values([%s])' % ',\n '.join(map(str, o)) + values = {v: v for v in o}.values() + self.assertEqual(pprint.pformat(values), exp) + + o = range(100) + exp = 'dict_items([%s])' % ',\n '.join("(%s, %s)" % (i, i) for i in o) + items = {v: v for v in o}.items() + self.assertEqual(pprint.pformat(items), exp) + + o = range(100) + exp = 'odict_keys([%s])' % ',\n '.join(map(str, o)) + keys = collections.OrderedDict.fromkeys(o).keys() + self.assertEqual(pprint.pformat(keys), exp) + + o = range(100) + exp = 'odict_values([%s])' % ',\n '.join(map(str, o)) + values = collections.OrderedDict({v: v for v in o}).values() + self.assertEqual(pprint.pformat(values), exp) + + o = range(100) + exp = 'odict_items([%s])' % ',\n '.join("(%s, %s)" % (i, i) for i in o) + items = collections.OrderedDict({v: v for v in o}).items() + self.assertEqual(pprint.pformat(items), exp) + o = range(100) exp = '[%s]' % ',\n '.join(map(str, o)) for type in [list, list2]: @@ -447,6 +479,28 @@ def test_mapping_proxy(self): ('lazy', 7), ('dog', 8)]))""") + def test_dict_views(self): + for dict_class in (dict, collections.OrderedDict): + empty = dict_class() + short = dict_class(zip('abcde', 'abcde')) + long = dict_class((chr(x), chr(x)) for x in range(65, 91)) + prefix = "dict" if dict_class is dict else "odict" + for d in empty, short, long: + is_short = len(d) < 6 + joiner = ", " if is_short else ",\n " + k = d.keys() + v = d.values() + i = d.items() + self.assertEqual(pprint.pformat(k, sort_dicts=True), + prefix + "_keys([%s])" % + joiner.join(repr(key) for key in sorted(k))) + self.assertEqual(pprint.pformat(v, sort_dicts=True), + prefix + "_values([%s])" % + joiner.join(repr(val) for val in sorted(v))) + self.assertEqual(pprint.pformat(i, sort_dicts=True), + prefix + "_items([%s])" % + joiner.join(repr(item) for item in sorted(i))) + def test_empty_simple_namespace(self): ns = types.SimpleNamespace() formatted = pprint.pformat(ns) @@ -860,6 +914,10 @@ def test_sort_unorderable_values(self): 'frozenset({' + ','.join(map(repr, skeys)) + '})') self.assertEqual(clean(pprint.pformat(dict.fromkeys(keys))), '{' + ','.join('%r:None' % k for k in skeys) + '}') + self.assertEqual(clean(pprint.pformat(dict.fromkeys(keys).keys())), + 'dict_keys([' + ','.join('%r' % k for k in skeys) + '])') + self.assertEqual(clean(pprint.pformat(dict.fromkeys(keys).items())), + 'dict_items([' + ','.join('(%r,None)' % k for k in skeys) + '])') # Issue 10017: TypeError on user-defined types as dict keys. self.assertEqual(pprint.pformat({Unorderable: 0, 1: 0}), From 9500a7491d46905491a9b856d8b85258f7d99731 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Thu, 16 Dec 2021 21:04:26 -0300 Subject: [PATCH 02/28] Use _private names for _dict_*_view attributes of PrettyPrinter. --- Lib/pprint.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index edfb0c701feb78..cb5cd1d0fe3ac4 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -249,17 +249,17 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level, i context, level) write('])') - dict_keys_view = type({}.keys()) - _dispatch[dict_keys_view.__repr__] = _pprint_dict_view + _dict_keys_view = type({}.keys()) + _dispatch[_dict_keys_view.__repr__] = _pprint_dict_view - dict_values_view = type({}.values()) - _dispatch[dict_values_view.__repr__] = _pprint_dict_view + _dict_values_view = type({}.values()) + _dispatch[_dict_values_view.__repr__] = _pprint_dict_view def _pprint_dict_items_view(self, object, stream, indent, allowance, context, level): self._pprint_dict_view(object, stream, indent, allowance, context, level, True) - dict_items_view = type({}.items()) - _dispatch[dict_items_view.__repr__] = _pprint_dict_items_view + _dict_items_view = type({}.items()) + _dispatch[_dict_items_view.__repr__] = _pprint_dict_items_view def _pprint_list(self, object, stream, indent, allowance, context, level): stream.write('[') From 653cdea1231ff5f9894c4bd18592c45e138d5947 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sat, 18 Dec 2021 09:16:14 -0300 Subject: [PATCH 03/28] Use explicit 'items' keyword when calling _pprint_dict_view from _pprint_dict_items_view. --- Lib/pprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index cb5cd1d0fe3ac4..30136314d37347 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -256,7 +256,7 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level, i _dispatch[_dict_values_view.__repr__] = _pprint_dict_view def _pprint_dict_items_view(self, object, stream, indent, allowance, context, level): - self._pprint_dict_view(object, stream, indent, allowance, context, level, True) + self._pprint_dict_view(object, stream, indent, allowance, context, level, items=True) _dict_items_view = type({}.items()) _dispatch[_dict_items_view.__repr__] = _pprint_dict_items_view From 884f224b5475a791cfbf349c2ebfb99a77cf25bc Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 18 Dec 2021 12:46:21 +0000 Subject: [PATCH 04/28] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst diff --git a/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst b/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst new file mode 100644 index 00000000000000..6ad3d03545fac0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst @@ -0,0 +1 @@ +:mod:`pprint` can now pretty-print dict views. \ No newline at end of file From 48be1e9408f23dfd1f5cfc4eaf8d78d9583b39a1 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Tue, 18 Jan 2022 22:33:27 -0300 Subject: [PATCH 05/28] Properly indent code. --- Lib/pprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 30136314d37347..03db921949124d 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -246,7 +246,7 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level, i else: entries = object self._format_items(entries, stream, indent, allowance + 1, - context, level) + context, level) write('])') _dict_keys_view = type({}.keys()) From 974549119a2901caa0c45e8a9c625321e6a14478 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Wed, 19 Jan 2022 00:31:58 -0300 Subject: [PATCH 06/28] WIP: Improve tests and make short views sortable. --- Lib/pprint.py | 12 +++++++++++ Lib/test/test_pprint.py | 45 +++++++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 03db921949124d..5f6ceb89f58dda 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -620,6 +620,18 @@ def _safe_repr(self, object, context, maxlevels, level): del context[objid] return "{%s}" % ", ".join(components), readable, recursive + views = self._dict_keys_view, self._dict_values_view, self._dict_items_view + view_reprs = {cls.__repr__ for cls in views} + if issubclass(typ, views) and r in view_reprs: + key = _safe_key + if isinstance(typ, self._dict_items_view): + key = _safe_tuple + if self._sort_dicts: + object = sorted(object, key=key) + format = typ.__name__ + '([%s])' + # TODO: Figure out whether we need to handle recursion here + return format % ', '.join(repr(x) for x in object), True, False + if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__): if issubclass(typ, list): diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 008ccd7167b75f..953ff8ba2d01f9 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -482,24 +482,35 @@ def test_mapping_proxy(self): def test_dict_views(self): for dict_class in (dict, collections.OrderedDict): empty = dict_class() - short = dict_class(zip('abcde', 'abcde')) - long = dict_class((chr(x), chr(x)) for x in range(65, 91)) + short = dict_class(zip('edcba', 'edcba')) + long = dict_class((chr(x), chr(x)) for x in range(90, 64, -1)) + lengths = {"empty": empty, "short": short, "long": long} prefix = "dict" if dict_class is dict else "odict" - for d in empty, short, long: - is_short = len(d) < 6 - joiner = ", " if is_short else ",\n " - k = d.keys() - v = d.values() - i = d.items() - self.assertEqual(pprint.pformat(k, sort_dicts=True), - prefix + "_keys([%s])" % - joiner.join(repr(key) for key in sorted(k))) - self.assertEqual(pprint.pformat(v, sort_dicts=True), - prefix + "_values([%s])" % - joiner.join(repr(val) for val in sorted(v))) - self.assertEqual(pprint.pformat(i, sort_dicts=True), - prefix + "_items([%s])" % - joiner.join(repr(item) for item in sorted(i))) + for name, d in lengths.items(): + with self.subTest(lenght=name, prefix=prefix): + is_short = len(d) < 6 + joiner = ", " if is_short else ",\n " + k = d.keys() + v = d.values() + i = d.items() + self.assertEqual(pprint.pformat(k, sort_dicts=True), + prefix + "_keys([%s])" % + joiner.join(repr(key) for key in sorted(k))) + self.assertEqual(pprint.pformat(v, sort_dicts=True), + prefix + "_values([%s])" % + joiner.join(repr(val) for val in sorted(v))) + self.assertEqual(pprint.pformat(i, sort_dicts=True), + prefix + "_items([%s])" % + joiner.join(repr(item) for item in sorted(i))) + self.assertEqual(pprint.pformat(k, sort_dicts=False), + prefix + "_keys([%s])" % + joiner.join(repr(key) for key in k)) + self.assertEqual(pprint.pformat(v, sort_dicts=False), + prefix + "_values([%s])" % + joiner.join(repr(val) for val in v)) + self.assertEqual(pprint.pformat(i, sort_dicts=False), + prefix + "_items([%s])" % + joiner.join(repr(item) for item in i)) def test_empty_simple_namespace(self): ns = types.SimpleNamespace() From 7edb29d3ee161a662585ac8b1353f6612d0cb6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric?= Date: Sun, 24 Apr 2022 12:50:55 -0400 Subject: [PATCH 07/28] fix typo --- Lib/test/test_pprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 953ff8ba2d01f9..23151bce283fc8 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -487,7 +487,7 @@ def test_dict_views(self): lengths = {"empty": empty, "short": short, "long": long} prefix = "dict" if dict_class is dict else "odict" for name, d in lengths.items(): - with self.subTest(lenght=name, prefix=prefix): + with self.subTest(length=name, prefix=prefix): is_short = len(d) < 6 joiner = ", " if is_short else ",\n " k = d.keys() From 15dd2663ffd55f8f84aead2c0fd7568c1b405a8e Mon Sep 17 00:00:00 2001 From: Oleg Iarygin Date: Sat, 4 Mar 2023 18:08:56 +0400 Subject: [PATCH 08/28] Address the failing Docs check --- .../next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst b/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst index 6ad3d03545fac0..bcafa64d2638dc 100644 --- a/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst +++ b/Misc/NEWS.d/next/Library/2021-12-18-12-46-20.bpo-45959.vPlr3P.rst @@ -1 +1 @@ -:mod:`pprint` can now pretty-print dict views. \ No newline at end of file +:mod:`pprint` can now pretty-print dict views. From 67e3f14c56c3b4cd0c39cd4edf7f8f0bb3d6a332 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 00:48:26 -0300 Subject: [PATCH 09/28] Add tests for collections.abc.[Keys|Items|Mapping|Values]View support in pprint. --- Lib/test/test_pprint.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 893edcb9407a11..a68cd5e90b7ba7 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -11,6 +11,7 @@ import test.support import types import unittest +from collections.abc import KeysView, ItemsView, MappingView, ValuesView # list, tuple and dict subclasses that do or don't overwrite __repr__ class list2(list): @@ -512,6 +513,37 @@ def test_dict_views(self): prefix + "_items([%s])" % joiner.join(repr(item) for item in i)) + def test_abc_views(self): + empty = {} + short = dict(zip('edcba', 'edcba')) + long = dict((chr(x), chr(x)) for x in range(90, 64, -1)) + lengths = {"empty": empty, "short": short, "long": long} + + for name, d in lengths.items(): + with self.subTest(length=name, name="Views"): + is_short = len(d) < 6 + joiner = ", " if is_short else ",\n " + i = d.items() + s = sorted(i) + joined_items = "({%s})" % joiner.join(["%r: %r" % (k, v) for (k, v) in i]) + sorted_items = "({%s})" % joiner.join(["%r: %r" % (k, v) for (k, v) in s]) + self.assertEqual(pprint.pformat(KeysView(d), sort_dicts=True), + KeysView.__name__ + sorted_items) + self.assertEqual(pprint.pformat(ItemsView(d), sort_dicts=True), + ItemsView.__name__ + sorted_items) + self.assertEqual(pprint.pformat(MappingView(d), sort_dicts=True), + MappingView.__name__ + sorted_items) + self.assertEqual(pprint.pformat(ValuesView(d), sort_dicts=True), + ValuesView.__name__ + sorted_items) + self.assertEqual(pprint.pformat(KeysView(d), sort_dicts=False), + KeysView.__name__ + joined_items) + self.assertEqual(pprint.pformat(ItemsView(d), sort_dicts=False), + ItemsView.__name__ + joined_items) + self.assertEqual(pprint.pformat(MappingView(d), sort_dicts=False), + MappingView.__name__ + joined_items) + self.assertEqual(pprint.pformat(ValuesView(d), sort_dicts=False), + ValuesView.__name__ + joined_items) + def test_empty_simple_namespace(self): ns = types.SimpleNamespace() formatted = pprint.pformat(ns) From 65e0be20372e20fbc9c3e5651af37550b499c24a Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 00:49:14 -0300 Subject: [PATCH 10/28] Add support for collections.abc.[Keys|Items|Mapping|Values]View in pprint. --- Lib/pprint.py | 60 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 32df59ab685a68..75620cba59da90 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -41,6 +41,9 @@ import types as _types from io import StringIO as _StringIO +ABC_VIEW_TYPES = (_collections.abc.KeysView, _collections.abc.ItemsView, + _collections.abc.ValuesView, _collections.abc.MappingView) + __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", "PrettyPrinter", "pp"] @@ -236,21 +239,36 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict - def _pprint_dict_view(self, object, stream, indent, allowance, context, level, items=False): - key = _safe_tuple if items else _safe_key + def _pprint_dict_view(self, object, stream, indent, allowance, context, level): + if isinstance(object, (self._dict_items_view, _collections.abc.ItemsView)): + key = _safe_tuple + else: + key = _safe_key + open_brace, close_brace = '([', '])' + if type(object) in ABC_VIEW_TYPES: + open_brace, close_brace = '({', '})' write = stream.write - write(object.__class__.__name__ + '([') + write(object.__class__.__name__ + open_brace) if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) if length: - if self._sort_dicts: - entries = sorted(object, key=key) + if hasattr(object, '_mapping'): + object = object._mapping.items() + if self._sort_dicts: + entries = sorted(object, key=key) + else: + entries = object + self._format_dict_items(entries, stream, indent, allowance + 1, + context, level) else: - entries = object - self._format_items(entries, stream, indent, allowance + 1, - context, level) - write('])') + if self._sort_dicts: + entries = sorted(object, key=key) + else: + entries = object + self._format_items(entries, stream, indent, allowance + 1, + context, level) + write(close_brace) _dict_keys_view = type({}.keys()) _dispatch[_dict_keys_view.__repr__] = _pprint_dict_view @@ -258,11 +276,10 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level, i _dict_values_view = type({}.values()) _dispatch[_dict_values_view.__repr__] = _pprint_dict_view - def _pprint_dict_items_view(self, object, stream, indent, allowance, context, level): - self._pprint_dict_view(object, stream, indent, allowance, context, level, items=True) - _dict_items_view = type({}.items()) - _dispatch[_dict_items_view.__repr__] = _pprint_dict_items_view + _dispatch[_dict_items_view.__repr__] = _pprint_dict_view + + _dispatch[_collections.abc.MappingView.__repr__] = _pprint_dict_view def _pprint_list(self, object, stream, indent, allowance, context, level): stream.write('[') @@ -624,16 +641,25 @@ def _safe_repr(self, object, context, maxlevels, level): return "{%s}" % ", ".join(components), readable, recursive views = self._dict_keys_view, self._dict_values_view, self._dict_items_view - view_reprs = {cls.__repr__ for cls in views} + views += (_collections.abc.MappingView,) + mapping_view_repr = {_collections.abc.MappingView.__repr__} + view_reprs = {cls.__repr__ for cls in views}.union(mapping_view_repr) if issubclass(typ, views) and r in view_reprs: key = _safe_key - if isinstance(typ, self._dict_items_view): + if isinstance(typ, (self._dict_items_view, _collections.abc.ItemsView)): key = _safe_tuple + format = typ.__name__ + '([%s])' + if typ in ABC_VIEW_TYPES: + format = typ.__name__ + '({%s})' + object = object._mapping.items() if self._sort_dicts: object = sorted(object, key=key) - format = typ.__name__ + '([%s])' + if typ in ABC_VIEW_TYPES: + formatted = format % ", ".join(["%r: %r" % (k, v) for (k, v) in object]) + else: + formatted = format % ', '.join(repr(x) for x in object) # TODO: Figure out whether we need to handle recursion here - return format % ', '.join(repr(x) for x in object), True, False + return formatted, True, False if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__): From c91af096fa38a641da03fdf3c849dbfda7330f81 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 04:12:05 -0300 Subject: [PATCH 11/28] Split _pprint_dict_view into _pprint_abc_view, so pretty-printing normal dict views and ABC views is handled in two simple methods. --- Lib/pprint.py | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 75620cba59da90..df9d0244f1f7a6 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -240,35 +240,40 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict def _pprint_dict_view(self, object, stream, indent, allowance, context, level): - if isinstance(object, (self._dict_items_view, _collections.abc.ItemsView)): + """Pretty print dict views (keys, values, items).""" + if isinstance(object, self._dict_items_view): key = _safe_tuple else: key = _safe_key - open_brace, close_brace = '([', '])' - if type(object) in ABC_VIEW_TYPES: - open_brace, close_brace = '({', '})' write = stream.write - write(object.__class__.__name__ + open_brace) + write(object.__class__.__name__ + '([') if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) if length: - if hasattr(object, '_mapping'): - object = object._mapping.items() - if self._sort_dicts: - entries = sorted(object, key=key) - else: - entries = object - self._format_dict_items(entries, stream, indent, allowance + 1, - context, level) + if self._sort_dicts: + entries = sorted(object, key=key) + else: + entries = object + self._format_items(entries, stream, indent, allowance + 1, + context, level) + write('])') + + def _pprint_abc_view(self, object, stream, indent, allowance, context, level): + """Pretty print views from collections.abc.""" + write = stream.write + write(object.__class__.__name__ + '({') + if self._indent_per_level > 1: + write((self._indent_per_level - 1) * ' ') + length = len(object) + if length: + if self._sort_dicts: + entries = sorted(object._mapping.items(), key=_safe_tuple) else: - if self._sort_dicts: - entries = sorted(object, key=key) - else: - entries = object - self._format_items(entries, stream, indent, allowance + 1, - context, level) - write(close_brace) + entries = object._mapping.items() + self._format_dict_items(entries, stream, indent, allowance + 1, + context, level) + write('})') _dict_keys_view = type({}.keys()) _dispatch[_dict_keys_view.__repr__] = _pprint_dict_view @@ -279,7 +284,7 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level): _dict_items_view = type({}.items()) _dispatch[_dict_items_view.__repr__] = _pprint_dict_view - _dispatch[_collections.abc.MappingView.__repr__] = _pprint_dict_view + _dispatch[_collections.abc.MappingView.__repr__] = _pprint_abc_view def _pprint_list(self, object, stream, indent, allowance, context, level): stream.write('[') From 5b2341c11cd7c7de2b45eed25ae8469d3ebc193a Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 04:25:52 -0300 Subject: [PATCH 12/28] Simplify redundant code. --- Lib/pprint.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index df9d0244f1f7a6..cd8ddbbed6cca1 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -645,10 +645,9 @@ def _safe_repr(self, object, context, maxlevels, level): del context[objid] return "{%s}" % ", ".join(components), readable, recursive - views = self._dict_keys_view, self._dict_values_view, self._dict_items_view - views += (_collections.abc.MappingView,) - mapping_view_repr = {_collections.abc.MappingView.__repr__} - view_reprs = {cls.__repr__ for cls in views}.union(mapping_view_repr) + views = (self._dict_keys_view, self._dict_values_view, self._dict_items_view, + _collections.abc.MappingView) + view_reprs = {cls.__repr__ for cls in views} if issubclass(typ, views) and r in view_reprs: key = _safe_key if isinstance(typ, (self._dict_items_view, _collections.abc.ItemsView)): From a9ff3dfdb8a40bb3b465ac2c1d5484a3a101e2be Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 04:47:48 -0300 Subject: [PATCH 13/28] Add collections.abc views to some existing pprint tests. --- Lib/test/test_pprint.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index a68cd5e90b7ba7..201d055fa5a278 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -233,6 +233,7 @@ def test_same_as_repr(self): frozenset(), frozenset2(), frozenset3(), {}, dict2(), dict3(), {}.keys(), {}.values(), {}.items(), + MappingView({}), KeysView({}), ItemsView({}), ValuesView({}), self.assertTrue, pprint, -6, -6, -6-6j, -1.5, "x", b"x", bytearray(b"x"), (3,), [3], {3: 6}, @@ -243,6 +244,8 @@ def test_same_as_repr(self): frozenset({8}), frozenset2({8}), frozenset3({8}), dict2({5: 6}), dict3({5: 6}), {5: 6}.keys(), {5: 6}.values(), {5: 6}.items(), + MappingView({5: 6}), KeysView({5: 6}), + ItemsView({5: 6}), ValuesView({5: 6}), range(10, -11, -1), True, False, None, ..., ): @@ -329,6 +332,26 @@ def test_basic_line_wrap(self): items = collections.OrderedDict({v: v for v in o}).items() self.assertEqual(pprint.pformat(items), exp) + o = range(100) + exp = 'KeysView({%s})' % (': None,\n '.join(map(str, o)) + ': None') + keys_view = KeysView(dict.fromkeys(o)) + self.assertEqual(pprint.pformat(keys_view), exp) + + o = range(100) + exp = 'ItemsView({%s})' % (': None,\n '.join(map(str, o)) + ': None') + items_view = ItemsView(dict.fromkeys(o)) + self.assertEqual(pprint.pformat(items_view), exp) + + o = range(100) + exp = 'MappingView({%s})' % (': None,\n '.join(map(str, o)) + ': None') + mapping_view = MappingView(dict.fromkeys(o)) + self.assertEqual(pprint.pformat(mapping_view), exp) + + o = range(100) + exp = 'ValuesView({%s})' % (': None,\n '.join(map(str, o)) + ': None') + values_view = ValuesView(dict.fromkeys(o)) + self.assertEqual(pprint.pformat(values_view), exp) + o = range(100) exp = '[%s]' % ',\n '.join(map(str, o)) for type in [list, list2]: From 4c6716650b13f15b6774ba0cc773a88e124d98d2 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 04:50:01 -0300 Subject: [PATCH 14/28] Remove TODO. --- Lib/pprint.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index cd8ddbbed6cca1..43d7f218851f6f 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -662,7 +662,6 @@ def _safe_repr(self, object, context, maxlevels, level): formatted = format % ", ".join(["%r: %r" % (k, v) for (k, v) in object]) else: formatted = format % ', '.join(repr(x) for x in object) - # TODO: Figure out whether we need to handle recursion here return formatted, True, False if (issubclass(typ, list) and r is list.__repr__) or \ From bfa9868e6524d8861983086a87561ea5125aa2b5 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 05:03:12 -0300 Subject: [PATCH 15/28] Test that views from collection.UserDict are correctly formatted by pprint. --- Lib/test/test_pprint.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 201d055fa5a278..779c439046c74d 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1214,6 +1214,36 @@ def test_user_dict(self): 'over': 5, 'quick': 1, 'the': 0}""") + self.assertEqual(pprint.pformat(d.keys()), """\ +KeysView({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0})""") + self.assertEqual(pprint.pformat(d.items()), """\ +ItemsView({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0})""") + self.assertEqual(pprint.pformat(d.values()), """\ +ValuesView({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0})""") def test_user_list(self): d = collections.UserList() From 148b4690354bcefc11e8c14391ea7d5769415510 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:02:15 -0300 Subject: [PATCH 16/28] Handle recursive dict and ABC views. --- Lib/pprint.py | 34 ++++++++++++++++++++++++++++------ Lib/test/test_pprint.py | 12 ++++++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 43d7f218851f6f..0b343b201fd559 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -648,7 +648,12 @@ def _safe_repr(self, object, context, maxlevels, level): views = (self._dict_keys_view, self._dict_values_view, self._dict_items_view, _collections.abc.MappingView) view_reprs = {cls.__repr__ for cls in views} - if issubclass(typ, views) and r in view_reprs: + if issubclass(typ, _collections.abc.MappingView) and r in view_reprs: + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}", False, objid in context + if objid in context: + return _recursion(object), False, True key = _safe_key if isinstance(typ, (self._dict_items_view, _collections.abc.ItemsView)): key = _safe_tuple @@ -658,11 +663,28 @@ def _safe_repr(self, object, context, maxlevels, level): object = object._mapping.items() if self._sort_dicts: object = sorted(object, key=key) - if typ in ABC_VIEW_TYPES: - formatted = format % ", ".join(["%r: %r" % (k, v) for (k, v) in object]) - else: - formatted = format % ', '.join(repr(x) for x in object) - return formatted, True, False + context[objid] = 1 + readable = True + recursive = False + components = [] + append = components.append + level += 1 + for val in object: + krepr = "" + kreadable = krecur = False + if typ in ABC_VIEW_TYPES: + key, val = val + krepr, kreadable, krecur = self.format( + key, context, maxlevels, level) + krepr = "%s: " % krepr + vrepr, vreadable, vrecur = self.format( + val, context, maxlevels, level) + append("%s%s" % (krepr, vrepr)) + readable = readable and kreadable and vreadable + if krecur or vrecur: + recursive = True + del context[objid] + return format % ", ".join(components), readable, recursive if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__): diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 779c439046c74d..e60ccdd8b2a0b3 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -175,10 +175,17 @@ def test_knotted(self): # Messy dict. self.d = {} self.d[0] = self.d[1] = self.d[2] = self.d + self.e = {} + self.v = ValuesView(self.e) + self.m = MappingView(self.e) + self.dv = self.e.values() + self.e["v"] = self.v + self.e["m"] = self.m + self.e["dv"] = self.dv pp = pprint.PrettyPrinter() - for icky in self.a, self.b, self.d, (self.d, self.d): + for icky in self.a, self.b, self.d, (self.d, self.d), self.e, self.v, self.m, self.dv: self.assertTrue(pprint.isrecursive(icky), "expected isrecursive") self.assertFalse(pprint.isreadable(icky), "expected not isreadable") self.assertTrue(pp.isrecursive(icky), "expected isrecursive") @@ -186,10 +193,11 @@ def test_knotted(self): # Break the cycles. self.d.clear() + self.e.clear() del self.a[:] del self.b[:] - for safe in self.a, self.b, self.d, (self.d, self.d): + for safe in self.a, self.b, self.d, (self.d, self.d), self.e, self.v, self.m, self.dv: # module-level convenience functions self.assertFalse(pprint.isrecursive(safe), "expected not isrecursive for %r" % (safe,)) From 34abce71c667554c999b7b6b0b86f2053f4a6e86 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:16:05 -0300 Subject: [PATCH 17/28] Test that subclasses of ABC views work in pprint. --- Lib/test/test_pprint.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index e60ccdd8b2a0b3..9370cf465599d4 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -69,6 +69,14 @@ class dict_custom_repr(dict): def __repr__(self): return '*'*len(dict.__repr__(self)) +class mappingview_custom_repr(MappingView): + def __repr__(self): + return '*'*len(MappingView.__repr__(self)) + +class keysview_custom_repr(KeysView): + def __repr__(self): + return '*'*len(KeysView.__repr__(self)) + @dataclasses.dataclass class dataclass1: field1: str @@ -283,6 +291,12 @@ def test_container_repr_override_called(self): dict_custom_repr(), dict_custom_repr({5: 6}), dict_custom_repr(zip(range(N),range(N))), + mappingview_custom_repr({}), + mappingview_custom_repr({5: 6}), + mappingview_custom_repr(dict(zip(range(N),range(N)))), + keysview_custom_repr({}), + keysview_custom_repr({5: 6}), + keysview_custom_repr(dict(zip(range(N),range(N)))), ): native = repr(cont) expected = '*' * len(native) From 09839a60fc0e8d74687545c0aaeb80b31ce70fa1 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:31:01 -0300 Subject: [PATCH 18/28] Test dict views coming from collections.Counter. --- Lib/test/test_pprint.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 9370cf465599d4..6efe22455fee75 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -526,12 +526,12 @@ def test_mapping_proxy(self): ('dog', 8)]))""") def test_dict_views(self): - for dict_class in (dict, collections.OrderedDict): - empty = dict_class() - short = dict_class(zip('edcba', 'edcba')) - long = dict_class((chr(x), chr(x)) for x in range(90, 64, -1)) + for dict_class in (dict, collections.OrderedDict, collections.Counter): + empty = dict_class({}) + short = dict_class(dict(zip('edcba', 'edcba'))) + long = dict_class(dict((chr(x), chr(x)) for x in range(90, 64, -1))) lengths = {"empty": empty, "short": short, "long": long} - prefix = "dict" if dict_class is dict else "odict" + prefix = "odict" if dict_class is collections.OrderedDict else "dict" for name, d in lengths.items(): with self.subTest(length=name, prefix=prefix): is_short = len(d) < 6 From 091c3bbe45c69fd003a4d2edadd246aba3451c59 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:38:29 -0300 Subject: [PATCH 19/28] Test ABC views coming from collections.ChainMap. --- Lib/test/test_pprint.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 6efe22455fee75..831667b0042f29 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1189,6 +1189,39 @@ def test_chainmap(self): ('a', 6), ('lazy', 7), ('dog', 8)]))""") + self.assertEqual(pprint.pformat(d.keys()), +"""\ +KeysView({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0})""") + self.assertEqual(pprint.pformat(d.items()), + """\ +ItemsView({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0})""") + self.assertEqual(pprint.pformat(d.values()), + """\ +ValuesView({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0})""") def test_deque(self): d = collections.deque() From 29655a03cc0d1943325e4b7f9cf9e2a78f8c767e Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:58:55 -0300 Subject: [PATCH 20/28] Test odict views coming from collections.OrderedDict. --- Lib/test/test_pprint.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 831667b0042f29..7c4e71b54f044b 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -497,6 +497,30 @@ def test_ordered_dict(self): ('a', 6), ('lazy', 7), ('dog', 8)])""") + self.assertEqual(pprint.pformat(d.keys(), sort_dicts=False), +"""\ +odict_keys(['the', + 'quick', + 'brown', + 'fox', + 'jumped', + 'over', + 'a', + 'lazy', + 'dog'])""") + self.assertEqual(pprint.pformat(d.items(), sort_dicts=False), +"""\ +odict_items([('the', 0), + ('quick', 1), + ('brown', 2), + ('fox', 3), + ('jumped', 4), + ('over', 5), + ('a', 6), + ('lazy', 7), + ('dog', 8)])""") + self.assertEqual(pprint.pformat(d.values(), sort_dicts=False), + "odict_values([0, 1, 2, 3, 4, 5, 6, 7, 8])") def test_mapping_proxy(self): words = 'the quick brown fox jumped over a lazy dog'.split() From 646816ae4fc957366776acfb91549982749fc150 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 8 Apr 2024 07:05:10 -0300 Subject: [PATCH 21/28] Rename _pprint_abc_view to _pprint_mapping_abc_view. --- Lib/pprint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 0b343b201fd559..bc014244332e7d 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -259,7 +259,7 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level): context, level) write('])') - def _pprint_abc_view(self, object, stream, indent, allowance, context, level): + def _pprint_mapping_abc_view(self, object, stream, indent, allowance, context, level): """Pretty print views from collections.abc.""" write = stream.write write(object.__class__.__name__ + '({') @@ -284,7 +284,7 @@ def _pprint_abc_view(self, object, stream, indent, allowance, context, level): _dict_items_view = type({}.items()) _dispatch[_dict_items_view.__repr__] = _pprint_dict_view - _dispatch[_collections.abc.MappingView.__repr__] = _pprint_abc_view + _dispatch[_collections.abc.MappingView.__repr__] = _pprint_mapping_abc_view def _pprint_list(self, object, stream, indent, allowance, context, level): stream.write('[') From 7537a52a4e758180ad062aaff9e45ab6cc1392bb Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Tue, 9 Apr 2024 07:44:53 -0300 Subject: [PATCH 22/28] Add pprint test for mapping ABC views where ._mapping has a custom __repr__ and fix ChainMap test. --- Lib/test/test_pprint.py | 121 ++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 7c4e71b54f044b..568131d8b01d7c 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -11,7 +11,7 @@ import test.support import types import unittest -from collections.abc import KeysView, ItemsView, MappingView, ValuesView +from collections.abc import ItemsView, KeysView, Mapping, MappingView, ValuesView # list, tuple and dict subclasses that do or don't overwrite __repr__ class list2(list): @@ -613,6 +613,44 @@ def test_abc_views(self): self.assertEqual(pprint.pformat(ValuesView(d), sort_dicts=False), ValuesView.__name__ + joined_items) + def test_mapping_subclass_repr(self): + """Test that mapping ABC views use their ._mapping's __repr__.""" + class MyMapping(Mapping): + def __init__(self, keys=None): + self._keys = {} if keys is None else dict.fromkeys(keys) + + def __getitem__(self, item): + return self._keys[item] + + def __len__(self): + return len(self._keys) + + def __iter__(self): + return iter(self._keys) + + def __repr__(self): + return f"{self.__class__.__name__}([{', '.join(map(repr, self._keys.keys()))}])" + + m = MyMapping(["test", 1]) + self.assertEqual(repr(m), "MyMapping(['test', 1])") + short_view_repr = "%s(MyMapping(['test', 1]))" + self.assertEqual(repr(m.keys()), short_view_repr % "KeysView") + self.assertEqual(pprint.pformat(m.items()), short_view_repr % "ItemsView") + self.assertEqual(pprint.pformat(m.keys()), short_view_repr % "KeysView") + self.assertEqual(pprint.pformat(MappingView(m)), short_view_repr % "MappingView") + self.assertEqual(pprint.pformat(m.values()), short_view_repr % "ValuesView") + + alpha = "abcdefghijklmnopqrstuvwxyz" + m = MyMapping(alpha) + alpha_repr = ", ".join(map(repr, list(alpha))) + long_view_repr = "%%s(MyMapping([%s]))" % alpha_repr + self.assertEqual(repr(m), "MyMapping([%s])" % alpha_repr) + self.assertEqual(repr(m.keys()), long_view_repr % "KeysView") + self.assertEqual(pprint.pformat(m.items()), long_view_repr % "ItemsView") + self.assertEqual(pprint.pformat(m.keys()), long_view_repr % "KeysView") + self.assertEqual(pprint.pformat(MappingView(m)), long_view_repr % "MappingView") + self.assertEqual(pprint.pformat(m.values()), long_view_repr % "ValuesView") + def test_empty_simple_namespace(self): ns = types.SimpleNamespace() formatted = pprint.pformat(ns) @@ -1215,37 +1253,64 @@ def test_chainmap(self): ('dog', 8)]))""") self.assertEqual(pprint.pformat(d.keys()), """\ -KeysView({'a': 6, - 'brown': 2, - 'dog': 8, - 'fox': 3, - 'jumped': 4, - 'lazy': 7, - 'over': 5, - 'quick': 1, - 'the': 0})""") +KeysView(ChainMap({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0}, + OrderedDict([('the', 0), + ('quick', 1), + ('brown', 2), + ('fox', 3), + ('jumped', 4), + ('over', 5), + ('a', 6), + ('lazy', 7), + ('dog', 8)])))""") self.assertEqual(pprint.pformat(d.items()), """\ -ItemsView({'a': 6, - 'brown': 2, - 'dog': 8, - 'fox': 3, - 'jumped': 4, - 'lazy': 7, - 'over': 5, - 'quick': 1, - 'the': 0})""") +ItemsView(ChainMap({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0}, + OrderedDict([('the', 0), + ('quick', 1), + ('brown', 2), + ('fox', 3), + ('jumped', 4), + ('over', 5), + ('a', 6), + ('lazy', 7), + ('dog', 8)])))""") self.assertEqual(pprint.pformat(d.values()), """\ -ValuesView({'a': 6, - 'brown': 2, - 'dog': 8, - 'fox': 3, - 'jumped': 4, - 'lazy': 7, - 'over': 5, - 'quick': 1, - 'the': 0})""") +ValuesView(ChainMap({'a': 6, + 'brown': 2, + 'dog': 8, + 'fox': 3, + 'jumped': 4, + 'lazy': 7, + 'over': 5, + 'quick': 1, + 'the': 0}, + OrderedDict([('the', 0), + ('quick', 1), + ('brown', 2), + ('fox', 3), + ('jumped', 4), + ('over', 5), + ('a', 6), + ('lazy', 7), + ('dog', 8)])))""") def test_deque(self): d = collections.deque() From 7a6769d41f78a7f94e9bac095d4324fa2153dce4 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Tue, 9 Apr 2024 07:48:55 -0300 Subject: [PATCH 23/28] When a mapping ABC view has a ._mapping that defines a custom __repr__, dispatch pretty-printing it by that __repr__. --- Lib/pprint.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/pprint.py b/Lib/pprint.py index bc014244332e7d..aa6cbc7cbd68ab 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -262,6 +262,12 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level): def _pprint_mapping_abc_view(self, object, stream, indent, allowance, context, level): """Pretty print views from collections.abc.""" write = stream.write + # Dispatch formatting to the view's _mapping if it has a different __repr__ + if object._mapping.__class__.__repr__ is not _collections.abc.MappingView.__repr__: + write(object.__class__.__name__ + '(') + self._format(object._mapping, stream, indent, allowance, context, level) + write(')') + return write(object.__class__.__name__ + '({') if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') @@ -659,6 +665,11 @@ def _safe_repr(self, object, context, maxlevels, level): key = _safe_tuple format = typ.__name__ + '([%s])' if typ in ABC_VIEW_TYPES: + # Dispatch formatting to the view's _mapping if it has a different __repr__ + if object._mapping.__class__.__repr__ is not _collections.abc.MappingView.__repr__: + mapping_repr, readable, recursive = self.format( + object._mapping, context, maxlevels, level) + return (typ.__name__ + '(%s)' % mapping_repr), readable, recursive format = typ.__name__ + '({%s})' object = object._mapping.items() if self._sort_dicts: From 58692d2c4b67cc7d6f962fad8739d899f6281751 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:11:15 -0300 Subject: [PATCH 24/28] Add tests for ABC mapping views subclasses that don't replace __repr__, also handling those that delete ._mapping on instances. --- Lib/test/test_pprint.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 568131d8b01d7c..658f1b77cd31bf 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -587,6 +587,8 @@ def test_abc_views(self): short = dict(zip('edcba', 'edcba')) long = dict((chr(x), chr(x)) for x in range(90, 64, -1)) lengths = {"empty": empty, "short": short, "long": long} + # Test that a subclass that doesn't replace __repr__ works with different lengths + class MV(MappingView): pass for name, d in lengths.items(): with self.subTest(length=name, name="Views"): @@ -602,6 +604,8 @@ def test_abc_views(self): ItemsView.__name__ + sorted_items) self.assertEqual(pprint.pformat(MappingView(d), sort_dicts=True), MappingView.__name__ + sorted_items) + self.assertEqual(pprint.pformat(MV(d), sort_dicts=True), + MV.__name__ + sorted_items) self.assertEqual(pprint.pformat(ValuesView(d), sort_dicts=True), ValuesView.__name__ + sorted_items) self.assertEqual(pprint.pformat(KeysView(d), sort_dicts=False), @@ -610,9 +614,20 @@ def test_abc_views(self): ItemsView.__name__ + joined_items) self.assertEqual(pprint.pformat(MappingView(d), sort_dicts=False), MappingView.__name__ + joined_items) + self.assertEqual(pprint.pformat(MV(d), sort_dicts=False), + MV.__name__ + joined_items) self.assertEqual(pprint.pformat(ValuesView(d), sort_dicts=False), ValuesView.__name__ + joined_items) + def test_mapping_view_subclass_no_mapping(self): + class BMV(MappingView): + def __init__(self, d): + super().__init__(d) + self.mapping = self._mapping + del self._mapping + + self.assertRaises(AttributeError, pprint.pformat, BMV({})) + def test_mapping_subclass_repr(self): """Test that mapping ABC views use their ._mapping's __repr__.""" class MyMapping(Mapping): From 8e605c1244133a7cad2ae33830ed86b56c7960ee Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:12:34 -0300 Subject: [PATCH 25/28] Simplify the pretty printing of ABC mapping views. --- Lib/pprint.py | 57 +++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index aa6cbc7cbd68ab..54ef90a08553a1 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -41,9 +41,6 @@ import types as _types from io import StringIO as _StringIO -ABC_VIEW_TYPES = (_collections.abc.KeysView, _collections.abc.ItemsView, - _collections.abc.ValuesView, _collections.abc.MappingView) - __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", "PrettyPrinter", "pp"] @@ -260,26 +257,12 @@ def _pprint_dict_view(self, object, stream, indent, allowance, context, level): write('])') def _pprint_mapping_abc_view(self, object, stream, indent, allowance, context, level): - """Pretty print views from collections.abc.""" + """Pretty print mapping views from collections.abc.""" write = stream.write - # Dispatch formatting to the view's _mapping if it has a different __repr__ - if object._mapping.__class__.__repr__ is not _collections.abc.MappingView.__repr__: - write(object.__class__.__name__ + '(') - self._format(object._mapping, stream, indent, allowance, context, level) - write(')') - return - write(object.__class__.__name__ + '({') - if self._indent_per_level > 1: - write((self._indent_per_level - 1) * ' ') - length = len(object) - if length: - if self._sort_dicts: - entries = sorted(object._mapping.items(), key=_safe_tuple) - else: - entries = object._mapping.items() - self._format_dict_items(entries, stream, indent, allowance + 1, - context, level) - write('})') + write(object.__class__.__name__ + '(') + # Dispatch formatting to the view's _mapping + self._format(object._mapping, stream, indent, allowance, context, level) + write(')') _dict_keys_view = type({}.keys()) _dispatch[_dict_keys_view.__repr__] = _pprint_dict_view @@ -664,14 +647,15 @@ def _safe_repr(self, object, context, maxlevels, level): if isinstance(typ, (self._dict_items_view, _collections.abc.ItemsView)): key = _safe_tuple format = typ.__name__ + '([%s])' - if typ in ABC_VIEW_TYPES: - # Dispatch formatting to the view's _mapping if it has a different __repr__ - if object._mapping.__class__.__repr__ is not _collections.abc.MappingView.__repr__: - mapping_repr, readable, recursive = self.format( - object._mapping, context, maxlevels, level) - return (typ.__name__ + '(%s)' % mapping_repr), readable, recursive - format = typ.__name__ + '({%s})' - object = object._mapping.items() + if hasattr(object, "_mapping"): + # Dispatch formatting to the view's _mapping + mapping_repr, readable, recursive = self.format( + object._mapping, context, maxlevels, level) + return (typ.__name__ + '(%s)' % mapping_repr), readable, recursive + elif hasattr(typ, "_mapping"): + # We have a view that somehow has lost its type's _mapping, raise + # an error by calling repr() instead of failing cryptically later + return repr(object), True, False if self._sort_dicts: object = sorted(object, key=key) context[objid] = 1 @@ -681,18 +665,11 @@ def _safe_repr(self, object, context, maxlevels, level): append = components.append level += 1 for val in object: - krepr = "" - kreadable = krecur = False - if typ in ABC_VIEW_TYPES: - key, val = val - krepr, kreadable, krecur = self.format( - key, context, maxlevels, level) - krepr = "%s: " % krepr vrepr, vreadable, vrecur = self.format( val, context, maxlevels, level) - append("%s%s" % (krepr, vrepr)) - readable = readable and kreadable and vreadable - if krecur or vrecur: + append(vrepr) + readable = readable and vreadable + if vrecur: recursive = True del context[objid] return format % ", ".join(components), readable, recursive From 1dc4ab2fd1e24745e67634468f615576f42327e9 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 21 Apr 2024 19:34:16 -0300 Subject: [PATCH 26/28] Add a test for depth handling when pretty printing dict views. --- Lib/test/test_pprint.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 658f1b77cd31bf..7419bb7140de6f 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -619,6 +619,23 @@ class MV(MappingView): pass self.assertEqual(pprint.pformat(ValuesView(d), sort_dicts=False), ValuesView.__name__ + joined_items) + def test_nested_views(self): + d = {1: MappingView({1: MappingView({1: MappingView({1: 2})})})} + self.assertEqual(repr(d), + "{1: MappingView({1: MappingView({1: MappingView({1: 2})})})}") + self.assertEqual(pprint.pformat(d), + "{1: MappingView({1: MappingView({1: MappingView({1: 2})})})}") + self.assertEqual(pprint.pformat(d, depth=2), + "{1: MappingView({1: {...}})}") + d = {} + d1 = {1: d.values()} + d2 = {1: d1.values()} + d3 = {1: d2.values()} + self.assertEqual(pprint.pformat(d3), + "{1: dict_values([dict_values([dict_values([])])])}") + self.assertEqual(pprint.pformat(d3, depth=2), + "{1: dict_values([{...}])}") + def test_mapping_view_subclass_no_mapping(self): class BMV(MappingView): def __init__(self, d): From f2674524ca8906b5b73874714457ede1c6c2719e Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:35:53 -0300 Subject: [PATCH 27/28] Fix checking whether the view type is a subclass of an items view, add a test. --- Lib/pprint.py | 2 +- Lib/test/test_pprint.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 54ef90a08553a1..8e87a9e17d2098 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -644,7 +644,7 @@ def _safe_repr(self, object, context, maxlevels, level): if objid in context: return _recursion(object), False, True key = _safe_key - if isinstance(typ, (self._dict_items_view, _collections.abc.ItemsView)): + if issubclass(typ, (self._dict_items_view, _collections.abc.ItemsView)): key = _safe_tuple format = typ.__name__ + '([%s])' if hasattr(object, "_mapping"): diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 7419bb7140de6f..98c56b91239b20 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -636,6 +636,18 @@ def test_nested_views(self): self.assertEqual(pprint.pformat(d3, depth=2), "{1: dict_values([{...}])}") + def test_unorderable_items_views(self): + """Check that views with unorderable items have stable sorting.""" + d = dict((((3+1j), 3), ((1+1j), (1+0j)), (1j, 0j), (500, None), (499, None))) + iv = ItemsView(d) + self.assertEqual(pprint.pformat(iv), + pprint.pformat(iv)) + self.assertTrue(pprint.pformat(iv).endswith(", 499: None, 500: None})"), + pprint.pformat(iv)) + self.assertEqual(pprint.pformat(d.items()), # Won't be equal unless _safe_tuple + pprint.pformat(d.items())) # is used in _safe_repr + self.assertTrue(pprint.pformat(d.items()).endswith(", (499, None), (500, None)])")) + def test_mapping_view_subclass_no_mapping(self): class BMV(MappingView): def __init__(self, d): From 386733836cd4b023755ce2a9d9445ea5f77595f4 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:49:36 -0300 Subject: [PATCH 28/28] Move construction of the views __repr__ set out of _safe_repr. --- Lib/pprint.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 8e87a9e17d2098..0e13170a5e8b89 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -275,6 +275,10 @@ def _pprint_mapping_abc_view(self, object, stream, indent, allowance, context, l _dispatch[_collections.abc.MappingView.__repr__] = _pprint_mapping_abc_view + _view_reprs = {cls.__repr__ for cls in + (_dict_keys_view, _dict_values_view, _dict_items_view, + _collections.abc.MappingView)} + def _pprint_list(self, object, stream, indent, allowance, context, level): stream.write('[') self._format_items(object, stream, indent, allowance + 1, @@ -634,10 +638,7 @@ def _safe_repr(self, object, context, maxlevels, level): del context[objid] return "{%s}" % ", ".join(components), readable, recursive - views = (self._dict_keys_view, self._dict_values_view, self._dict_items_view, - _collections.abc.MappingView) - view_reprs = {cls.__repr__ for cls in views} - if issubclass(typ, _collections.abc.MappingView) and r in view_reprs: + if issubclass(typ, _collections.abc.MappingView) and r in self._view_reprs: objid = id(object) if maxlevels and level >= maxlevels: return "{...}", False, objid in context @@ -646,7 +647,6 @@ def _safe_repr(self, object, context, maxlevels, level): key = _safe_key if issubclass(typ, (self._dict_items_view, _collections.abc.ItemsView)): key = _safe_tuple - format = typ.__name__ + '([%s])' if hasattr(object, "_mapping"): # Dispatch formatting to the view's _mapping mapping_repr, readable, recursive = self.format( @@ -672,7 +672,7 @@ def _safe_repr(self, object, context, maxlevels, level): if vrecur: recursive = True del context[objid] - return format % ", ".join(components), readable, recursive + return typ.__name__ + '([%s])' % ", ".join(components), readable, recursive if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__):