33from collections import OrderedDict
44from 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'
99MYPY = False
1010if MYPY :
11- from typing import Any , Union
11+ from typing import Any , Iterable , Union
1212
1313PARENTHESES_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+
4246class 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 ' )
0 commit comments