Skip to content

Commit 9730786

Browse files
committed
Add inspect utils + tests
1 parent 57c59c3 commit 9730786

File tree

8 files changed

+259
-139
lines changed

8 files changed

+259
-139
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"python.pythonPath": "C:\\Python39\\python.exe"
2+
"python.pythonPath": "c:\\src\\servicestack-python\\venv\\Scripts\\python.exe"
33
}

servicestack/clients.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -196,26 +196,6 @@ def register_deserializer(cls: Type, deserializer: Callable[[Any], Any]):
196196
}
197197

198198

199-
def is_optional(cls: Type): return f"{cls}".startswith("typing.Optional")
200-
201-
202-
def is_list(cls: Type):
203-
return cls == typing.List or cls == list or get_origin(cls) == list
204-
205-
206-
def is_dict(cls: Type):
207-
return cls == typing.Dict or cls == dict or get_origin(cls) == dict
208-
209-
210-
def generic_arg(cls: Type): return generic_args(cls)[0]
211-
212-
213-
def generic_args(cls: Type):
214-
if not hasattr(cls, '__args__'):
215-
raise TypeError(f"{cls} is not a Generic Type")
216-
return cls.__args__
217-
218-
219199
def _resolve_forwardref(cls: Type, orig: Type = None):
220200
type_name = cls.__forward_arg__
221201
if orig is not None and orig.__name__ == type_name:

servicestack/utils.py

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
11
import base64
22
import json
3+
import numbers
4+
import os
5+
import pathlib
6+
import platform
7+
import dataclasses
8+
import typing
39
from datetime import datetime, timezone, timedelta
4-
from typing import Optional
10+
from typing import Optional, List, Dict, Any, Type, get_origin, Union, get_args
11+
from dataclasses import dataclass, field, asdict
12+
from functools import reduce
13+
14+
15+
def is_optional(cls: Type): return get_origin(cls) is Union and type(None) in get_args(cls)
16+
17+
18+
def is_list(cls: Type): return cls == typing.List or cls == list or get_origin(cls) == list
19+
20+
21+
def is_dict(cls: Type): return cls == typing.Dict or cls == dict or get_origin(cls) == dict
22+
23+
24+
def generic_args(cls: Type):
25+
if not hasattr(cls, '__args__'):
26+
raise TypeError(f"{cls} is not a Generic Type")
27+
return cls.__args__
28+
29+
30+
def generic_arg(cls: Type): return generic_args(cls)[0]
531

632

733
def index_of(target: str, needle: str):
@@ -220,3 +246,148 @@ def inspect_jwt(jwt: str):
220246
body = _decode_base64url_payload(left_part(right_part(jwt, '.'), '.'))
221247
exp = int(body['exp'])
222248
return head, body, datetime.fromtimestamp(exp, timezone.utc)
249+
250+
251+
# inspect utils
252+
def _asdict(obj):
253+
if isinstance(obj, dict):
254+
return obj
255+
elif dataclasses.is_dataclass(obj):
256+
return asdict(obj)
257+
elif hasattr(obj, '__dict__'):
258+
return obj.__dict__
259+
else:
260+
return obj
261+
262+
263+
def _asdicts(obj):
264+
t = type(obj)
265+
if is_list(t):
266+
to = []
267+
for o in obj:
268+
to.append(_asdicts(o))
269+
return to
270+
elif is_dict(t):
271+
to = {}
272+
for k in obj:
273+
to[k] = _asdicts(obj[k])
274+
return to
275+
else:
276+
return _asdict(obj)
277+
278+
279+
def _allkeys(obj):
280+
keys = []
281+
for o in obj:
282+
for key in o:
283+
if not key in keys:
284+
keys.append(key)
285+
return keys
286+
287+
288+
def inspect_vars(objs):
289+
if not isinstance(objs, dict):
290+
raise TypeError('objs must be a dictionary')
291+
292+
to = _asdicts(objs)
293+
294+
inspect_vars_path = os.environ.get('INSPECT_VARS')
295+
if inspect_vars_path is None:
296+
return
297+
if platform.system() == 'Windows':
298+
inspect_vars_path = inspect_vars_path.replace("/", "\\")
299+
else:
300+
inspect_vars_path = inspect_vars_path.replace("\\", "/")
301+
302+
pathlib.Path(os.path.dirname(inspect_vars_path)).mkdir(parents=True, exist_ok=True)
303+
304+
with open(inspect_vars_path, 'w') as outfile:
305+
json.dump(to, outfile)
306+
307+
308+
def dump(obj):
309+
print(_asdicts(obj))
310+
return json.dumps(_asdicts(obj), indent=4).replace('"', '').replace(': null', ':')
311+
312+
313+
def printdump(obj):
314+
print(dump(obj))
315+
316+
317+
def _align_left(s: str, length: int, pad: str = ' '):
318+
if length < 0:
319+
return ""
320+
alen = length + 1 - len(s)
321+
if alen <= 0:
322+
return s
323+
return pad + s + (pad * (length + 1 - len(s)))
324+
325+
326+
def _align_center(s: str, length: int, pad: str = ' '):
327+
if length < 0:
328+
return ""
329+
nlen = len(s)
330+
half = (length // 2 - nlen // 2)
331+
odds = abs((nlen % 2) - (length % 2))
332+
return (pad * (half + 1)) + s + (pad * (half + 1 + odds))
333+
334+
335+
def _align_right(s: str, length: int, pad: str = ' '):
336+
if length < 0:
337+
return ""
338+
alen = length + 1 - len(s)
339+
if alen <= 0:
340+
return s
341+
return (pad * (length + 1 - len(s))) + s + pad
342+
343+
344+
def _align_auto(obj: Any, length: int, pad: str = ' '):
345+
s = f"{obj}"
346+
if len(s) <= length:
347+
if isinstance(obj, numbers.Number):
348+
return _align_right(s, length, pad)
349+
return _align_left(s, length, pad)
350+
return s
351+
352+
353+
def dumptable(objs, headers=None):
354+
if not is_list(type(objs)):
355+
raise TypeError('objs must be a list')
356+
map_rows = _asdicts(objs)
357+
if headers is None:
358+
headers = _allkeys(map_rows)
359+
col_sizes: Dict[str, int] = {}
360+
361+
for k in headers:
362+
max = len(k)
363+
for row in map_rows:
364+
if k in row:
365+
col = row[k]
366+
val_size = len(f"{col}")
367+
if val_size > max:
368+
max = val_size
369+
col_sizes[k] = max
370+
371+
# sum + ' padding ' + |
372+
row_width = reduce(lambda x, y: x + y, col_sizes.values(), 0) + \
373+
(len(col_sizes) * 2) + \
374+
(len(col_sizes) + 1)
375+
sb: List[str] = [f"+{'-' * (row_width - 2)}+"]
376+
head = "|"
377+
for k in headers:
378+
head += _align_center(k, col_sizes[k]) + "|"
379+
sb.append(head)
380+
sb.append(f"|{'-' * (row_width - 2)}|")
381+
382+
for row in map_rows:
383+
to = "|"
384+
for k in headers:
385+
to += '' + _align_auto(row[k], col_sizes[k]) + "|"
386+
sb.append(to)
387+
388+
sb.append(f"+{'-' * (row_width - 2)}+")
389+
return '\n'.join(sb)
390+
391+
392+
def printdumptable(obj, headers=None):
393+
print(dumptable(obj, headers))

tests/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
TEST_URL = "http://test.servicestack.net"
66
TECHSTACKS_URL = "https://techstacks.io"
77

8+
89
def create_test_client():
910
return JsonServiceClient(TEST_URL)
1011

@@ -15,4 +16,3 @@ def create_techstacks_client():
1516

1617
def clear_session(client: JsonServiceClient):
1718
client.post(Authenticate(provider="logout"))
18-

tests/dtos.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
""" Options:
2-
Date: 2021-07-11 06:31:33
2+
Date: 2021-07-11 10:30:38
33
Version: 5.111
44
Tip: To override a DTO option, remove "//" prefix before updating
5-
BaseUrl: https://localhost:5001
5+
BaseUrl: https://test.servicestack.net
66
77
#GlobalNamespace:
88
#MakePropertiesOptional: False
@@ -166,9 +166,9 @@ class NestedClass:
166166

167167

168168
class EnumType(str, Enum):
169-
VALUE1 = 'VALUE1'
170-
VALUE2 = 'VALUE2'
171-
VALUE3 = 'VALUE3'
169+
VALUE1 = 'Value1'
170+
VALUE2 = 'Value2'
171+
VALUE3 = 'Value3'
172172

173173

174174
# @Flags()
@@ -200,12 +200,12 @@ class EnumAsInt(Enum):
200200

201201

202202
class EnumStyle(str, Enum):
203-
LOWER = 'LOWER'
203+
LOWER = 'lower'
204204
UPPER = 'UPPER'
205-
PASCAL_CASE = 'PASCAL_CASE'
206-
CAMEL_CASE = 'CAMEL_CASE'
207-
CAMEL_U_P_P_E_R = 'CAMEL_U_P_P_E_R'
208-
PASCAL_U_P_P_E_R = 'PASCAL_U_P_P_E_R'
205+
PASCAL_CASE = 'PascalCase'
206+
CAMEL_CASE = 'camelCase'
207+
CAMEL_U_P_P_E_R = 'camelUPPER'
208+
PASCAL_U_P_P_E_R = 'PascalUPPER'
209209

210210

211211
class EnumStyleMembers(str, Enum):
@@ -286,13 +286,13 @@ class EmptyClass:
286286

287287

288288
class DayOfWeek(str, Enum):
289-
SUNDAY = 'SUNDAY'
290-
MONDAY = 'MONDAY'
291-
TUESDAY = 'TUESDAY'
292-
WEDNESDAY = 'WEDNESDAY'
293-
THURSDAY = 'THURSDAY'
294-
FRIDAY = 'FRIDAY'
295-
SATURDAY = 'SATURDAY'
289+
SUNDAY = 'Sunday'
290+
MONDAY = 'Monday'
291+
TUESDAY = 'Tuesday'
292+
WEDNESDAY = 'Wednesday'
293+
THURSDAY = 'Thursday'
294+
FRIDAY = 'Friday'
295+
SATURDAY = 'Saturday'
296296

297297

298298
class ScopeType(Enum):
@@ -349,8 +349,8 @@ class QueryDbTenant(Generic[From, Into], QueryDb2[From, Into]):
349349

350350

351351
class LivingStatus(str, Enum):
352-
ALIVE = 'ALIVE'
353-
DEAD = 'DEAD'
352+
ALIVE = 'Alive'
353+
DEAD = 'Dead'
354354

355355

356356
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@@ -589,9 +589,9 @@ class InnerType:
589589

590590

591591
class InnerEnum(str, Enum):
592-
FOO = 'FOO'
593-
BAR = 'BAR'
594-
BAZ = 'BAZ'
592+
FOO = 'Foo'
593+
BAR = 'Bar'
594+
BAZ = 'Baz'
595595

596596

597597
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@@ -1198,7 +1198,6 @@ class DeclarativeCollectiveValidationTest(IReturn[EmptyResponse]):
11981198
site: Optional[str] = None
11991199

12001200
declarative_validations: Optional[List[DeclarativeChildValidation]] = None
1201-
12021201
fluent_validations: Optional[List[FluentChildValidation]] = None
12031202

12041203

@@ -1210,7 +1209,6 @@ class DeclarativeSingleValidationTest(IReturn[EmptyResponse]):
12101209
site: Optional[str] = None
12111210

12121211
declarative_single_validation: Optional[DeclarativeSingleValidation] = None
1213-
12141212
fluent_single_validation: Optional[FluentSingleValidation] = None
12151213

12161214

0 commit comments

Comments
 (0)