diff --git a/Lib/pprint.py b/Lib/pprint.py index dc0953cec67..6b574218277 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -44,20 +44,22 @@ def pprint(object, stream=None, indent=1, width=80, depth=None, *, - compact=False, sort_dicts=True, underscore_numbers=False): + compact=False, expand=False, sort_dicts=True, + underscore_numbers=False): """Pretty-print a Python object to a stream [default is sys.stdout].""" printer = PrettyPrinter( stream=stream, indent=indent, width=width, depth=depth, - compact=compact, sort_dicts=sort_dicts, + compact=compact, expand=expand, sort_dicts=sort_dicts, underscore_numbers=underscore_numbers) printer.pprint(object) def pformat(object, indent=1, width=80, depth=None, *, - compact=False, sort_dicts=True, underscore_numbers=False): + compact=False, expand=False, sort_dicts=True, + underscore_numbers=False): """Format a Python object into a pretty-printed representation.""" return PrettyPrinter(indent=indent, width=width, depth=depth, - compact=compact, sort_dicts=sort_dicts, + compact=compact, expand=expand, sort_dicts=sort_dicts, underscore_numbers=underscore_numbers).pformat(object) @@ -111,7 +113,8 @@ def _safe_tuple(t): class PrettyPrinter: def __init__(self, indent=1, width=80, depth=None, stream=None, *, - compact=False, sort_dicts=True, underscore_numbers=False): + compact=False, expand=False, sort_dicts=True, + underscore_numbers=False): """Handle pretty printing operations onto a stream using a set of configured parameters. @@ -130,6 +133,12 @@ def __init__(self, indent=1, width=80, depth=None, stream=None, *, compact If true, several items will be combined in one line. + Incompatible with expand mode. + + expand + If true, the output will be formatted similar to + pretty-printed json.dumps() when ``indent`` is supplied. + Incompatible with compact mode. sort_dicts If true, dict keys are sorted. @@ -146,6 +155,8 @@ def __init__(self, indent=1, width=80, depth=None, stream=None, *, raise ValueError('depth must be > 0') if not width: raise ValueError('width must be != 0') + if compact and expand: + raise ValueError('compact and expand are incompatible') self._depth = depth self._indent_per_level = indent self._width = width @@ -154,6 +165,7 @@ def __init__(self, indent=1, width=80, depth=None, stream=None, *, else: self._stream = _sys.stdout self._compact = bool(compact) + self._expand = bool(expand) self._sort_dicts = sort_dicts self._underscore_numbers = underscore_numbers @@ -175,6 +187,11 @@ def isreadable(self, object): return readable and not recursive def _format(self, object, stream, indent, allowance, context, level): + # Width of any "key: " prefix already written on the current line by + # _format_child(). In expand mode `indent` is the block indent and so + # does not include it, but it still consumes width here. + prefix_len = self._pending_prefix_len + self._pending_prefix_len = 0 objid = id(object) if objid in context: stream.write(_recursion(object)) @@ -182,7 +199,7 @@ def _format(self, object, stream, indent, allowance, context, level): self._readable = False return rep = self._repr(object, context, level) - max_width = self._width - indent - allowance + max_width = self._width - indent - prefix_len - allowance if len(rep) > max_width: p = self._dispatch.get(type(object).__repr__, None) # Lazy import to improve module import time @@ -205,24 +222,64 @@ def _format(self, object, stream, indent, allowance, context, level): return stream.write(rep) + def _format_block_start(self, start_str, indent): + if self._expand: + return f"{start_str}\n{' ' * indent}" + return start_str + + def _format_block_end(self, end_str, indent): + if self._expand: + return f"\n{' ' * indent}{end_str}" + return end_str + + def _child_indent(self, indent, prefix_len): + if self._expand: + return indent + return indent + prefix_len + + # Set by _format_child() immediately before it calls _format(), and + # consumed there. Passing it out of band keeps _format()'s signature + # unchanged for third-party subclasses that override it. + _pending_prefix_len = 0 + + def _format_child(self, object, stream, indent, allowance, context, level, + prefix_len): + if self._expand: + # Aligned mode folds the prefix into the indent (see + # _child_indent), so only expand mode needs to report it. + self._pending_prefix_len = prefix_len + try: + self._format(object, stream, indent, allowance, context, level) + finally: + self._pending_prefix_len = 0 + + def _write_indent_padding(self, write): + if self._expand: + if self._indent_per_level > 0: + write(self._indent_per_level * " ") + elif self._indent_per_level > 1: + write((self._indent_per_level - 1) * " ") + def _pprint_dataclass(self, object, stream, indent, allowance, context, level): # Lazy import to improve module import time from dataclasses import fields as dataclass_fields cls_name = object.__class__.__name__ - indent += len(cls_name) + 1 + if self._expand: + indent += self._indent_per_level + else: + indent += len(cls_name) + 1 items = [(f.name, getattr(object, f.name)) for f in dataclass_fields(object) if f.repr] - stream.write(cls_name + '(') + stream.write(self._format_block_start(cls_name + '(', indent)) self._format_namespace_items(items, stream, indent, allowance, context, level) - stream.write(')') + stream.write(self._format_block_end(')', indent - self._indent_per_level)) _dispatch = {} def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write - write('{') - if self._indent_per_level > 1: - write((self._indent_per_level - 1) * ' ') + write(self._format_block_start('{', indent)) + self._write_indent_padding(write) length = len(object) if length: if self._sort_dicts: @@ -231,37 +288,117 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): items = object.items() self._format_dict_items(items, stream, indent, allowance + 1, context, level) - write('}') + write(self._format_block_end('}', indent)) _dispatch[dict.__repr__] = _pprint_dict + def _pprint_frozendict(self, object, stream, indent, allowance, context, level): + write = stream.write + cls = object.__class__ + if not len(object): + write(repr(object)) + return + + write(self._format_block_start(cls.__name__ + "({", indent)) + self._write_indent_padding(write) + + if self._sort_dicts: + items = sorted(object.items(), key=_safe_tuple) + else: + items = object.items() + self._format_dict_items( + items, + stream, + self._child_indent(indent, len(cls.__name__) + 1), + allowance + 2, + context, + level, + ) + write(self._format_block_end("})", indent)) + + _dispatch[frozendict.__repr__] = _pprint_frozendict + def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): if not len(object): stream.write(repr(object)) return cls = object.__class__ stream.write(cls.__name__ + '(') - self._format(list(object.items()), stream, - indent + len(cls.__name__) + 1, allowance + 1, - context, level) + self._format_child( + list(object.items()), + stream, + self._child_indent(indent, len(cls.__name__) + 1), + allowance + 1, + context, + level, + len(cls.__name__) + 1, + ) stream.write(')') _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict + def _pprint_dict_view(self, object, stream, indent, allowance, context, level): + """Pretty print dict views (keys, values, items).""" + if isinstance(object, self._dict_items_view): + key = _safe_tuple + else: + key = _safe_key + + write = stream.write + write( + self._format_block_start(object.__class__.__name__ + "([", indent) + ) + + if len(object): + if self._sort_dicts: + entries = sorted(object, key=key) + else: + entries = object + self._format_items( + entries, stream, indent, allowance + 2, context, level + ) + write(self._format_block_end("])", indent)) + + def _pprint_mapping_abc_view(self, object, stream, indent, allowance, context, level): + """Pretty print mapping views from collections.abc.""" + write = stream.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 + + _dict_values_view = type({}.values()) + _dispatch[_dict_values_view.__repr__] = _pprint_dict_view + + _dict_items_view = type({}.items()) + _dispatch[_dict_items_view.__repr__] = _pprint_dict_view + + _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('[') + stream.write(self._format_block_start('[', indent)) self._format_items(object, stream, indent, allowance + 1, context, level) - stream.write(']') + stream.write(self._format_block_end(']', indent)) _dispatch[list.__repr__] = _pprint_list def _pprint_tuple(self, object, stream, indent, allowance, context, level): - stream.write('(') - endchar = ',)' if len(object) == 1 else ')' + stream.write(self._format_block_start('(', indent)) + if len(object) == 1 and not self._expand: + endchar = ',)' + else: + endchar = ')' self._format_items(object, stream, indent, allowance + len(endchar), context, level) - stream.write(endchar) + stream.write(self._format_block_end(endchar, indent)) _dispatch[tuple.__repr__] = _pprint_tuple @@ -271,16 +408,17 @@ def _pprint_set(self, object, stream, indent, allowance, context, level): return typ = object.__class__ if typ is set: - stream.write('{') + stream.write(self._format_block_start('{', indent)) endchar = '}' else: - stream.write(typ.__name__ + '({') + stream.write(self._format_block_start(typ.__name__ + '({', indent)) endchar = '})' - indent += len(typ.__name__) + 1 + if not self._expand: + indent += len(typ.__name__) + 1 object = sorted(object, key=_safe_key) self._format_items(object, stream, indent, allowance + len(endchar), context, level) - stream.write(endchar) + stream.write(self._format_block_end(endchar, indent)) _dispatch[set.__repr__] = _pprint_set _dispatch[frozenset.__repr__] = _pprint_set @@ -293,7 +431,10 @@ def _pprint_str(self, object, stream, indent, allowance, context, level): chunks = [] lines = object.splitlines(True) if level == 1: - indent += 1 + if self._expand: + indent += self._indent_per_level + else: + indent += 1 allowance += 1 max_width1 = max_width = self._width - indent for i, line in enumerate(lines): @@ -329,13 +470,13 @@ def _pprint_str(self, object, stream, indent, allowance, context, level): write(rep) return if level == 1: - write('(') + write(self._format_block_start("(", indent)) for i, rep in enumerate(chunks): if i > 0: write('\n' + ' '*indent) write(rep) if level == 1: - write(')') + write(self._format_block_end(")", indent - self._indent_per_level)) _dispatch[str.__repr__] = _pprint_str @@ -346,9 +487,12 @@ def _pprint_bytes(self, object, stream, indent, allowance, context, level): return parens = level == 1 if parens: - indent += 1 + if self._expand: + indent += self._indent_per_level + else: + indent += 1 allowance += 1 - write('(') + write(self._format_block_start('(', indent)) delim = '' for rep in _wrap_bytes_repr(object, self._width - indent, allowance): write(delim) @@ -356,23 +500,35 @@ def _pprint_bytes(self, object, stream, indent, allowance, context, level): if not delim: delim = '\n' + ' '*indent if parens: - write(')') + write(self._format_block_end(')', indent - self._indent_per_level)) _dispatch[bytes.__repr__] = _pprint_bytes def _pprint_bytearray(self, object, stream, indent, allowance, context, level): write = stream.write - write('bytearray(') - self._pprint_bytes(bytes(object), stream, indent + 10, + write(self._format_block_start('bytearray(', indent)) + if self._expand: + write(' ' * self._indent_per_level) + recursive_indent = indent + self._indent_per_level + else: + recursive_indent = indent + 10 + self._pprint_bytes(bytes(object), stream, recursive_indent, allowance + 1, context, level + 1) - write(')') + write(self._format_block_end(')', indent)) _dispatch[bytearray.__repr__] = _pprint_bytearray def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): stream.write('mappingproxy(') - self._format(object.copy(), stream, indent + 13, allowance + 1, - context, level) + self._format_child( + object.copy(), + stream, + self._child_indent(indent, 13), + allowance + 1, + context, + level, + 13, + ) stream.write(')') _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy @@ -384,11 +540,15 @@ def _pprint_simplenamespace(self, object, stream, indent, allowance, context, le cls_name = 'namespace' else: cls_name = object.__class__.__name__ - indent += len(cls_name) + 1 + if self._expand: + indent += self._indent_per_level + else: + indent += len(cls_name) + 1 items = object.__dict__.items() - stream.write(cls_name + '(') - self._format_namespace_items(items, stream, indent, allowance, context, level) - stream.write(')') + stream.write(self._format_block_start(cls_name + '(', indent)) + self._format_namespace_items(items, stream, indent, allowance, context, + level) + stream.write(self._format_block_end(')', indent - self._indent_per_level)) _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace @@ -403,11 +563,19 @@ def _format_dict_items(self, items, stream, indent, allowance, context, rep = self._repr(key, context, level) write(rep) write(': ') - self._format(ent, stream, indent + len(rep) + 2, - allowance if last else 1, - context, level) + self._format_child( + ent, + stream, + self._child_indent(indent, len(rep) + 2), + allowance if last else 1, + context, + level, + len(rep) + 2, + ) if not last: write(delimnl) + elif self._expand: + write(',') def _format_namespace_items(self, items, stream, indent, allowance, context, level): write = stream.write @@ -422,17 +590,24 @@ def _format_namespace_items(self, items, stream, indent, allowance, context, lev # recursive dataclass repr. write("...") else: - self._format(ent, stream, indent + len(key) + 1, - allowance if last else 1, - context, level) + self._format_child( + ent, + stream, + self._child_indent(indent, len(key) + 1), + allowance if last else 1, + context, + level, + len(key) + 1, + ) if not last: write(delimnl) + elif self._expand: + write(',') def _format_items(self, items, stream, indent, allowance, context, level): write = stream.write indent += self._indent_per_level - if self._indent_per_level > 1: - write((self._indent_per_level - 1) * ' ') + self._write_indent_padding(write) delimnl = ',\n' + ' ' * indent delim = '' width = max_width = self._width - indent + 1 @@ -468,6 +643,8 @@ def _format_items(self, items, stream, indent, allowance, context, level): self._format(ent, stream, indent, allowance if last else 1, context, level) + if last and self._expand: + write(',') def _repr(self, object, context, level): repr, readable, recursive = self.format(object, context.copy(), @@ -491,9 +668,13 @@ def _pprint_default_dict(self, object, stream, indent, allowance, context, level return rdf = self._repr(object.default_factory, context, level) cls = object.__class__ - indent += len(cls.__name__) + 1 - stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent)) - self._pprint_dict(object, stream, indent, allowance + 1, context, level) + if self._expand: + stream.write('%s(%s, ' % (cls.__name__, rdf)) + else: + indent += len(cls.__name__) + 1 + stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent)) + self._pprint_dict(object, stream, indent, allowance + 1, context, + level) stream.write(')') _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict @@ -503,14 +684,18 @@ def _pprint_counter(self, object, stream, indent, allowance, context, level): stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '({') - if self._indent_per_level > 1: - stream.write((self._indent_per_level - 1) * ' ') + stream.write(self._format_block_start(cls.__name__ + '({', indent)) + self._write_indent_padding(stream.write) items = object.most_common() - self._format_dict_items(items, stream, - indent + len(cls.__name__) + 1, allowance + 2, - context, level) - stream.write('})') + self._format_dict_items( + items, + stream, + self._child_indent(indent, len(cls.__name__) + 1), + allowance + 2, + context, + level, + ) + stream.write(self._format_block_end('})', indent)) _dispatch[_collections.Counter.__repr__] = _pprint_counter @@ -519,12 +704,18 @@ def _pprint_chain_map(self, object, stream, indent, allowance, context, level): stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '(') - indent += len(cls.__name__) + 1 + stream.write(self._format_block_start(cls.__name__ + '(', + indent + self._indent_per_level)) + if self._expand: + indent += self._indent_per_level + else: + indent += len(cls.__name__) + 1 for i, m in enumerate(object.maps): if i == len(object.maps) - 1: self._format(m, stream, indent, allowance + 1, context, level) - stream.write(')') + if self._expand: + stream.write(',') + stream.write(self._format_block_end(')', indent - self._indent_per_level)) else: self._format(m, stream, indent, 1, context, level) stream.write(',\n' + ' ' * indent) @@ -536,18 +727,21 @@ def _pprint_deque(self, object, stream, indent, allowance, context, level): stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '(') - indent += len(cls.__name__) + 1 - stream.write('[') + stream.write(self._format_block_start(cls.__name__ + '([', indent)) + if not self._expand: + indent += len(cls.__name__) + 1 if object.maxlen is None: self._format_items(object, stream, indent, allowance + 2, context, level) - stream.write('])') + stream.write(self._format_block_end('])', indent)) else: self._format_items(object, stream, indent, 2, context, level) rml = self._repr(object.maxlen, context, level) - stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml)) + if self._expand: + stream.write('%s], maxlen=%s)' % ('\n' + ' ' * indent, rml)) + else: + stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml)) _dispatch[_collections.deque.__repr__] = _pprint_deque @@ -566,6 +760,61 @@ def _pprint_user_string(self, object, stream, indent, allowance, context, level) _dispatch[_collections.UserString.__repr__] = _pprint_user_string + def _pprint_template(self, object, stream, indent, allowance, context, level): + cls_name = object.__class__.__name__ + if self._expand: + indent += self._indent_per_level + else: + indent += len(cls_name) + 1 + + items = ( + ("strings", object.strings), + ("interpolations", object.interpolations), + ) + stream.write(self._format_block_start(cls_name + "(", indent)) + self._format_namespace_items( + items, stream, indent, allowance, context, level + ) + stream.write( + self._format_block_end(")", indent - self._indent_per_level) + ) + + def _pprint_interpolation(self, object, stream, indent, allowance, context, level): + cls_name = object.__class__.__name__ + if self._expand: + indent += self._indent_per_level + items = ( + ("value", object.value), + ("expression", object.expression), + ("conversion", object.conversion), + ("format_spec", object.format_spec), + ) + stream.write(self._format_block_start(cls_name + "(", indent)) + self._format_namespace_items( + items, stream, indent, allowance, context, level + ) + stream.write( + self._format_block_end(")", indent - self._indent_per_level) + ) + else: + indent += len(cls_name) + items = ( + object.value, + object.expression, + object.conversion, + object.format_spec, + ) + stream.write(cls_name + "(") + self._format_items( + items, stream, indent, allowance, context, level + ) + stream.write(")") + + t = t"{0}" + _dispatch[type(t).__repr__] = _pprint_template + _dispatch[type(t.interpolations[0]).__repr__] = _pprint_interpolation + del t + def _safe_repr(self, object, context, maxlevels, level): # Return triple (repr_string, isreadable, isrecursive). typ = type(object) @@ -580,12 +829,21 @@ def _safe_repr(self, object, context, maxlevels, level): else: return repr(object), True, False - if issubclass(typ, dict) and r is dict.__repr__: + if ((issubclass(typ, dict) and r is dict.__repr__) + or (issubclass(typ, frozendict) and r is frozendict.__repr__)): + is_frozendict = issubclass(typ, frozendict) if not object: - return "{}", True, False + if is_frozendict: + rep = f"{object.__class__.__name__}()" + else: + rep = "{}" + return rep, True, False objid = id(object) if maxlevels and level >= maxlevels: - return "{...}", False, objid in context + rep = "{...}" + if is_frozendict: + rep = f"{object.__class__.__name__}({rep})" + return rep, False, objid in context if objid in context: return _recursion(object), False, True context[objid] = 1 @@ -608,7 +866,10 @@ def _safe_repr(self, object, context, maxlevels, level): if krecur or vrecur: recursive = True del context[objid] - return "{%s}" % ", ".join(components), readable, recursive + rep = "{%s}" % ", ".join(components) + if is_frozendict: + rep = f"{object.__class__.__name__}({rep})" + return rep, readable, recursive if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__): @@ -644,6 +905,42 @@ def _safe_repr(self, object, context, maxlevels, level): del context[objid] return format % ", ".join(components), readable, recursive + 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 + if objid in context: + return _recursion(object), False, True + key = _safe_key + if issubclass(typ, (self._dict_items_view, _collections.abc.ItemsView)): + key = _safe_tuple + 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 + readable = True + recursive = False + components = [] + append = components.append + level += 1 + for val in object: + vrepr, vreadable, vrecur = self.format( + val, context, maxlevels, level) + append(vrepr) + readable = readable and vreadable + if vrecur: + recursive = True + del context[objid] + return typ.__name__ + '([%s])' % ", ".join(components), readable, recursive + rep = repr(object) return rep, (rep and not rep.startswith('<')), False diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 403d2e90084..dcb1cd69307 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -10,6 +10,7 @@ import re import types import unittest +from collections.abc import ItemsView, KeysView, Mapping, MappingView, ValuesView from test.support import cpython_only from test.support.import_helper import ensure_lazy_imports @@ -66,10 +67,25 @@ class dict3(dict): def __repr__(self): return dict.__repr__(self) +class frozendict2(frozendict): + pass + +class frozendict3(frozendict): + def __repr__(self): + return frozendict.__repr__(self) + 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 @@ -148,6 +164,7 @@ def test_init(self): self.assertRaises(ValueError, pprint.PrettyPrinter, depth=0) self.assertRaises(ValueError, pprint.PrettyPrinter, depth=-1) self.assertRaises(ValueError, pprint.PrettyPrinter, width=0) + self.assertRaises(ValueError, pprint.PrettyPrinter, compact=True, expand=True) def test_basic(self): # Verify .isrecursive() and .isreadable() w/o recursion @@ -180,10 +197,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") @@ -191,10 +215,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,)) @@ -237,15 +262,24 @@ def test_same_as_repr(self): set(), set2(), set3(), frozenset(), frozenset2(), frozenset3(), {}, dict2(), dict3(), + frozendict(), frozendict2(), frozendict3(), + {}.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}, - (1,2), [3,4], {5: 6}, + (1,2), [3,4], tuple2((1,2)), tuple3((1,2)), tuple3(range(100)), [3,4], list2([3,4]), list3([3,4]), list3(range(100)), set({7}), set2({7}), set3({7}), frozenset({8}), frozenset2({8}), frozenset3({8}), - dict2({5: 6}), dict3({5: 6}), + {5: 6}, dict2({5: 6}), dict3({5: 6}), + frozendict({5: 6}), frozendict2({5: 6}), frozendict3({5: 6}), + {5: 6}.keys(), {5: 6}.values(), {5: 6}.items(), + frozendict({5: 6}).keys(), frozendict({5: 6}).values(), + frozendict({5: 6}).items(), + MappingView({5: 6}), KeysView({5: 6}), + ItemsView({5: 6}), ValuesView({5: 6}), range(10, -11, -1), True, False, None, ..., ): @@ -275,6 +309,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) @@ -302,6 +342,81 @@ def test_basic_line_wrap(self): for type in [dict, dict2]: self.assertEqual(pprint.pformat(type(o)), exp) + exp = """\ +frozendict({'RPM_cal': 0, + 'RPM_cal2': 48059, + 'Speed_cal': 0, + 'controldesk_runtime_us': 0, + 'main_code_runtime_us': 0, + 'read_io_runtime_us': 0, + 'write_io_runtime_us': 43690})""" + self.assertEqual(pprint.pformat(frozendict(o)), exp) + exp = """\ +frozendict2({'RPM_cal': 0, + 'RPM_cal2': 48059, + 'Speed_cal': 0, + 'controldesk_runtime_us': 0, + 'main_code_runtime_us': 0, + 'read_io_runtime_us': 0, + 'write_io_runtime_us': 43690})""" + self.assertEqual(pprint.pformat(frozendict2(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) + keys = frozendict.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) + values = frozendict({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) + items = frozendict({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 = '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]: @@ -425,6 +540,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() @@ -453,6 +592,152 @@ def test_mapping_proxy(self): ('lazy', 7), ('dog', 8)]))""") + def test_dict_views(self): + 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 = "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 + 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_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} + # 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"): + 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(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), + 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(MV(d), sort_dicts=False), + MV.__name__ + joined_items) + 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_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): + 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): + 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) @@ -768,6 +1053,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}), @@ -1010,6 +1299,12 @@ def test_counter(self): 'e': 4, 'n': 2, 'l': 1})""") + self.assertEqual(pprint.pformat(d, indent=2, width=1), +"""\ +Counter({ 's': 6, + 'e': 4, + 'n': 2, + 'l': 1})""") def test_chainmap(self): d = collections.ChainMap() @@ -1049,6 +1344,66 @@ def test_chainmap(self): ('a', 6), ('lazy', 7), ('dog', 8)]))""") + self.assertEqual(pprint.pformat(d.keys()), +"""\ +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(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(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() @@ -1096,6 +1451,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() @@ -1130,6 +1515,564 @@ def test_user_string(self): 'jumped over a ' 'lazy dog'}""") + def test_template(self): + d = t"" + self.assertEqual(pprint.pformat(d), + "Template(strings=('',), interpolations=())") + self.assertEqual(pprint.pformat(d), repr(d)) + self.assertEqual(pprint.pformat(d, width=1), +"""\ +Template(strings=('',), + interpolations=())""") + name = "World" + d = t"Hello {name}" + self.assertEqual(pprint.pformat(d), +"""\ +Template(strings=('Hello ', ''), + interpolations=(Interpolation('World', 'name', None, ''),))""") + ver = {3.13: False, 3.14: True} + d = t"Hello { {"name": "Python", "version": ver}!s:z}!" + self.assertEqual(pprint.pformat(d, width=1), +"""\ +Template(strings=('Hello ', + '!'), + interpolations=(Interpolation({'name': 'Python', + 'version': {3.13: False, + 3.14: True}}, + ' ' + '{"name": ' + '"Python", ' + '"version": ' + 'ver}', + 's', + 'z'),))""") + + def test_expand_template(self): + d = t"" + self.assertEqual( + pprint.pformat(d, expand=True), + "Template(strings=('',), interpolations=())", + ) + name = "World" + d = t"Hello {name}" + self.assertEqual( + pprint.pformat(d, width=40, indent=4, expand=True), + """\ +Template( + strings=('Hello ', ''), + interpolations=( + Interpolation( + value='World', + expression='name', + conversion=None, + format_spec='', + ), + ), +)""", + ) + ver = {3.13: False, 3.14: True} + d = t"Hello { {"name": "Python", "version": ver}!s:z}!" + self.assertEqual( + pprint.pformat(d, width=40, indent=4, expand=True), + """\ +Template( + strings=('Hello ', '!'), + interpolations=( + Interpolation( + value={ + 'name': 'Python', + 'version': { + 3.13: False, + 3.14: True, + }, + }, + expression=' {"name": "Python", ' + '"version": ver}', + conversion='s', + format_spec='z', + ), + ), +)""", + ) + + def test_expand_dataclass(self): + @dataclasses.dataclass + class DummyDataclass: + foo: str + bar: float + baz: bool + qux: dict = dataclasses.field(default_factory=dict) + quux: list = dataclasses.field(default_factory=list) + corge: int = 1 + garply: tuple = (1, 2, 3, 4) + dummy_dataclass = DummyDataclass( + foo="foo", + bar=1.2, + baz=False, + qux={"foo": "bar", "baz": 123}, + quux=["foo", "bar", "baz"], + corge=7, + garply=(1, 2, 3, 4), + ) + self.assertEqual(pprint.pformat(dummy_dataclass, width=40, indent=4, + expand=True), +"""\ +DummyDataclass( + foo='foo', + bar=1.2, + baz=False, + qux={'baz': 123, 'foo': 'bar'}, + quux=['foo', 'bar', 'baz'], + corge=7, + garply=(1, 2, 3, 4), +)""") + + def test_expand_dict(self): + dummy_dict = { + "foo": "bar", + "baz": 123, + "qux": {"foo": "bar", "baz": 123}, + "quux": ["foo", "bar", "baz"], + "corge": 7, + } + self.assertEqual(pprint.pformat(dummy_dict, width=40, indent=4, + expand=True, sort_dicts=False), +"""\ +{ + 'foo': 'bar', + 'baz': 123, + 'qux': {'foo': 'bar', 'baz': 123}, + 'quux': ['foo', 'bar', 'baz'], + 'corge': 7, +}""") + + def test_expand_respects_width_with_long_keys(self): + # gh-155596: in expand mode the width of the "key: " prefix was not + # counted when deciding whether a value fits on the current line, so + # values under long keys could overflow width. + obj = {'a' * 12: 1, 'b' * 20: 2, 'c' * 30: {'d' * 5: 3, 'e' * 40: 3}} + result = pprint.pformat(obj, expand=True) + self.assertEqual(result, +"""\ +{ + 'aaaaaaaaaaaa': 1, + 'bbbbbbbbbbbbbbbbbbbb': 2, + 'cccccccccccccccccccccccccccccc': { + 'ddddd': 3, + 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 3, + }, +}""") + # The nested value must be broken up rather than overflowing. + self.assertTrue(all(len(line) <= 80 for line in result.splitlines()), + max(result.splitlines(), key=len)) + + def test_expand_ordered_dict(self): + dummy_ordered_dict = collections.OrderedDict( + [ + ("foo", 1), + ("bar", 12), + ("baz", 123), + ] + ) + self.assertEqual(pprint.pformat(dummy_ordered_dict, width=20, indent=4, + expand=True), +"""\ +OrderedDict([ + ('foo', 1), + ('bar', 12), + ('baz', 123), +])""") + + def test_expand_list(self): + dummy_list = [ + "foo", + "bar", + "baz", + "qux", + ] + self.assertEqual(pprint.pformat(dummy_list, width=20, indent=4, + expand=True), +"""\ +[ + 'foo', + 'bar', + 'baz', + 'qux', +]""") + + def test_expand_tuple(self): + dummy_tuple = ( + "foo", + "bar", + "baz", + 4, + 5, + 6, + ) + self.assertEqual(pprint.pformat(dummy_tuple, width=20, indent=4, + expand=True), +"""\ +( + 'foo', + 'bar', + 'baz', + 4, + 5, + 6, +)""") + + def test_expand_single_element_tuple(self): + self.assertEqual( + pprint.pformat((1,), width=1, indent=4, expand=True), + """\ +( + 1, +)""") + + def test_expand_set(self): + dummy_set = { + "foo", + "bar", + "baz", + "qux", + (1, 2, 3), + } + self.assertEqual(pprint.pformat(dummy_set, width=20, indent=4, + expand=True), +"""\ +{ + 'bar', + 'baz', + 'foo', + 'qux', + (1, 2, 3), +}""") + + def test_expand_frozenset(self): + dummy_set = { + (1, 2, 3), + } + dummy_frozenset = frozenset( + { + "foo", + "bar", + "baz", + (1, 2, 3), + frozenset(dummy_set), + } + ) + self.assertEqual(pprint.pformat(dummy_frozenset, width=40, indent=4, + expand=True), +"""\ +frozenset({ + frozenset({(1, 2, 3)}), + 'bar', + 'baz', + 'foo', + (1, 2, 3), +})""") + + def test_expand_frozendict(self): + dummy_frozendict = frozendict( + {"foo": "bar", "baz": 123, "qux": [1, 2]} + ) + self.assertEqual( + pprint.pformat(dummy_frozendict, width=20, indent=4, expand=True), + """\ +frozendict({ + 'baz': 123, + 'foo': 'bar', + 'qux': [1, 2], +})""", + ) + + def test_expand_bytes(self): + dummy_bytes = b"Hello world! foo bar baz 123 456 789" + self.assertEqual(pprint.pformat(dummy_bytes, width=20, indent=4, + expand=True), +"""\ +( + b'Hello world!' + b' foo bar baz' + b' 123 456 789' +)""") + + def test_expand_bytearray(self): + dummy_bytes = b"Hello world! foo bar baz 123 456 789" + dummy_byte_array = bytearray(dummy_bytes) + self.assertEqual(pprint.pformat(dummy_byte_array, width=40, indent=4, + expand=True), +"""\ +bytearray( + b'Hello world! foo bar baz 123 456' + b' 789' +)""") + + def test_expand_mappingproxy(self): + dummy_dict = { + "foo": "bar", + "baz": 123, + "qux": {"foo": "bar", "baz": 123}, + "quux": ["foo", "bar", "baz"], + "corge": 7, + } + dummy_mappingproxy = types.MappingProxyType(dummy_dict) + self.assertEqual(pprint.pformat(dummy_mappingproxy, width=40, indent=4, + expand=True), +"""\ +mappingproxy({ + 'baz': 123, + 'corge': 7, + 'foo': 'bar', + 'quux': ['foo', 'bar', 'baz'], + 'qux': {'baz': 123, 'foo': 'bar'}, +})""") + + def test_expand_namespace(self): + dummy_namespace = types.SimpleNamespace( + foo="bar", + bar=42, + baz=types.SimpleNamespace( + x=321, + y="string", + d={"foo": True, "bar": "baz"}, + ), + ) + + self.assertEqual(pprint.pformat(dummy_namespace, width=40, indent=4, + expand=True), +"""\ +namespace( + foo='bar', + bar=42, + baz=namespace( + x=321, + y='string', + d={'bar': 'baz', 'foo': True}, + ), +)""") + + def test_expand_defaultdict(self): + dummy_defaultdict = collections.defaultdict(list) + dummy_defaultdict["foo"].append("bar") + dummy_defaultdict["foo"].append("baz") + dummy_defaultdict["foo"].append("qux") + dummy_defaultdict["bar"] = {"foo": "bar", "baz": None} + self.assertEqual(pprint.pformat(dummy_defaultdict, width=40, indent=4, + expand=True), +"""\ +defaultdict(, { + 'bar': {'baz': None, 'foo': 'bar'}, + 'foo': ['bar', 'baz', 'qux'], +})""") + + def test_expand_counter(self): + dummy_counter = collections.Counter("abcdeabcdabcaba") + expected = """\ +Counter({ + 'a': 5, + 'b': 4, + 'c': 3, + 'd': 2, + 'e': 1, +})""" + self.assertEqual(pprint.pformat(dummy_counter, width=40, indent=4, + expand=True), expected) + + expected2 = """\ +Counter({ + 'a': 5, + 'b': 4, + 'c': 3, + 'd': 2, + 'e': 1, +})""" + self.assertEqual(pprint.pformat(dummy_counter, width=20, indent=2, + expand=True), expected2) + + def test_expand_chainmap(self): + dummy_dict = { + "foo": "bar", + "baz": 123, + "qux": {"foo": "bar", "baz": 123}, + "quux": ["foo", "bar", "baz"], + "corge": 7, + } + dummy_chainmap = collections.ChainMap( + {"foo": "bar"}, + {"baz": "qux"}, + {"corge": dummy_dict}, + ) + dummy_chainmap.maps.append({"garply": "waldo"}) + self.assertEqual(pprint.pformat(dummy_chainmap, width=40, indent=4, + expand=True), +"""\ +ChainMap( + {'foo': 'bar'}, + {'baz': 'qux'}, + { + 'corge': { + 'baz': 123, + 'corge': 7, + 'foo': 'bar', + 'quux': [ + 'foo', + 'bar', + 'baz', + ], + 'qux': { + 'baz': 123, + 'foo': 'bar', + }, + }, + }, + {'garply': 'waldo'}, +)""") + + def test_expand_deque(self): + dummy_dict = { + "foo": "bar", + "baz": 123, + "qux": {"foo": "bar", "baz": 123}, + "quux": ["foo", "bar", "baz"], + "corge": 7, + } + dummy_list = [ + "foo", + "bar", + "baz", + ] + dummy_set = { + (1, 2, 3), + } + dummy_deque = collections.deque(maxlen=10) + dummy_deque.append("foo") + dummy_deque.append(123) + dummy_deque.append(dummy_dict) + dummy_deque.extend(dummy_list) + dummy_deque.appendleft(dummy_set) + self.assertEqual(pprint.pformat(dummy_deque, width=40, indent=4, + expand=True), +"""\ +deque([ + {(1, 2, 3)}, + 'foo', + 123, + { + 'baz': 123, + 'corge': 7, + 'foo': 'bar', + 'quux': ['foo', 'bar', 'baz'], + 'qux': { + 'baz': 123, + 'foo': 'bar', + }, + }, + 'foo', + 'bar', + 'baz', +], maxlen=10)""") + + def test_expand_userdict(self): + class DummyUserDict(collections.UserDict): + """A custom UserDict with some extra attributes""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.access_count = 0 + dummy_userdict = DummyUserDict({ "foo": "bar", "baz": 123, + "qux": {"foo": "bar", "baz": 123}, + "quux": ["foo", "bar", "baz"], + "corge": 7 }) + dummy_userdict.access_count = 5 + + self.assertEqual(pprint.pformat(dummy_userdict, width=40, indent=4, + expand=True), +"""\ +{ + 'baz': 123, + 'corge': 7, + 'foo': 'bar', + 'quux': ['foo', 'bar', 'baz'], + 'qux': {'baz': 123, 'foo': 'bar'}, +}""") + + def test_expand_userlist(self): + class DummyUserList(collections.UserList): + """A custom UserList with some extra attributes""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.description = "foo" + dummy_userlist = DummyUserList(["first", 2, {"key": "value"}, + [4, 5, 6]]) + + self.assertEqual(pprint.pformat(dummy_userlist, width=40, indent=4, + expand=True), +"""\ +[ + 'first', + 2, + {'key': 'value'}, + [4, 5, 6], +]""") + + def test_expand_dict_keys(self): + d = {"foo": 1, "bar": 2, "baz": 3, "qux": 4, "quux": 5} + self.assertEqual( + pprint.pformat(d.keys(), width=20, indent=4, expand=True), + """\ +dict_keys([ + 'bar', + 'baz', + 'foo', + 'quux', + 'qux', +])""", + ) + + def test_expand_dict_values(self): + d = {"foo": 1, "bar": 2, "baz": 3, "qux": 4, "quux": 5} + self.assertEqual( + pprint.pformat(d.values(), width=20, indent=4, expand=True), + """\ +dict_values([ + 1, + 2, + 3, + 4, + 5, +])""", + ) + + def test_expand_dict_items(self): + d = {"foo": 1, "bar": 2, "baz": 3, "qux": 4, "quux": 5} + self.assertEqual( + pprint.pformat(d.items(), width=20, indent=4, expand=True), + """\ +dict_items([ + ('bar', 2), + ('baz', 3), + ('foo', 1), + ('quux', 5), + ('qux', 4), +])""", + ) + + def test_expand_str(self): + s = "The quick brown fox jumped over the lazy dog " * 3 + self.assertEqual( + pprint.pformat(s, width=40, indent=4, expand=True), + """\ +( + 'The quick brown fox jumped over ' + 'the lazy dog The quick brown fox ' + 'jumped over the lazy dog The ' + 'quick brown fox jumped over the ' + 'lazy dog ' +)""", + ) + class DottedPrettyPrinter(pprint.PrettyPrinter): diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 78359770e08..24fedfcc6f5 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -5,7 +5,6 @@ use super::{ }; use crate::common::lock::LazyLock; use crate::object::{Traverse, TraverseFn}; -use crate::stdlib::PyOrderedDictItems; use crate::{ AsObject, Context, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, atomic_func, @@ -1761,7 +1760,6 @@ trait DictView: PyPayload + PyClassDef + Iterable + Representable { type ReverseIter: PyPayload + core::fmt::Debug; fn dict(&self) -> &PyAnyDict; - fn item(vm: &VirtualMachine, key: PyObjectRef, value: PyObjectRef) -> PyObjectRef; fn __len__(&self) -> usize { self.dict().__len__() @@ -1805,10 +1803,6 @@ macro_rules! dict_view { &self.dict } - fn item(vm: &VirtualMachine, key: PyObjectRef, value: PyObjectRef) -> PyObjectRef { - $result_fn(vm, $project_fn(&key, &value)) - } - fn __reversed__(zelf: &Py) -> Self::ReverseIter { $reverse_iter_name::new(zelf.dict.clone()) } @@ -1830,19 +1824,21 @@ macro_rules! dict_view { #[inline] fn repr(zelf: &Py, vm: &VirtualMachine) -> PyResult { let s = if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) { - let mut result = Wtf8Buf::from(format!("{}([", Self::NAME)); - let mut first = true; - for (key, value) in zelf.dict() { - if !first { - result.push_str(", "); - } - first = false; - result.push_wtf8(Self::item(vm, key, value).repr(vm)?.as_wtf8()); - } - result.push_str("])"); + // CPython snapshots through the view's actual iterator before + // calling element representations, including for OrderedDict subclasses. + let items = ArgIterable::::try_from_object( + vm, + zelf.as_object().to_owned(), + )? + .iter(vm)? + .collect::>>()?; + let list_repr = vm.ctx.new_list(items).as_object().repr(vm)?; + let mut result = Wtf8Buf::from(format!("{}(", zelf.class().slot_name())); + result.push_wtf8(list_repr.as_wtf8()); + result.push_str(")"); vm.ctx.new_str(result) } else { - vm.ctx.intern_str("{...}").to_owned() + vm.ctx.intern_str("...").to_owned() }; Ok(s) } @@ -2432,10 +2428,7 @@ pub(crate) fn set_item_view_number_xor( } fn dict_item_view_number_xor(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyResult { - let is_native_item_view = |obj: &PyObject| { - obj.downcast_ref::().is_some() - || obj.downcast_ref::().is_some() - }; + let is_native_item_view = |obj: &PyObject| obj.downcast_ref::().is_some(); set_item_view_number_xor(a, b, is_native_item_view(a) && is_native_item_view(b), vm) } diff --git a/crates/vm/src/stdlib/_collections/ordered_dict.rs b/crates/vm/src/stdlib/_collections/ordered_dict.rs index 568658b846a..1a2222c7bba 100644 --- a/crates/vm/src/stdlib/_collections/ordered_dict.rs +++ b/crates/vm/src/stdlib/_collections/ordered_dict.rs @@ -7,18 +7,15 @@ pub(crate) mod ordered_dict { AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, atomic_func, builtins::{ - PyDict, PyGenericAlias, PyMappingProxy, PyStrRef, PyTuple, PyTypeRef, - dict::{ - PyDictItems, set_inner_number_or, set_inner_number_subtract, set_inner_number_xor, - set_item_view_number_xor, set_view_number_and, - }, + PyAnyDict, PyDict, PyGenericAlias, PyTypeRef, + dict::{PyDictItems, PyDictKeys, PyDictValues}, iter::builtins_iter, }, class::StaticType, common::{hash::PyHash, lock::PyMutex}, convert::ToPyObject, dict_inner::DictKey, - function::{ArgIterable, FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue}, + function::{FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue}, object::{Traverse, TraverseFn}, protocol::{PyIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, recursion::ReprGuard, @@ -27,7 +24,6 @@ pub(crate) mod ordered_dict { Initializer, IterNext, Iterable, PyComparisonOp, Representable, SelfIter, }, }; - use rustpython_common::wtf8::Wtf8Buf; use std::collections::HashMap; const ODICT_ITER_REVERSED: u8 = 1; @@ -531,17 +527,17 @@ pub(crate) mod ordered_dict { #[pymethod] fn keys(zelf: PyRef, vm: &VirtualMachine) -> PyObjectRef { - PyODictKeys { od: zelf }.to_pyobject(vm) + PyODictKeys::new(zelf).to_pyobject(vm) } #[pymethod] fn values(zelf: PyRef, vm: &VirtualMachine) -> PyObjectRef { - PyODictValues { od: zelf }.to_pyobject(vm) + PyODictValues::new(zelf).to_pyobject(vm) } #[pymethod] fn items(zelf: PyRef, vm: &VirtualMachine) -> PyObjectRef { - PyOrderedDictItems { od: zelf }.to_pyobject(vm) + PyOrderedDictItems::new(zelf).to_pyobject(vm) } #[pymethod] @@ -928,383 +924,129 @@ pub(crate) mod ordered_dict { } } - fn odict_view_eq( - od: &PyRef, - kind: u8, - other: &PyObject, - vm: &VirtualMachine, - ) -> PyResult { - if kind == ODICT_ITER_VALUES { - return Ok(PyArithmeticValue::NotImplemented); - } - let set_like = other.fast_isinstance(vm.ctx.types.set_type) - || other.fast_isinstance(vm.ctx.types.frozenset_type) - || other.class().is(vm.ctx.types.dict_keys_type) - || other.class().is(vm.ctx.types.dict_items_type) - || other.downcast_ref::().is_some() - || other.downcast_ref::().is_some(); - if !set_like { - return Ok(PyArithmeticValue::NotImplemented); - } - let Ok(other_len) = other.length(vm) else { - return Ok(PyArithmeticValue::NotImplemented); - }; - if od.dict.__len__() != other_len { - return Ok(PyArithmeticValue::Implemented(false)); - } - let iter_obj = PyODictIter::new(od.clone(), kind).to_pyobject(vm); - let a_iter = PyIter::try_from_object(vm, iter_obj)?; - for item in a_iter.iter::(vm)? { - let item = item?; - if !other.sequence_unchecked().contains(&item, vm)? { - return Ok(PyArithmeticValue::Implemented(false)); - } - } - Ok(PyArithmeticValue::Implemented(true)) - } - - fn ordered_view_number_and(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyResult { - let a_is_view = a.downcast_ref::().is_some() - || a.downcast_ref::().is_some(); - let (view, other) = if a_is_view { (a, b) } else { (b, a) }; - set_view_number_and(view, other, vm) - } - - fn ordered_item_view_number_xor(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyResult { - let is_item_view = |obj: &PyObject| { - obj.downcast_ref::().is_some() - || obj.downcast_ref::().is_some() - }; - set_item_view_number_xor(a, b, is_item_view(a) && is_item_view(b), vm) - } - - fn ordered_view_repr( - view: &PyObject, - od: &PyRef, - kind: u8, - name: &str, - vm: &VirtualMachine, - ) -> PyResult { - let Some(_guard) = ReprGuard::enter(vm, view) else { - return Ok(vm.ctx.intern_str("...").to_owned()); - }; - // Snapshot the view before calling user-defined repr methods, which - // can mutate the underlying ordered dictionary. - let iterator = PyODictIter::new(od.clone(), kind).to_pyobject(vm); - let items = PyIter::try_from_object(vm, iterator)? - .iter::(vm)? - .collect::>>()?; - let list_repr = vm.ctx.new_list(items).as_object().repr(vm)?; - let mut result = Wtf8Buf::from(format!("{name}(")); - result.push_wtf8(list_repr.as_wtf8()); - result.push_str(")"); - Ok(vm.ctx.new_str(result)) - } - #[pyattr] - #[pyclass(name = "odict_keys", module = "builtins", unhashable = true, traverse)] - #[derive(Debug, PyPayload)] + #[pyclass(name = "odict_keys", module = false, base = PyDictKeys, traverse)] + #[derive(Debug)] struct PyODictKeys { - od: PyRef, + view: PyDictKeys, } - #[pyclass(with(Iterable, Comparable, AsMapping, AsSequence, AsNumber, Representable))] - impl Py { - #[pymethod] - fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { - PyODictIter::new(self.od.clone(), ODICT_ITER_KEYS | ODICT_ITER_REVERSED).to_pyobject(vm) - } - - #[pygetset] - fn mapping(&self, vm: &VirtualMachine) -> PyResult { - PyMappingProxy::from_object(self.od.as_object().to_owned(), vm) - } - - #[pymethod] - fn isdisjoint(&self, other: ArgIterable, vm: &VirtualMachine) -> PyResult { - for item in other.iter(vm)? { - if self.od.dict.inner_getitem_opt(&*item?, vm)?.is_some() { - return Ok(false); - } + impl PyODictKeys { + fn new(od: PyRef) -> Self { + let dict = PyAnyDict::from_object(od.as_object()) + .expect("OrderedDict has a dict payload") + .to_owned(); + Self { + view: PyDictKeys::new(dict), } - Ok(true) } - } - impl Representable for PyODictKeys { - fn repr(zelf: &Py, vm: &VirtualMachine) -> PyResult { - ordered_view_repr( - zelf.as_object(), - &zelf.od, - ODICT_ITER_KEYS, - "odict_keys", - vm, - ) - } - - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - unreachable!("use repr instead") - } - } - - impl AsMapping for PyODictKeys { - fn as_mapping() -> &'static PyMappingMethods { - static AS_MAPPING: PyMappingMethods = PyMappingMethods { - length: atomic_func!(|mapping, _vm| Ok(PyODictKeys::mapping_downcast(mapping) - .od - .dict - .__len__())), - ..PyMappingMethods::NOT_IMPLEMENTED - }; - &AS_MAPPING + fn od(&self) -> &Py { + self.view + .dict + .as_object() + .downcast_ref::() + .expect("ordered dict views own an OrderedDict") } } - impl AsSequence for PyODictKeys { - fn as_sequence() -> &'static PySequenceMethods { - static AS_SEQUENCE: PySequenceMethods = PySequenceMethods { - length: atomic_func!(|seq, _vm| Ok(PyODictKeys::sequence_downcast(seq) - .od - .dict - .__len__())), - contains: atomic_func!(|seq, target, vm| { - PyODictKeys::sequence_downcast(seq) - .od - .dict - .inner_getitem_opt(target, vm) - .map(|v| v.is_some()) - }), - ..PySequenceMethods::NOT_IMPLEMENTED - }; - &AS_SEQUENCE + #[pyclass(flags(DISALLOW_INSTANTIATION), with(Iterable))] + impl Py { + #[pymethod] + fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { + PyODictIter::new(self.od().to_owned(), ODICT_ITER_KEYS | ODICT_ITER_REVERSED) + .to_pyobject(vm) } } impl Iterable for PyODictKeys { fn iter(zelf: PyRef, vm: &VirtualMachine) -> PyResult { - Ok(PyODictIter::new(zelf.od.clone(), ODICT_ITER_KEYS).to_pyobject(vm)) - } - } - - impl Comparable for PyODictKeys { - fn cmp( - zelf: &Py, - other: &PyObject, - op: PyComparisonOp, - vm: &VirtualMachine, - ) -> PyResult { - op.eq_only(|| odict_view_eq(&zelf.od, ODICT_ITER_KEYS, other, vm)) - } - } - - impl AsNumber for PyODictKeys { - fn as_number() -> &'static PyNumberMethods { - static AS_NUMBER: PyNumberMethods = PyNumberMethods { - subtract: Some(set_inner_number_subtract), - and: Some(ordered_view_number_and), - xor: Some(set_inner_number_xor), - or: Some(set_inner_number_or), - ..PyNumberMethods::NOT_IMPLEMENTED - }; - &AS_NUMBER + Ok(PyODictIter::new(zelf.od().to_owned(), ODICT_ITER_KEYS).to_pyobject(vm)) } } #[pyattr] - #[pyclass(name = "odict_values", module = "builtins", traverse)] - #[derive(Debug, PyPayload)] + #[pyclass(name = "odict_values", module = false, base = PyDictValues, traverse)] + #[derive(Debug)] struct PyODictValues { - od: PyRef, + view: PyDictValues, } - #[pyclass(with(Iterable, Comparable, AsMapping, Representable))] - impl Py { - #[pymethod] - fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { - PyODictIter::new(self.od.clone(), ODICT_ITER_VALUES | ODICT_ITER_REVERSED) - .to_pyobject(vm) + impl PyODictValues { + fn new(od: PyRef) -> Self { + let dict = PyAnyDict::from_object(od.as_object()) + .expect("OrderedDict has a dict payload") + .to_owned(); + Self { + view: PyDictValues::new(dict), + } } - #[pygetset] - fn mapping(&self, vm: &VirtualMachine) -> PyResult { - PyMappingProxy::from_object(self.od.as_object().to_owned(), vm) + fn od(&self) -> &Py { + self.view + .dict + .as_object() + .downcast_ref::() + .expect("ordered dict views own an OrderedDict") } } - impl Representable for PyODictValues { - fn repr(zelf: &Py, vm: &VirtualMachine) -> PyResult { - ordered_view_repr( - zelf.as_object(), - &zelf.od, - ODICT_ITER_VALUES, - "odict_values", - vm, + #[pyclass(flags(DISALLOW_INSTANTIATION), with(Iterable))] + impl Py { + #[pymethod] + fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { + PyODictIter::new( + self.od().to_owned(), + ODICT_ITER_VALUES | ODICT_ITER_REVERSED, ) - } - - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - unreachable!("use repr instead") - } - } - - impl AsMapping for PyODictValues { - fn as_mapping() -> &'static PyMappingMethods { - static AS_MAPPING: PyMappingMethods = PyMappingMethods { - length: atomic_func!(|mapping, _vm| Ok(PyODictValues::mapping_downcast(mapping) - .od - .dict - .__len__())), - ..PyMappingMethods::NOT_IMPLEMENTED - }; - &AS_MAPPING + .to_pyobject(vm) } } impl Iterable for PyODictValues { fn iter(zelf: PyRef, vm: &VirtualMachine) -> PyResult { - Ok(PyODictIter::new(zelf.od.clone(), ODICT_ITER_VALUES).to_pyobject(vm)) - } - } - - impl Comparable for PyODictValues { - fn cmp( - zelf: &Py, - other: &PyObject, - op: PyComparisonOp, - vm: &VirtualMachine, - ) -> PyResult { - op.eq_only(|| odict_view_eq(&zelf.od, ODICT_ITER_VALUES, other, vm)) + Ok(PyODictIter::new(zelf.od().to_owned(), ODICT_ITER_VALUES).to_pyobject(vm)) } } #[pyattr] - #[pyclass(name = "odict_items", module = "builtins", unhashable = true, traverse)] - #[derive(Debug, PyPayload)] - pub(crate) struct PyOrderedDictItems { - od: PyRef, + #[pyclass(name = "odict_items", module = false, base = PyDictItems, traverse)] + #[derive(Debug)] + struct PyOrderedDictItems { + view: PyDictItems, } - #[pyclass(with(Iterable, Comparable, AsMapping, AsSequence, AsNumber, Representable))] - impl Py { - #[pymethod] - fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { - PyODictIter::new(self.od.clone(), ODICT_ITER_ITEMS | ODICT_ITER_REVERSED) - .to_pyobject(vm) - } - - #[pygetset] - fn mapping(&self, vm: &VirtualMachine) -> PyResult { - PyMappingProxy::from_object(self.od.as_object().to_owned(), vm) - } - - #[pymethod] - fn isdisjoint(&self, other: ArgIterable, vm: &VirtualMachine) -> PyResult { - for item in other.iter(vm)? { - let item = item?; - let Some(needle) = item.downcast_ref::() else { - continue; - }; - if needle.as_slice().len() != 2 { - continue; - } - let Some(found) = self.od.dict.inner_getitem_opt(&*needle.as_slice()[0], vm)? - else { - continue; - }; - if vm.identical_or_equal(&found, &needle.as_slice()[1])? { - return Ok(false); - } + impl PyOrderedDictItems { + fn new(od: PyRef) -> Self { + let dict = PyAnyDict::from_object(od.as_object()) + .expect("OrderedDict has a dict payload") + .to_owned(); + Self { + view: PyDictItems::new(dict), } - Ok(true) - } - } - - impl Representable for PyOrderedDictItems { - fn repr(zelf: &Py, vm: &VirtualMachine) -> PyResult { - ordered_view_repr( - zelf.as_object(), - &zelf.od, - ODICT_ITER_ITEMS, - "odict_items", - vm, - ) - } - - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - unreachable!("use repr instead") } - } - impl AsMapping for PyOrderedDictItems { - fn as_mapping() -> &'static PyMappingMethods { - static AS_MAPPING: PyMappingMethods = PyMappingMethods { - length: atomic_func!(|mapping, _vm| Ok(PyOrderedDictItems::mapping_downcast( - mapping - ) - .od + fn od(&self) -> &Py { + self.view .dict - .__len__())), - ..PyMappingMethods::NOT_IMPLEMENTED - }; - &AS_MAPPING + .as_object() + .downcast_ref::() + .expect("ordered dict views own an OrderedDict") } } - impl AsSequence for PyOrderedDictItems { - fn as_sequence() -> &'static PySequenceMethods { - static AS_SEQUENCE: PySequenceMethods = PySequenceMethods { - length: atomic_func!(|seq, _vm| Ok(PyOrderedDictItems::sequence_downcast(seq) - .od - .dict - .__len__())), - contains: atomic_func!(|seq, target, vm| { - let needle: &Py = match target.downcast_ref() { - Some(needle) => needle, - None => return Ok(false), - }; - if needle.as_slice().len() != 2 { - return Ok(false); - } - let zelf = PyOrderedDictItems::sequence_downcast(seq); - let key = &needle.as_slice()[0]; - let Some(found) = zelf.od.dict.inner_getitem_opt(&**key, vm)? else { - return Ok(false); - }; - vm.identical_or_equal(&found, &needle.as_slice()[1]) - }), - ..PySequenceMethods::NOT_IMPLEMENTED - }; - &AS_SEQUENCE + #[pyclass(flags(DISALLOW_INSTANTIATION), with(Iterable))] + impl Py { + #[pymethod] + fn __reversed__(&self, vm: &VirtualMachine) -> PyObjectRef { + PyODictIter::new(self.od().to_owned(), ODICT_ITER_ITEMS | ODICT_ITER_REVERSED) + .to_pyobject(vm) } } impl Iterable for PyOrderedDictItems { fn iter(zelf: PyRef, vm: &VirtualMachine) -> PyResult { - Ok(PyODictIter::new(zelf.od.clone(), ODICT_ITER_ITEMS).to_pyobject(vm)) - } - } - - impl Comparable for PyOrderedDictItems { - fn cmp( - zelf: &Py, - other: &PyObject, - op: PyComparisonOp, - vm: &VirtualMachine, - ) -> PyResult { - op.eq_only(|| odict_view_eq(&zelf.od, ODICT_ITER_ITEMS, other, vm)) - } - } - - impl AsNumber for PyOrderedDictItems { - fn as_number() -> &'static PyNumberMethods { - static AS_NUMBER: PyNumberMethods = PyNumberMethods { - subtract: Some(set_inner_number_subtract), - and: Some(ordered_view_number_and), - xor: Some(ordered_item_view_number_xor), - or: Some(set_inner_number_or), - ..PyNumberMethods::NOT_IMPLEMENTED - }; - &AS_NUMBER + Ok(PyODictIter::new(zelf.od().to_owned(), ODICT_ITER_ITEMS).to_pyobject(vm)) } } } diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index c89ea3101c2..d5dde116ec1 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -3,7 +3,7 @@ mod _abc; pub(crate) mod _ast; mod _codecs; mod _collections; -pub(crate) use _collections::ordered_dict::ordered_dict::{PyOrderedDict, PyOrderedDictItems}; +pub(crate) use _collections::ordered_dict::ordered_dict::PyOrderedDict; mod _functools; mod _imp; pub mod _io;