1
1
import base64
2
- import dataclasses
3
2
import decimal
4
3
import inspect
5
4
import json
5
+ import math
6
6
import numbers
7
7
import os
8
8
import pathlib
9
9
import platform
10
10
import typing
11
- from dataclasses import asdict
12
11
from dataclasses import fields , is_dataclass
13
12
from datetime import datetime , timedelta
14
- from enum import Enum , IntEnum
13
+ from enum import Enum , IntEnum , EnumMeta
15
14
from functools import reduce
15
+ from types import MappingProxyType
16
16
from typing import Callable , Type , get_origin , ForwardRef , Union , TypeVar , get_args
17
17
from typing import List , Dict , Any
18
18
@@ -29,7 +29,7 @@ def is_optional(cls: Type): return get_origin(cls) is Union and type(None) in ge
29
29
def is_list (cls : Type ): return cls == typing .List or cls == list or get_origin (cls ) == list
30
30
31
31
32
- def is_dict (cls : Type ): return cls == typing .Dict or cls == dict or get_origin (cls ) == dict
32
+ def is_dict (cls : Type ): return cls == typing .Dict or cls == dict or get_origin (cls ) == dict or cls == MappingProxyType
33
33
34
34
35
35
def nameof (instance ):
@@ -78,31 +78,51 @@ def _empty(x):
78
78
return x is None or x == {} or x == []
79
79
80
80
81
- def to_dict (obj : Any ):
82
- if obj is None :
83
- return {}
81
+ def _str (x : Any ):
82
+ if type (x ) == str :
83
+ return x
84
+ return f"{ x } "
85
+
86
+
87
+ def identity (x : Any ): return x
88
+
89
+
90
+ _JSON_TYPES = {str , bool , int , float }
91
+ _BUILT_IN_TYPES = {str , bool , int , float , decimal .Decimal , datetime , timedelta , bytes , bytearray , complex }
92
+
93
+
94
+ def to_dict (obj : Any , key_case : Callable [[str ], str ] = identity , remove_empty : bool = True ):
84
95
t = type (obj )
96
+ if obj is None or t in _BUILT_IN_TYPES or issubclass (t , Enum ):
97
+ return obj
85
98
if is_list (t ):
86
99
to = []
87
100
for o in obj :
88
- to .append (to_dict (o ))
89
- return to
101
+ use_val = to_dict (o , key_case = key_case , remove_empty = remove_empty )
102
+ if not remove_empty or use_val is not None :
103
+ to .append (use_val )
90
104
elif is_dict (t ):
91
105
to = {}
92
- for k in obj :
93
- to [k ] = to_dict (obj [k ])
94
- return to
95
- if is_dataclass (obj ):
96
- d = asdict (obj )
106
+ for k , v in obj .items ():
107
+ use_key = key_case (_str (k ))
108
+ use_val = to_dict (v , key_case = key_case , remove_empty = remove_empty )
109
+ if not remove_empty or use_val is not None :
110
+ to [use_key ] = use_val
111
+ elif hasattr (obj , 'to_dict' ): # dataclass
112
+ d = obj .to_dict ()
97
113
to = {}
98
114
for k , v in d .items ():
99
- use_key = camelcase ( k )
100
- if use_key [ - 1 ] == '_' :
101
- use_key = use_key [ 0 : - 1 ]
102
- to [use_key ] = v
115
+ use_key = key_case ( _str ( k ) )
116
+ use_val = to_dict ( v , key_case = key_case , remove_empty = remove_empty )
117
+ if not remove_empty or use_val is not None :
118
+ to [use_key ] = use_val
103
119
elif hasattr (obj , '__dict__' ):
104
- return obj .__dict__
105
- return clean_any (to )
120
+ to = to_dict (vars (obj ), key_case = key_case , remove_empty = remove_empty )
121
+ else :
122
+ to = obj
123
+ if remove_empty :
124
+ return clean_any (to )
125
+ return to
106
126
107
127
108
128
def _clean_list (d : list ):
@@ -115,19 +135,18 @@ def _clean_dict(d: dict):
115
135
116
136
def clean_any (d ):
117
137
"""recursively remove empty lists, empty dicts, or None elements from a dictionary"""
118
- if not isinstance ( d , ( dict , list ) ):
119
- return d
120
- elif isinstance ( d , list ):
138
+ if is_dict ( d ):
139
+ return _clean_dict ( d )
140
+ elif is_list ( d ):
121
141
return _clean_list (d )
122
142
else :
123
- return _clean_dict ( d )
143
+ return d
124
144
125
145
126
146
def _json_encoder (obj : Any ):
127
- if is_dataclass (obj ):
147
+ t = type (obj )
148
+ if is_dataclass (t ) or is_dict (t ):
128
149
return to_dict (obj )
129
- if hasattr (obj , '__dict__' ):
130
- return vars (obj )
131
150
if isinstance (obj , datetime ):
132
151
return obj .isoformat ()
133
152
if isinstance (obj , timedelta ):
@@ -136,14 +155,15 @@ def _json_encoder(obj: Any):
136
155
return base64 .b64encode (obj ).decode ('ascii' )
137
156
if isinstance (obj , decimal .Decimal ):
138
157
return float (obj )
139
- raise TypeError (f"Unsupported Type in JSON encoding: { type (obj )} " )
158
+ if t in _JSON_TYPES :
159
+ return obj
160
+ if t in _BUILT_IN_TYPES or (type (obj ) == type and issubclass (obj , Enum )):
161
+ return _str (obj )
162
+ raise TypeError (f"Unsupported Type in JSON encoding: { t } " )
140
163
141
164
142
165
def to_json (obj : Any , indent = None ):
143
- if is_dataclass (obj ):
144
- obj_dict = clean_any (obj .to_dict ())
145
- return json .dumps (obj_dict , indent = indent , default = _json_encoder )
146
- return json .dumps (obj , indent = indent , default = _json_encoder )
166
+ return json .dumps (to_dict (obj ), indent = indent , default = _json_encoder )
147
167
148
168
149
169
class TypeConverters :
@@ -316,7 +336,7 @@ def convert(into: Type, obj: Any, substitute_types: Dict[Type, type] = None):
316
336
except Exception as e :
317
337
Log .error (f"into().deserialize(obj) { into } ({ obj } )" , e )
318
338
raise e
319
- elif is_type and issubclass (into , Enum ):
339
+ elif is_type and ( issubclass (into , Enum ) or into == EnumMeta ):
320
340
try :
321
341
return enum_get (into , obj )
322
342
except Exception as e :
@@ -345,7 +365,8 @@ def all_keys(obj):
345
365
keys = []
346
366
for o in obj :
347
367
for key in o :
348
- if not key in keys :
368
+ key = _str (key )
369
+ if key is not None and key not in keys :
349
370
keys .append (key )
350
371
return keys
351
372
@@ -392,7 +413,7 @@ def _align_center(s: str, length: int, pad: str = ' '):
392
413
if length < 0 :
393
414
return ""
394
415
nlen = len (s )
395
- half = (length // 2 - nlen / / 2 )
416
+ half = math . floor (length / 2 - nlen / 2 )
396
417
odds = abs ((nlen % 2 ) - (length % 2 ))
397
418
return (pad * (half + 1 )) + s + (pad * (half + 1 + odds ))
398
419
@@ -407,28 +428,28 @@ def _align_right(s: str, length: int, pad: str = ' '):
407
428
408
429
409
430
def _align_auto (obj : Any , length : int , pad : str = ' ' ):
410
- s = f" { obj } "
431
+ s = _str ( obj )
411
432
if len (s ) <= length :
412
433
if isinstance (obj , numbers .Number ):
413
434
return _align_right (s , length , pad )
414
435
return _align_left (s , length , pad )
415
436
return s
416
437
417
438
418
- def dumptable (objs , headers = None ):
439
+ def table (objs , headers = None ):
419
440
if not is_list (type (objs )):
420
441
raise TypeError ('objs must be a list' )
421
442
map_rows = to_dict (objs )
422
443
if headers is None :
423
- headers = _allkeys (map_rows )
444
+ headers = all_keys (map_rows )
424
445
col_sizes : Dict [str , int ] = {}
425
446
426
447
for k in headers :
427
448
max = len (k )
428
449
for row in map_rows :
429
450
if k in row :
430
451
col = row [k ]
431
- val_size = len (f" { col } " )
452
+ val_size = len (_str ( col ) )
432
453
if val_size > max :
433
454
max = val_size
434
455
col_sizes [k ] = max
@@ -454,5 +475,5 @@ def dumptable(objs, headers=None):
454
475
return '\n ' .join (sb )
455
476
456
477
457
- def printdumptable (obj , headers = None ):
458
- print (dumptable (obj , headers ))
478
+ def printtable (obj , headers = None ):
479
+ print (table (obj , headers ))
0 commit comments