Skip to content

Commit 3abf580

Browse files
authored
Display more generators (#88)
* display more generator types, fix #86 * actual test for filter * display of counters * support dataclasses, fix #70
1 parent 259b242 commit 3abf580

File tree

3 files changed

+157
-48
lines changed

3 files changed

+157
-48
lines changed

devtools/prettier.py

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
from collections import OrderedDict
44
from collections.abc import Generator
55

6-
from .utils import env_true, isatty
6+
from .utils import DataClassType, LaxMapping, env_true, isatty
77

88
__all__ = 'PrettyFormat', 'pformat', 'pprint'
99
MYPY = False
1010
if MYPY:
11-
from typing import Any, Union
11+
from typing import Any, Iterable, Union
1212

1313
PARENTHESES_LOOKUP = [
1414
(list, '[', ']'),
@@ -39,6 +39,10 @@ def get_pygments():
3939
return pygments, PythonLexer(), Terminal256Formatter(style='vim')
4040

4141

42+
# common generator types (this is not exhaustive: things like chain are not include to avoid the import)
43+
generator_types = Generator, map, filter, zip, enumerate
44+
45+
4246
class PrettyFormat:
4347
def __init__(
4448
self,
@@ -60,7 +64,11 @@ def __init__(
6064
((str, bytes), self._format_str_bytes),
6165
(tuple, self._format_tuples),
6266
((list, set, frozenset), self._format_list_like),
63-
(Generator, self._format_generators),
67+
(bytearray, self._format_bytearray),
68+
(generator_types, self._format_generator),
69+
# put this last as the check can be slow
70+
(LaxMapping, self._format_dict),
71+
(DataClassType, self._format_dataclass),
6472
]
6573

6674
def __call__(self, value: 'Any', *, indent: int = 0, indent_first: bool = False, highlight: bool = False):
@@ -96,7 +104,7 @@ def _format(self, value: 'Any', indent_current: int, indent_first: bool):
96104
return
97105

98106
value_repr = repr(value)
99-
if len(value_repr) <= self._simple_cutoff and not isinstance(value, Generator):
107+
if len(value_repr) <= self._simple_cutoff and not isinstance(value, generator_types):
100108
self._stream.write(value_repr)
101109
else:
102110
indent_new = indent_current + self._indent_step
@@ -105,18 +113,6 @@ def _format(self, value: 'Any', indent_current: int, indent_first: bool):
105113
func(value, value_repr, indent_current, indent_new)
106114
return
107115

108-
# very blunt check for things that look like dictionaries but do not necessarily inherit from Mapping
109-
# e.g. asyncpg Records
110-
# HELP: are there any other checks we should include here?
111-
if (
112-
hasattr(value, '__getitem__')
113-
and hasattr(value, 'items')
114-
and callable(value.items)
115-
and not type(value) == type
116-
):
117-
self._format_dict(value, value_repr, indent_current, indent_new)
118-
return
119-
120116
self._format_raw(value, value_repr, indent_current, indent_new)
121117

122118
def _render_pretty(self, gen, indent: int):
@@ -139,12 +135,12 @@ def _render_pretty(self, gen, indent: int):
139135
# shouldn't happen but will
140136
self._stream.write(repr(v))
141137

142-
def _format_dict(self, value: 'Any', value_repr: str, indent_current: int, indent_new: int):
138+
def _format_dict(self, value: 'Any', _: str, indent_current: int, indent_new: int):
143139
open_, before_, split_, after_, close_ = '{\n', indent_new * self._c, ': ', ',\n', '}'
144140
if isinstance(value, OrderedDict):
145141
open_, split_, after_, close_ = 'OrderedDict([\n', ', ', '),\n', '])'
146142
before_ += '('
147-
elif not isinstance(value, dict):
143+
elif type(value) != dict:
148144
open_, close_ = '<{}({{\n'.format(value.__class__.__name__), '})>'
149145

150146
self._stream.write(open_)
@@ -156,9 +152,7 @@ def _format_dict(self, value: 'Any', value_repr: str, indent_current: int, inden
156152
self._stream.write(after_)
157153
self._stream.write(indent_current * self._c + close_)
158154

159-
def _format_list_like(
160-
self, value: 'Union[list, tuple, set]', value_repr: str, indent_current: int, indent_new: int
161-
):
155+
def _format_list_like(self, value: 'Union[list, tuple, set]', _: str, indent_current: int, indent_new: int):
162156
open_, close_ = '(', ')'
163157
for t, *oc in PARENTHESES_LOOKUP:
164158
if isinstance(value, t):
@@ -194,15 +188,18 @@ def _format_str_bytes(self, value: 'Union[str, bytes]', value_repr: str, indent_
194188
else:
195189
lines = list(self._wrap_lines(value, indent_new))
196190
if len(lines) > 1:
197-
self._stream.write('(\n')
198-
prefix = indent_new * self._c
199-
for line in lines:
200-
self._stream.write(prefix + repr(line) + '\n')
201-
self._stream.write(indent_current * self._c + ')')
191+
self._str_lines(lines, indent_current, indent_new)
202192
else:
203193
self._stream.write(value_repr)
204194

205-
def _wrap_lines(self, s, indent_new):
195+
def _str_lines(self, lines: 'Iterable[str]', indent_current: int, indent_new: int) -> None:
196+
self._stream.write('(\n')
197+
prefix = indent_new * self._c
198+
for line in lines:
199+
self._stream.write(prefix + repr(line) + '\n')
200+
self._stream.write(indent_current * self._c + ')')
201+
202+
def _wrap_lines(self, s, indent_new) -> 'Generator[str, None, None]':
206203
width = self._width - indent_new - 3
207204
for line in s.splitlines(True):
208205
start = 0
@@ -211,17 +208,38 @@ def _wrap_lines(self, s, indent_new):
211208
start = pos
212209
yield line[start:]
213210

214-
def _format_generators(self, value: Generator, value_repr: str, indent_current: int, indent_new: int):
211+
def _format_generator(self, value: Generator, value_repr: str, indent_current: int, indent_new: int):
215212
if self._repr_generators:
216213
self._stream.write(value_repr)
217214
else:
218-
self._stream.write('(\n')
215+
name = value.__class__.__name__
216+
if name == 'generator':
217+
# no name if the name is just "generator"
218+
self._stream.write('(\n')
219+
else:
220+
self._stream.write(f'{name}(\n')
219221
for v in value:
220222
self._format(v, indent_new, True)
221223
self._stream.write(',\n')
222224
self._stream.write(indent_current * self._c + ')')
223225

224-
def _format_raw(self, value: 'Any', value_repr: str, indent_current: int, indent_new: int):
226+
def _format_bytearray(self, value: 'Any', _: str, indent_current: int, indent_new: int):
227+
self._stream.write('bytearray')
228+
lines = self._wrap_lines(bytes(value), indent_new)
229+
self._str_lines(lines, indent_current, indent_new)
230+
231+
def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int):
232+
from dataclasses import asdict
233+
234+
before_ = indent_new * self._c
235+
self._stream.write(f'{value.__class__.__name__}(\n')
236+
for k, v in asdict(value).items():
237+
self._stream.write(f'{before_}{k}=')
238+
self._format(v, indent_new, False)
239+
self._stream.write(',\n')
240+
self._stream.write(indent_current * self._c + ')')
241+
242+
def _format_raw(self, _: 'Any', value_repr: str, indent_current: int, indent_new: int):
225243
lines = value_repr.splitlines(True)
226244
if len(lines) > 1 or (len(value_repr) + indent_current) >= self._width:
227245
self._stream.write('(\n')

devtools/utils.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
22
import sys
33

4-
__all__ = ('isatty',)
4+
__all__ = 'isatty', 'env_true', 'env_bool', 'use_highlight', 'is_literal', 'LaxMapping', 'DataClassType'
55

66
MYPY = False
77
if MYPY:
8-
from typing import Optional
8+
from typing import Any, Optional
99

1010

1111
def isatty(stream=None):
@@ -106,3 +106,32 @@ def is_literal(s):
106106
return False
107107
else:
108108
return True
109+
110+
111+
class MetaLaxMapping(type):
112+
def __instancecheck__(self, instance: 'Any') -> bool:
113+
return (
114+
hasattr(instance, '__getitem__')
115+
and hasattr(instance, 'items')
116+
and callable(instance.items)
117+
and type(instance) != type
118+
)
119+
120+
121+
class LaxMapping(metaclass=MetaLaxMapping):
122+
pass
123+
124+
125+
class MetaDataClassType(type):
126+
def __instancecheck__(self, instance: 'Any') -> bool:
127+
try:
128+
from dataclasses import _is_dataclass_instance
129+
except ImportError:
130+
# python 3.6
131+
return False
132+
else:
133+
return _is_dataclass_instance(instance)
134+
135+
136+
class DataClassType(metaclass=MetaDataClassType):
137+
pass

tests/test_prettier.py

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
22
import string
33
import sys
4-
from collections import OrderedDict, namedtuple
4+
from collections import Counter, OrderedDict, namedtuple
5+
from dataclasses import dataclass
6+
from typing import List
57
from unittest.mock import MagicMock
68

79
import pytest
@@ -159,6 +161,80 @@ def test_short_bytes():
159161
assert "b'abcdefghijklmnopqrstuvwxyz'" == pformat(string.ascii_lowercase.encode())
160162

161163

164+
def test_bytearray():
165+
pformat_ = PrettyFormat(width=18)
166+
v = pformat_(bytearray(string.ascii_lowercase.encode()))
167+
assert v == """\
168+
bytearray(
169+
b'abcdefghijk'
170+
b'lmnopqrstuv'
171+
b'wxyz'
172+
)"""
173+
174+
175+
def test_bytearray_short():
176+
v = pformat(bytearray(b'boo'))
177+
assert v == """\
178+
bytearray(
179+
b'boo'
180+
)"""
181+
182+
183+
def test_map():
184+
v = pformat(map(str.strip, ['x', 'y ', ' z']))
185+
assert v == """\
186+
map(
187+
'x',
188+
'y',
189+
'z',
190+
)"""
191+
192+
193+
def test_filter():
194+
v = pformat(filter(None, [1, 2, False, 3]))
195+
assert v == """\
196+
filter(
197+
1,
198+
2,
199+
3,
200+
)"""
201+
202+
203+
def test_counter():
204+
c = Counter()
205+
c['x'] += 1
206+
c['x'] += 1
207+
c['y'] += 1
208+
v = pformat(c)
209+
assert v == """\
210+
<Counter({
211+
'x': 2,
212+
'y': 1,
213+
})>"""
214+
215+
216+
@pytest.mark.skipif(sys.version_info > (3, 7), reason='no datalcasses before 3.6')
217+
def test_dataclass():
218+
@dataclass
219+
class FooDataclass:
220+
x: int
221+
y: List[int]
222+
223+
f = FooDataclass(123, [1, 2, 3, 4])
224+
v = pformat(f)
225+
print(v)
226+
assert v == """\
227+
FooDataclass(
228+
x=123,
229+
y=[
230+
1,
231+
2,
232+
3,
233+
4,
234+
],
235+
)"""
236+
237+
162238
@pytest.mark.skipif(numpy is None, reason='numpy not installed')
163239
def test_indent_numpy():
164240
v = pformat({'numpy test': numpy.array(range(20))})
@@ -238,21 +314,7 @@ def test_deep_objects():
238314
)"""
239315

240316

241-
@pytest.mark.skipif(sys.version_info > (3, 5, 3), reason='like this only for old 3.5')
242-
def test_call_args_py353():
243-
m = MagicMock()
244-
m(1, 2, 3, a=4)
245-
v = pformat(m.call_args)
246-
247-
assert v == """\
248-
_Call(
249-
(1, 2, 3),
250-
{'a': 4},
251-
)"""
252-
253-
254-
@pytest.mark.skipif(sys.version_info <= (3, 5, 3), reason='different for old 3.5')
255-
def test_call_args_py354():
317+
def test_call_args():
256318
m = MagicMock()
257319
m(1, 2, 3, a=4)
258320
v = pformat(m.call_args)

0 commit comments

Comments
 (0)