3
3
from collections import OrderedDict
4
4
from collections .abc import Generator
5
5
6
- from .utils import env_true , isatty
6
+ from .utils import DataClassType , LaxMapping , env_true , isatty
7
7
8
8
__all__ = 'PrettyFormat' , 'pformat' , 'pprint'
9
9
MYPY = False
10
10
if MYPY :
11
- from typing import Any , Union
11
+ from typing import Any , Iterable , Union
12
12
13
13
PARENTHESES_LOOKUP = [
14
14
(list , '[' , ']' ),
@@ -39,6 +39,10 @@ def get_pygments():
39
39
return pygments , PythonLexer (), Terminal256Formatter (style = 'vim' )
40
40
41
41
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
+
42
46
class PrettyFormat :
43
47
def __init__ (
44
48
self ,
@@ -60,7 +64,11 @@ def __init__(
60
64
((str , bytes ), self ._format_str_bytes ),
61
65
(tuple , self ._format_tuples ),
62
66
((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 ),
64
72
]
65
73
66
74
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):
96
104
return
97
105
98
106
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 ):
100
108
self ._stream .write (value_repr )
101
109
else :
102
110
indent_new = indent_current + self ._indent_step
@@ -105,18 +113,6 @@ def _format(self, value: 'Any', indent_current: int, indent_first: bool):
105
113
func (value , value_repr , indent_current , indent_new )
106
114
return
107
115
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
-
120
116
self ._format_raw (value , value_repr , indent_current , indent_new )
121
117
122
118
def _render_pretty (self , gen , indent : int ):
@@ -139,12 +135,12 @@ def _render_pretty(self, gen, indent: int):
139
135
# shouldn't happen but will
140
136
self ._stream .write (repr (v ))
141
137
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 ):
143
139
open_ , before_ , split_ , after_ , close_ = '{\n ' , indent_new * self ._c , ': ' , ',\n ' , '}'
144
140
if isinstance (value , OrderedDict ):
145
141
open_ , split_ , after_ , close_ = 'OrderedDict([\n ' , ', ' , '),\n ' , '])'
146
142
before_ += '('
147
- elif not isinstance (value , dict ) :
143
+ elif type (value ) != dict :
148
144
open_ , close_ = '<{}({{\n ' .format (value .__class__ .__name__ ), '})>'
149
145
150
146
self ._stream .write (open_ )
@@ -156,9 +152,7 @@ def _format_dict(self, value: 'Any', value_repr: str, indent_current: int, inden
156
152
self ._stream .write (after_ )
157
153
self ._stream .write (indent_current * self ._c + close_ )
158
154
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 ):
162
156
open_ , close_ = '(' , ')'
163
157
for t , * oc in PARENTHESES_LOOKUP :
164
158
if isinstance (value , t ):
@@ -194,15 +188,18 @@ def _format_str_bytes(self, value: 'Union[str, bytes]', value_repr: str, indent_
194
188
else :
195
189
lines = list (self ._wrap_lines (value , indent_new ))
196
190
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 )
202
192
else :
203
193
self ._stream .write (value_repr )
204
194
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]' :
206
203
width = self ._width - indent_new - 3
207
204
for line in s .splitlines (True ):
208
205
start = 0
@@ -211,17 +208,38 @@ def _wrap_lines(self, s, indent_new):
211
208
start = pos
212
209
yield line [start :]
213
210
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 ):
215
212
if self ._repr_generators :
216
213
self ._stream .write (value_repr )
217
214
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 ' )
219
221
for v in value :
220
222
self ._format (v , indent_new , True )
221
223
self ._stream .write (',\n ' )
222
224
self ._stream .write (indent_current * self ._c + ')' )
223
225
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 ):
225
243
lines = value_repr .splitlines (True )
226
244
if len (lines ) > 1 or (len (value_repr ) + indent_current ) >= self ._width :
227
245
self ._stream .write ('(\n ' )
0 commit comments