|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing classes and functions to dump variable contents. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 |
|
12 from DebugConfig import ConfigQtNames, ConfigKnownQtTypes, BatchSize |
|
13 |
|
14 # |
|
15 # This code was inspired by pydevd. |
|
16 # |
|
17 |
|
18 ############################################################ |
|
19 ## Classes implementing resolvers for various compund types |
|
20 ############################################################ |
|
21 |
|
22 |
|
23 class BaseResolver: |
|
24 """ |
|
25 Base class of the resolver class tree. |
|
26 """ |
|
27 def resolve(self, var, attribute): |
|
28 """ |
|
29 Public method to get an attribute from a variable. |
|
30 |
|
31 @param var variable to extract an attribute or value from |
|
32 @type any |
|
33 @param attribute name of the attribute to extract |
|
34 @type str |
|
35 @return value of the attribute |
|
36 @rtype any |
|
37 """ |
|
38 return getattr(var, attribute, None) |
|
39 |
|
40 def getDictionary(self, var): |
|
41 """ |
|
42 Public method to get the attributes of a variable as a dictionary. |
|
43 |
|
44 @param var variable to be converted |
|
45 @type any |
|
46 @return dictionary containing the variable attributes |
|
47 @rtype dict |
|
48 """ |
|
49 names = dir(var) |
|
50 if not names and hasattr(var, "__members__"): |
|
51 names = var.__members__ |
|
52 |
|
53 d = {} |
|
54 for name in names: |
|
55 with contextlib.suppress(Exception): |
|
56 attribute = getattr(var, name) |
|
57 d[name] = attribute |
|
58 |
|
59 return d |
|
60 |
|
61 |
|
62 ############################################################ |
|
63 ## Default Resolver |
|
64 ############################################################ |
|
65 |
|
66 |
|
67 class DefaultResolver(BaseResolver): |
|
68 """ |
|
69 Class used to resolve the default way. |
|
70 """ |
|
71 def getDictionary(self, var): |
|
72 """ |
|
73 Public method to get the attributes of a variable as a dictionary. |
|
74 |
|
75 @param var variable to be converted |
|
76 @type any |
|
77 @yield tuple containing the batch start index and a dictionary |
|
78 containing the variable attributes |
|
79 @ytype tuple of (int, dict) |
|
80 """ |
|
81 names = dir(var) |
|
82 if not names and hasattr(var, "__members__"): |
|
83 names = var.__members__ |
|
84 |
|
85 d = {} |
|
86 for name in names: |
|
87 with contextlib.suppress(Exception): |
|
88 attribute = getattr(var, name) |
|
89 d[name] = attribute |
|
90 |
|
91 yield -1, d |
|
92 while True: |
|
93 yield -2, {} |
|
94 |
|
95 |
|
96 ############################################################ |
|
97 ## Resolver for Dictionaries |
|
98 ############################################################ |
|
99 |
|
100 |
|
101 class DictResolver(BaseResolver): |
|
102 """ |
|
103 Class used to resolve from a dictionary. |
|
104 """ |
|
105 def resolve(self, var, attribute): |
|
106 """ |
|
107 Public method to get an attribute from a variable. |
|
108 |
|
109 @param var variable to extract an attribute or value from |
|
110 @type dict |
|
111 @param attribute name of the attribute to extract |
|
112 @type str |
|
113 @return value of the attribute |
|
114 @rtype any |
|
115 """ |
|
116 if " (ID:" not in attribute: |
|
117 try: |
|
118 return var[attribute] |
|
119 except Exception: |
|
120 return getattr(var, attribute, None) |
|
121 |
|
122 expectedID = int(attribute.split(" (ID:")[-1][:-1]) |
|
123 for key, value in var.items(): |
|
124 if id(key) == expectedID: |
|
125 return value |
|
126 |
|
127 return None |
|
128 |
|
129 def keyToStr(self, key): |
|
130 """ |
|
131 Public method to get a string representation for a key. |
|
132 |
|
133 @param key key to be converted |
|
134 @type any |
|
135 @return string representation of the given key |
|
136 @rtype str |
|
137 """ |
|
138 if isinstance(key, str): |
|
139 key = repr(key) |
|
140 # Special handling for bytes object |
|
141 # Raw and f-Strings are always converted to str |
|
142 if key[0] == 'b': |
|
143 key = key[1:] |
|
144 |
|
145 return key # __IGNORE_WARNING_M834__ |
|
146 |
|
147 def getDictionary(self, var): |
|
148 """ |
|
149 Public method to get the attributes of a variable as a dictionary. |
|
150 |
|
151 @param var variable to be converted |
|
152 @type any |
|
153 @yield tuple containing the batch start index and a dictionary |
|
154 containing the variable attributes |
|
155 @ytype tuple of (int, dict) |
|
156 """ |
|
157 d = {} |
|
158 start = count = 0 |
|
159 allItems = list(var.items()) |
|
160 try: |
|
161 # Fast path: all items from same type |
|
162 allItems.sort(key=lambda x: x[0]) |
|
163 except TypeError: |
|
164 # Slow path: only sort items with same type (Py3 only) |
|
165 allItems.sort(key=lambda x: (str(x[0]), x[0])) |
|
166 |
|
167 for key, value in allItems: |
|
168 key = "{0} (ID:{1})".format(self.keyToStr(key), id(key)) |
|
169 d[key] = value |
|
170 count += 1 |
|
171 if count >= BatchSize: |
|
172 yield start, d |
|
173 start += count |
|
174 count = 0 |
|
175 d = {} |
|
176 |
|
177 if d: |
|
178 yield start, d |
|
179 |
|
180 # in case it has additional fields |
|
181 d = super().getDictionary(var) |
|
182 yield -1, d |
|
183 |
|
184 while True: |
|
185 yield -2, {} |
|
186 |
|
187 |
|
188 ############################################################ |
|
189 ## Resolver for Lists and Tuples |
|
190 ############################################################ |
|
191 |
|
192 |
|
193 class ListResolver(BaseResolver): |
|
194 """ |
|
195 Class used to resolve from a tuple or list. |
|
196 """ |
|
197 def resolve(self, var, attribute): |
|
198 """ |
|
199 Public method to get an attribute from a variable. |
|
200 |
|
201 @param var variable to extract an attribute or value from |
|
202 @type tuple or list |
|
203 @param attribute name of the attribute to extract |
|
204 @type str |
|
205 @return value of the attribute |
|
206 @rtype any |
|
207 """ |
|
208 try: |
|
209 return var[int(attribute)] |
|
210 except Exception: |
|
211 return getattr(var, attribute, None) |
|
212 |
|
213 def getDictionary(self, var): |
|
214 """ |
|
215 Public method to get the attributes of a variable as a dictionary. |
|
216 |
|
217 @param var variable to be converted |
|
218 @type any |
|
219 @yield tuple containing the batch start index and a dictionary |
|
220 containing the variable attributes |
|
221 @ytype tuple of (int, dict) |
|
222 """ |
|
223 d = {} |
|
224 start = count = 0 |
|
225 for idx, value in enumerate(var): |
|
226 d[idx] = value |
|
227 count += 1 |
|
228 if count >= BatchSize: |
|
229 yield start, d |
|
230 start = idx + 1 |
|
231 count = 0 |
|
232 d = {} |
|
233 |
|
234 if d: |
|
235 yield start, d |
|
236 |
|
237 # in case it has additional fields |
|
238 d = super().getDictionary(var) |
|
239 yield -1, d |
|
240 |
|
241 while True: |
|
242 yield -2, {} |
|
243 |
|
244 |
|
245 ############################################################ |
|
246 ## Resolver for dict_items, dict_keys and dict_values |
|
247 ############################################################ |
|
248 |
|
249 |
|
250 class DictViewResolver(ListResolver): |
|
251 """ |
|
252 Class used to resolve from dict views. |
|
253 """ |
|
254 def resolve(self, var, attribute): |
|
255 """ |
|
256 Public method to get an attribute from a variable. |
|
257 |
|
258 @param var variable to extract an attribute or value from |
|
259 @type tuple or list |
|
260 @param attribute id of the value to extract |
|
261 @type str |
|
262 @return value of the attribute |
|
263 @rtype any |
|
264 """ |
|
265 return super().resolve(list(var), attribute) |
|
266 |
|
267 def getDictionary(self, var): |
|
268 """ |
|
269 Public method to get the attributes of a variable as a dictionary. |
|
270 |
|
271 @param var variable to be converted |
|
272 @type any |
|
273 @return dictionary containing the variable attributes |
|
274 @rtype dict |
|
275 """ |
|
276 return super().getDictionary(list(var)) |
|
277 |
|
278 |
|
279 ############################################################ |
|
280 ## Resolver for Sets and Frozensets |
|
281 ############################################################ |
|
282 |
|
283 |
|
284 class SetResolver(BaseResolver): |
|
285 """ |
|
286 Class used to resolve from a set or frozenset. |
|
287 """ |
|
288 def resolve(self, var, attribute): |
|
289 """ |
|
290 Public method to get an attribute from a variable. |
|
291 |
|
292 @param var variable to extract an attribute or value from |
|
293 @type tuple or list |
|
294 @param attribute id of the value to extract |
|
295 @type str |
|
296 @return value of the attribute |
|
297 @rtype any |
|
298 """ |
|
299 if attribute.startswith("'ID: "): |
|
300 attribute = attribute.split(None, 1)[1][:-1] |
|
301 try: |
|
302 attribute = int(attribute) |
|
303 except Exception: |
|
304 return getattr(var, attribute, None) |
|
305 |
|
306 for v in var: |
|
307 if id(v) == attribute: |
|
308 return v |
|
309 |
|
310 return None |
|
311 |
|
312 def getDictionary(self, var): |
|
313 """ |
|
314 Public method to get the attributes of a variable as a dictionary. |
|
315 |
|
316 @param var variable to be converted |
|
317 @type any |
|
318 @yield tuple containing the batch start index and a dictionary |
|
319 containing the variable attributes |
|
320 @ytype tuple of (int, dict) |
|
321 """ |
|
322 d = {} |
|
323 start = count = 0 |
|
324 for value in var: |
|
325 count += 1 |
|
326 d["'ID: {0}'".format(id(value))] = value |
|
327 if count >= BatchSize: |
|
328 yield start, d |
|
329 start += count |
|
330 count = 0 |
|
331 d = {} |
|
332 |
|
333 if d: |
|
334 yield start, d |
|
335 |
|
336 # in case it has additional fields |
|
337 additionals = super().getDictionary(var) |
|
338 yield -1, additionals |
|
339 |
|
340 while True: |
|
341 yield -2, {} |
|
342 |
|
343 |
|
344 ############################################################ |
|
345 ## Resolver for Numpy Arrays |
|
346 ############################################################ |
|
347 |
|
348 |
|
349 class NdArrayResolver(BaseResolver): |
|
350 """ |
|
351 Class used to resolve from numpy ndarray including some meta data. |
|
352 """ |
|
353 def __isNumeric(self, arr): |
|
354 """ |
|
355 Private method to check, if an array is of a numeric type. |
|
356 |
|
357 @param arr array to check |
|
358 @type ndarray |
|
359 @return flag indicating a numeric array |
|
360 @rtype bool |
|
361 """ |
|
362 try: |
|
363 return arr.dtype.kind in 'biufc' |
|
364 except AttributeError: |
|
365 return False |
|
366 |
|
367 def resolve(self, var, attribute): |
|
368 """ |
|
369 Public method to get an attribute from a variable. |
|
370 |
|
371 @param var variable to extract an attribute or value from |
|
372 @type tuple or list |
|
373 @param attribute id of the value to extract |
|
374 @type str |
|
375 @return value of the attribute |
|
376 @rtype any |
|
377 """ |
|
378 if attribute == 'min': |
|
379 if self.__isNumeric(var): |
|
380 return var.min() |
|
381 else: |
|
382 return None |
|
383 |
|
384 if attribute == 'max': |
|
385 if self.__isNumeric(var): |
|
386 return var.max() |
|
387 else: |
|
388 return None |
|
389 |
|
390 if attribute == 'mean': |
|
391 if self.__isNumeric(var): |
|
392 return var.mean() |
|
393 else: |
|
394 return None |
|
395 |
|
396 try: |
|
397 return var[int(attribute)] |
|
398 except Exception: |
|
399 return getattr(var, attribute, None) |
|
400 |
|
401 return None |
|
402 |
|
403 def getDictionary(self, var): |
|
404 """ |
|
405 Public method to get the attributes of a variable as a dictionary. |
|
406 |
|
407 @param var variable to be converted |
|
408 @type any |
|
409 @yield tuple containing the batch start index and a dictionary |
|
410 containing the variable attributes |
|
411 @ytype tuple of (int, dict) |
|
412 """ |
|
413 d = {} |
|
414 start = count = 0 |
|
415 try: |
|
416 len(var) # Check if it's an unsized object, e.g. np.ndarray(()) |
|
417 allItems = var.tolist() |
|
418 except TypeError: # TypeError: len() of unsized object |
|
419 allItems = [] |
|
420 |
|
421 for idx, value in enumerate(allItems): |
|
422 d[str(idx)] = value |
|
423 count += 1 |
|
424 if count >= BatchSize: |
|
425 yield start, d |
|
426 start += count |
|
427 count = 0 |
|
428 d = {} |
|
429 |
|
430 if d: |
|
431 yield start, d |
|
432 |
|
433 # in case it has additional fields |
|
434 d = super().getDictionary(var) |
|
435 |
|
436 if var.size > 1024 * 1024: |
|
437 d['min'] = ( |
|
438 'ndarray too big, calculating min would slow down debugging') |
|
439 d['max'] = ( |
|
440 'ndarray too big, calculating max would slow down debugging') |
|
441 d['mean'] = ( |
|
442 'ndarray too big, calculating mean would slow down debugging') |
|
443 elif self.__isNumeric(var): |
|
444 if var.size == 0: |
|
445 d['min'] = 'empty array' |
|
446 d['max'] = 'empty array' |
|
447 d['mean'] = 'empty array' |
|
448 else: |
|
449 d['min'] = var.min() |
|
450 d['max'] = var.max() |
|
451 d['mean'] = var.mean() |
|
452 else: |
|
453 d['min'] = 'not a numeric object' |
|
454 d['max'] = 'not a numeric object' |
|
455 d['mean'] = 'not a numeric object' |
|
456 |
|
457 yield -1, d |
|
458 |
|
459 while True: |
|
460 yield -2, {} |
|
461 |
|
462 |
|
463 ############################################################ |
|
464 ## Resolver for Django Multi Value Dictionaries |
|
465 ############################################################ |
|
466 |
|
467 |
|
468 class MultiValueDictResolver(DictResolver): |
|
469 """ |
|
470 Class used to resolve from Django multi value dictionaries. |
|
471 """ |
|
472 def resolve(self, var, attribute): |
|
473 """ |
|
474 Public method to get an attribute from a variable. |
|
475 |
|
476 @param var variable to extract an attribute or value from |
|
477 @type dict |
|
478 @param attribute name of the attribute to extract |
|
479 @type str |
|
480 @return value of the attribute |
|
481 @rtype any |
|
482 """ |
|
483 if " (ID:" not in attribute: |
|
484 try: |
|
485 return var[attribute] |
|
486 except Exception: |
|
487 return getattr(var, attribute, None) |
|
488 |
|
489 expectedID = int(attribute.split(" (ID:")[-1][:-1]) |
|
490 for key in var: |
|
491 if id(key) == expectedID: |
|
492 return var.getlist(key) |
|
493 |
|
494 return None |
|
495 |
|
496 def getDictionary(self, var): |
|
497 """ |
|
498 Public method to get the attributes of a variable as a dictionary. |
|
499 |
|
500 @param var variable to be converted |
|
501 @type any |
|
502 @yield tuple containing the batch start index and a dictionary |
|
503 containing the variable attributes |
|
504 @ytype tuple of (int, dict) |
|
505 """ |
|
506 d = {} |
|
507 start = count = 0 |
|
508 allKeys = list(var.keys()) |
|
509 try: |
|
510 # Fast path: all items from same type |
|
511 allKeys.sort() |
|
512 except TypeError: |
|
513 # Slow path: only sort items with same type (Py3 only) |
|
514 allKeys.sort(key=lambda x: (str(x), x)) |
|
515 |
|
516 for key in allKeys: |
|
517 dkey = "{0} (ID:{1})".format(self.keyToStr(key), id(key)) |
|
518 d[dkey] = var.getlist(key) |
|
519 count += 1 |
|
520 if count >= BatchSize: |
|
521 yield start, d |
|
522 start += count |
|
523 count = 0 |
|
524 d = {} |
|
525 |
|
526 if d: |
|
527 yield start, d |
|
528 |
|
529 # in case it has additional fields |
|
530 d = super().getDictionary(var) |
|
531 yield -1, d |
|
532 |
|
533 while True: |
|
534 yield -2, {} |
|
535 |
|
536 |
|
537 ############################################################ |
|
538 ## Resolver for array.array |
|
539 ############################################################ |
|
540 |
|
541 |
|
542 class ArrayResolver(BaseResolver): |
|
543 """ |
|
544 Class used to resolve from array.array including some meta data. |
|
545 """ |
|
546 TypeCodeMap = { |
|
547 "b": "int (signed char)", |
|
548 "B": "int (unsigned char)", |
|
549 "u": "Unicode character (Py_UNICODE)", |
|
550 "h": "int (signed short)", |
|
551 "H": "int (unsigned short)", |
|
552 "i": "int (signed int)", |
|
553 "I": "int (unsigned int)", |
|
554 "l": "int (signed long)", |
|
555 "L": "int (unsigned long)", |
|
556 "q": "int (signed long long)", |
|
557 "Q": "int (unsigned long long)", |
|
558 "f": "float (float)", |
|
559 "d": "float (double)", |
|
560 } |
|
561 |
|
562 def resolve(self, var, attribute): |
|
563 """ |
|
564 Public method to get an attribute from a variable. |
|
565 |
|
566 @param var variable to extract an attribute or value from |
|
567 @type tuple or list |
|
568 @param attribute id of the value to extract |
|
569 @type str |
|
570 @return value of the attribute |
|
571 @rtype any |
|
572 """ |
|
573 try: |
|
574 return var[int(attribute)] |
|
575 except Exception: |
|
576 return getattr(var, attribute, None) |
|
577 |
|
578 return None |
|
579 |
|
580 def getDictionary(self, var): |
|
581 """ |
|
582 Public method to get the attributes of a variable as a dictionary. |
|
583 |
|
584 @param var variable to be converted |
|
585 @type any |
|
586 @yield tuple containing the batch start index and a dictionary |
|
587 containing the variable attributes |
|
588 @ytype tuple of (int, dict) |
|
589 """ |
|
590 d = {} |
|
591 start = count = 0 |
|
592 allItems = var.tolist() |
|
593 |
|
594 for idx, value in enumerate(allItems): |
|
595 d[str(idx)] = value |
|
596 count += 1 |
|
597 if count >= BatchSize: |
|
598 yield start, d |
|
599 start += count |
|
600 count = 0 |
|
601 d = {} |
|
602 |
|
603 if d: |
|
604 yield start, d |
|
605 |
|
606 # in case it has additional fields |
|
607 d = super().getDictionary(var) |
|
608 |
|
609 # Special data for array type: convert typecode to readable text |
|
610 d['type'] = self.TypeCodeMap.get(var.typecode, 'illegal type') |
|
611 |
|
612 yield -1, d |
|
613 |
|
614 while True: |
|
615 yield -2, {} |
|
616 |
|
617 |
|
618 defaultResolver = DefaultResolver() |
|
619 dictResolver = DictResolver() |
|
620 listResolver = ListResolver() |
|
621 dictViewResolver = DictViewResolver() |
|
622 setResolver = SetResolver() |
|
623 ndarrayResolver = NdArrayResolver() |
|
624 multiValueDictResolver = MultiValueDictResolver() |
|
625 arrayResolver = ArrayResolver() |
|
626 |
|
627 ############################################################ |
|
628 ## Methods to determine the type of a variable and the |
|
629 ## resolver class to use |
|
630 ############################################################ |
|
631 |
|
632 _TypeMap = None |
|
633 |
|
634 |
|
635 def _initTypeMap(): |
|
636 """ |
|
637 Protected function to initialize the type map. |
|
638 """ |
|
639 global _TypeMap |
|
640 |
|
641 _TypeMap = [ |
|
642 (type(None), None,), |
|
643 (int, None), |
|
644 (float, None), |
|
645 (complex, None), |
|
646 (str, None), |
|
647 (tuple, listResolver), |
|
648 (list, listResolver), |
|
649 (dict, dictResolver), |
|
650 (set, setResolver), |
|
651 (frozenset, setResolver), |
|
652 ] |
|
653 |
|
654 with contextlib.suppress(Exception): |
|
655 _TypeMap.append((long, None)) # __IGNORE_WARNING__ |
|
656 |
|
657 with contextlib.suppress(ImportError): |
|
658 import array |
|
659 _TypeMap.append((array.array, arrayResolver)) |
|
660 # array.array may not be available |
|
661 |
|
662 with contextlib.suppress(ImportError): |
|
663 import numpy |
|
664 _TypeMap.append((numpy.ndarray, ndarrayResolver)) |
|
665 # numpy may not be installed |
|
666 |
|
667 with contextlib.suppress(ImportError): |
|
668 from django.utils.datastructures import MultiValueDict |
|
669 # it should go before dict |
|
670 _TypeMap.insert(0, (MultiValueDict, multiValueDictResolver)) |
|
671 # django may not be installed |
|
672 |
|
673 with contextlib.suppress(ImportError): |
|
674 from collections.abc import ItemsView, KeysView, ValuesView |
|
675 _TypeMap.append((ItemsView, dictViewResolver)) |
|
676 _TypeMap.append((KeysView, dictViewResolver)) |
|
677 _TypeMap.append((ValuesView, dictViewResolver)) |
|
678 # not available on all Python versions |
|
679 |
|
680 |
|
681 def getType(obj): |
|
682 """ |
|
683 Public method to get the type information for an object. |
|
684 |
|
685 @param obj object to get type information for |
|
686 @type any |
|
687 @return tuple containing the type name, type string and resolver |
|
688 @rtype tuple of str, str, BaseResolver |
|
689 """ |
|
690 typeObject = type(obj) |
|
691 typeName = typeObject.__name__ |
|
692 # Between PyQt and PySide the returned type is different (class vs. type) |
|
693 typeStr = str(typeObject).split(' ', 1)[-1] |
|
694 typeStr = typeStr[1:-2] |
|
695 |
|
696 if ( |
|
697 typeStr.startswith(ConfigQtNames) and |
|
698 typeStr.endswith(ConfigKnownQtTypes) |
|
699 ): |
|
700 resolver = None |
|
701 else: |
|
702 if _TypeMap is None: |
|
703 _initTypeMap() |
|
704 |
|
705 for typeData, resolver in _TypeMap: # __IGNORE_WARNING_M507__ |
|
706 if isinstance(obj, typeData): |
|
707 break |
|
708 else: |
|
709 resolver = defaultResolver |
|
710 |
|
711 return typeName, typeStr, resolver |
|
712 |
|
713 # |
|
714 # eflag: noqa = Y113 |