Cope 2.5.0
My personal "standard library" of all the generally useful code I've written for various projects over the years
Loading...
Searching...
No Matches
debugging.py
1"""
2Functions & classes that can be useful for debugging
3"""
4__version__ = '0.0.0'
5
6from inspect import stack
7import inspect
8import inspect as _inspect
9from os import get_terminal_size
10from os.path import relpath
11# from re import search as re_search
12from .colors import parse_color
13from re import match as re_match
14from typing import Union, Literal
15from varname import VarnameRetrievingError, argname, nameof
16from pprint import pformat
17from typing import *
18import logging
19# from logging import Logger
20from reprlib import Repr
21# This is a fantastic library. Use it if we have it.
22try:
23 from traceback_with_variables import activate_by_import
24except ImportError: pass
25from rich import print
26
27Log = logging.getLogger(__name__)
28_repr = repr
29_debug_count = 0
30root_dir = None
31display_func = True
32display_file = True
33display_path = False
34verbosity = 1
35
36def printArgs(*args, **kwargs):
37 print('args:', args)
38 print('kwargs:', kwargs)
39
40def get_metadata(calls:int=1) -> inspect.FrameInfo:
41 """ Gets the meta data of the line you're calling this function from.
42 `calls` is for how many function calls to look back from.
43 Returns None if that number of calls is invalid
44 """
45 try:
46 s = stack()[calls]
47 return s
48 except IndexError:
49 return None
50
52 iterable: Union[tuple, list, set, dict],
53 method: Literal['pprint', 'custom']='custom',
54 width: int=...,
55 depth: int=...,
56 indent: int=4,
57 ) -> str:
58 """ "Cast" a tuple, list, set or dict to a string, automatically shorten
59 it if it's long, and display how long it is.
60
61 Params:
62 limitToLine: if True, limit the length of list to a single line
63 minItems: show at least this many items in the list
64 maxItems: show at most this many items in the list
65 color: a simple int color
66
67 Note:
68 If limitToLine is True, it will overrule maxItems, but *not* minItems
69 """
70 if width is Ellipsis:
71 width == get_terminal_size().columns
72
73 if method == 'pprint':
74 return pformat(iterable, width=width, depth=depth, indent=indent)
75 elif method == 'custom':
76 def getBrace(opening):
77 if isinstance(iterable, list):
78 return '[' if opening else ']'
79 elif isinstance(iterable, (set, dict)):
80 return '{' if opening else '}'
81 else:
82 return '(' if opening else ')'
83
84 lengthAddOn = f'(len={len(iterable)})'
85 defaultStr = str(iterable)
86
87 # Print in lines
88 # if (not limitToLine and len(defaultStr) + len(lengthAddOn) > (get_terminal_size().columns / 2)) or useMultiline:
89 if len(defaultStr) + len(lengthAddOn) > (get_terminal_size().columns / 2):
90 rtnStr = f'{lengthAddOn} {getBrace(True)}'
91 if isinstance(iterable, dict):
92 for key, val in iterable.items():
93 rtnStr += f'\n\t<{type(key).__name__}> {key}: <{type(val).__name__}> {val}'
94 else:
95 for cnt, i in enumerate(iterable):
96 rtnStr += f'\n\t{cnt}: <{type(i).__name__}> {i}'
97 if len(iterable):
98 rtnStr += '\n'
99 rtnStr += getBrace(False)
100 else:
101 rtnStr = defaultStr + lengthAddOn
102
103 return rtnStr
104 else:
105 raise TypeError(f"Incorrect method `{method}` given. Options are 'pprint' and 'custom'.")
106
107def get_full_typename(var, add_braces:bool=True) -> str:
108 """ Get the name of the type of var, formatted nicely.
109 Also gets the types of the elements it holds, if `var` is a collection.
110 """
111 def getUniqueType(item):
112 returnMe = type(item).__name__
113 while isinstance(item, (tuple, list, set)):
114 try:
115 item = item[0]
116 except (KeyError, IndexError, TypeError):
117 returnMe += '('
118 break
119 returnMe += '(' + type(item).__name__
120
121 cnt = 0
122 for i in returnMe:
123 if i == '(':
124 cnt += 1
125 return returnMe + (')'*cnt)
126
127 if isinstance(var, dict):
128 name = type(var).__name__
129 if len(var) > 0:
130 rtn = f'{name}({type(list(var.keys())[0]).__name__}:{type(list(var.values())[0]).__name__})'
131 else:
132 rtn = f'{name}()'
133 elif isinstance(var, (tuple, list, set, dict)):
134 types = []
135 for i in var:
136 types.append(getUniqueType(i))
137 types = sorted(set(types), key=lambda x: types.index(x))
138 fullName = type(var).__name__ + str(tuple(types)).replace("'", "")
139 if len(types) == 1:
140 fullName = fullName[:-2] + ')'
141 rtn = fullName
142 else:
143 rtn = type(var).__name__
144 return f'<{rtn}>' if add_braces else rtn
145
146def print_debug_count(left_adjust:int=2):
147 """ Increment and print the debug count """
148 global _debug_count
149 _debug_count += 1
150 print(f'{str(_debug_count)+":":<{left_adjust+2}}', end='', style='count')
151
152def get_varname(var, full:bool=True, calls:int=1, metadata:inspect.FrameInfo=None) -> str:
153 """ Gets the variable name given to `var` """
154 def get_varname_manually(calls):
155 # Not quite sure why it's plus 2?...
156 context = get_metadata(calls=calls+2).code_context
157 if context is None:
158 return '?'
159 else:
160 line = context[0]
161 # Made with ezregex
162 # optional(stuff) + 'debug(' + group(stuff + multiOptional('(' + optional(stuff) + ')')) + ')' + optional(stuff)
163 match = re_match(r"(?:.+)?debug\‍((.+(?:\‍((?:.+)?\‍))*)\‍)(?:.+)?", line)
164 if match is None:
165 return '?'
166 else:
167 return match.groups()[0]
168 # ans.test('abc = lambda a: 6+ debug(parseColorParams((5, 5, 5)), name=8, clr=(a,b,c))\n')
169 return '?'
170
171 try:
172 rtn = argname('var', frame=calls+1)
173 # It's a *likely* string literal
174 except Exception as e:
175 if type(var) is str:
176 rtn = None
177 else:
178 try:
179 rtn = nameof(var, frame=calls+1, vars_only=False)
180 except Exception as e2:
181 if verbosity >= 2 and not isinstance(var, Exception):
182 raise e2
183 else:
184 rtn = get_varname_manually(calls+1)
185 except VarnameRetrievingError as e:
186 if verbosity:
187 raise e
188 else:
189 rtn = get_varname_manually(calls+1)
190
191 try:
192 # It auto-adds ' around strings
193 if rtn == str(var) or rtn == f"'{var}'":
194 # If the value is the same as the name, it must be a literal
195 return None
196 else:
197 return rtn
198 except:
199 return rtn
200
201def get_adjusted_filename(filename:str) -> str:
202 """ Gets the filename of the file given, adjusted to be relative to the appropriate root directory """
203 # Default behavior
204 if root_dir is None:
205 return relpath(filename)
206 elif len(root_dir):
207 return relpath(filename, root_dir)
208 else:
209 return filename
210
211def get_context(metadata:inspect.FrameInfo, func:bool=None, file:bool=None, path:bool=None) -> str:
212 """ Returns the stuff in the [] (the "context") """
213 if metadata is None:
214 return ''
215
216 func = display_func if func is None else (display_func or func)
217 file = display_file if file is None else (display_file or file)
218 path = display_path if path is None else (display_path or path)
219
220 # This logically must be true
221 if path:
222 file = True
223
224 s = '['
225 if path:
226 s += f'"{metadata.filename}", line {metadata.lineno}, in '
227 elif file:
228 s += f'"{get_adjusted_filename(metadata.filename)}", line {metadata.lineno}, in '
229 # We apparently don't want any context at all (for whatever reason)
230 elif not func:
231 return ''
232
233 if func:
234 if metadata.function.startswith('<'):
235 s += 'Global Scope'
236 else:
237 s += f'{metadata.function}()'
238 else:
239 # Take out the comma and the space, if we're not using them
240 s = s[:-5]
241 s += '] '
242 return s
243
244def print_stack_trace(calls, func, file, path):
245 for i in reversed(stack()[3:]):
246 print('\t', get_context(i, func, file, path), style='trace')
247
248# TODO Use inspect to do this instead (or possibly use __wrapped__ somehow?)
249def called_as_decorator(funcName, metadata=None, calls=1) -> 'Union[1, 2, 3, False]':
250 """ Return 1 if being used as a function decorator, 2 if as a class decorator, 3 if not sure, and False if neither. """
251 if metadata is None:
252 metadata = get_metadata(calls+1)
253
254 # print(metadata.code_context)
255 context = metadata.code_context
256 # I think this means we're using python from the command line
257 if context is None:
258 return False
259 line = context[0]
260
261 if funcName not in line:
262 if 'def ' in line:
263 return 1
264 elif 'class ' in line:
265 return 2
266 elif '@' in line:
267 return 3
268 elif '@' in line:
269 return 3
270
271 return False
272
273def print_context(calls:int=1, func:bool=True, file:bool=True, path:bool=False, color='context'):
274 print_debug_count()
275 print(get_context(get_metadata(1 + calls)), end='', style=color)
276
277class Undefined: pass
278undefined = Undefined()
279
280# Original Version
281def debug(
282 var=undefined, # The variable to debug
283 name: str=None, # Don't try to get the name, use this one instead
284 color=..., # A number (0-5), a 3 item tuple/list, or None
285 show: Literal['pprint', 'custom', 'repr']='custom',
286 func: bool=None, # Expressly show what function we're called from
287 file: bool=None, # Expressly show what file we're called from
288 path: bool=None, # Show just the file name, or the full filepath
289 # useRepr: bool=False, # Whether we should print the repr of var instead of str
290 calls: int=1, # Add extra calls
291 active: bool=True, # If this is false, don't do anything
292 background: bool=False, # Whether the color parameter applies to the forground or the background
293 # limitToLine: bool=True, # When printing iterables, whether we should only print items to the end of the line
294 # minItems: int=50, # Minimum number of items to print when printing iterables (overrides limitToLine)
295 # maxItems: int=-1, # Maximum number of items to print when printing iterables, use None or negative to specify no limit
296 depth: int=...,
297 width: int=...,
298 stackTrace: bool=False, # Print a stack trace
299 raiseError: bool=False, # If var is an error type, raise it
300 clr=..., # Alias of color
301 # repr: bool=False, # Alias of useRepr
302 trace: bool=False, # Alias of stackTrace
303 bg: bool=False, # Alias of background
304 throwError: bool=False, # Alias of raiseError
305 throw: bool=False # Alias of raiseError
306 ) -> "var":
307 """ Print variable names and values for easy debugging.
308
309 Usage:
310 debug() -> Prints a standard message to just tell you that it's getting called
311 debug('msg') -> Prints the string along with metadata
312 debug(var) -> Prints the variable name, type, and value
313 foo = debug(bar) -> Prints the variable name, type, and value, and returns the variable
314 @debug -> Use as a decorator to make note of when the function is called
315
316 Args:
317 var: The variable or variables to print
318 name: Manully specify the name of the variable
319 color: A number between 0-5, or 3 or 4 tuple/list of color data to print the debug message as
320 func: Ensure that the function the current call is called from is shown
321 file: Ensure that the file the current call is called from is shown
322 path: Show the full path of the current file, isntead of it's relative path
323 useRepr: Use __repr__() instead of __str__() on the given variable
324 limitToLine: If var is a list/tuple/dict/set, only show as many items as will fit on one terminal line, overriden by minItems
325 minItems: If var is a list/tuple/dict/set, don't truncate more than this many items
326 maxItems: If var is a list/tuple/dict/set, don't show more than this many items
327 stackTrace: Prints a neat stack trace of the current call
328 calls: If you're passing in a return from a function, say calls=2
329 background: Changes the background color instead of the forground color
330 active: Conditionally disables the function
331 clr: Alias of color
332 _repr: Alias of useRepr
333 trace: Alias of stackTrace
334 bg: Alias of background
335 """
336 if not active or not __debug__:
337 return var
338
339 # TODO:
340 # Implement rich.inspect
341 # implement inspect.ismodule, .ismethod, .isfunction, .isclass, etc.
342
343
344 stackTrace = stackTrace or trace
345 # useRepr = useRepr or repr
346 background = background or bg
347 throwError = throw or throwError or raiseError
348 file = file or display_file
349 func = func or display_func
350 path = path or display_path
351 useColor = ('default' if clr is Ellipsis else clr) if color is Ellipsis else color
352
353 if isinstance(var, Warning):
354 useColor = 'warn'
355 elif isinstance(var, Exception):
356 useColor = 'error'
357
358 # if maxItems < 0 or maxItems is None:
359 # maxItems = 1000000
360
361 # +1 call because we don't want to get this line, but the one before it
362 metadata = get_metadata(calls+1)
363
364 _print_context = lambda: print(get_context(metadata, func, file, path), end='', style='context')
365
366 #* First see if we're being called as a decorator
367 # inspect.
368 if callable(var) and called_as_decorator('debug', metadata):
369 def wrap(*args, **kwargs):
370 # +1 call because we don't want to get this line, but the one before it
371 metadata = get_metadata(2)
372 print_debug_count()
373
374 if stackTrace:
375 print_stack_trace(2, func, file, path)
376
377 _print_context() # was of style='note'
378 print(f'{var.__name__}() called!', style='note')
379 return var(*args, **kwargs)
380
381 return wrap
382
383 print_debug_count()
384
385 if stackTrace:
386 print_stack_trace(calls+1, func, file, path)
387
388 #* Only print the "HERE! HERE!" message
389 if var is undefined:
390 # print(get_context(metadata, func, file, path), end='', style=clr)
391 _print_context()
392
393 if not metadata.function.startswith('<'):
394 print(f'{metadata.function}() called ', end='', style=useColor)
395
396 print('HERE!', style=useColor)
397 return
398
399 #* Print the standard line
400 # print(get_context(metadata, func, file, path), end='', style='metadata')
401 _print_context()
402
403 #* Seperate the variables into a tuple of (typeStr, varString)
404 varType = get_full_typename(var)
405 if show == 'repr':
406 varVal = _repr(var)
407 else:
408 if isinstance(var, (tuple, list, set, dict)):
409 varVal = prettify(var, method=show)
410 else:
411 varVal = str(var)
412
413 #* Actually get the name
414 varName = get_varname(var, calls=calls, metadata=metadata) if name is None else name
415
416 # It's a string literal
417 if varName is None:
418 print('<literal> ', end='', style='type')
419 print(var, style='value')
420 return var
421
422 print(varType, end=' ', style='type')
423 print(varName, end=' ', style='name')
424 print('=', end=' ', style='equals')
425 print(varVal, style='value')
426
427 if isinstance(var, Exception) and throwError:
428 raise var
429
430 # Does the same this as debugged used to
431 return var
432
433class Debug:
434 def __init__(self):
435 Log.setLevel(logging.DEBUG)
436
437
438 def __call__(self,
439 var=undefined,
440 name:str=None,
441 color=...,
442 inspect:bool=False,
443 repr:bool=True,
444 trace:bool=False,
445 throw:bool=False,
446 calls:int=1,
447 active:bool=True,
448 clr=...,
449 ) -> "var":
450 """ Print variable names and values for easy debugging.
451
452 Usage:
453 debug() -> Prints a standard message to just tell you that it's getting called
454 debug('msg') -> Prints the string along with metadata
455 debug(var) -> Prints the variable name, type, and value
456 foo = debug(bar) -> Prints the variable name, type, and value, and returns the variable
457 @debug -> Use as a decorator to make note of when the function is called
458
459 Args:
460 var: The variable or variables to print
461 name: Manully specify the name of the variable
462 color/clr: Literally anything that specifies a color, including a single number for unique colors
463 inspect: Calls rich.inspect on var
464 repr: Uses repr by default, set to False to use str instead
465 trace: Prints a neat stack trace of the current call
466 calls: If you're passing in a return from a function, say calls=2
467 active: Conditionally disables the function
468 """
469 # If not active, don't display
470 if not active or not Log.isEnabledFor(logging.DEBUG):
471 return var
472
473 # print(_inspect.currentframe().f_back.function)
474 # print('---', stack()[-2].frame.f_code.co_names)
475 if hasattr(var, '__call__'):
476 # print(_inspect.signature)
477 # if _inspect.currentframe().f_back.f_locals.get('__class__') == self.__class__:
478 print("Called as a decorator")
479 return var
480 else:
481 print("Called directly")
482 return var
483
484
485 # 'clear',
486 # 'f_back',
487 # 'f_builtins',
488 # 'f_code',
489 # 'f_globals',
490 # 'f_lasti',
491 # 'f_lineno',
492 # 'f_locals',
493 # 'f_trace',
494 # 'f_trace_lines',
495 # 'f_trace_opcodes'
496
497 stackTrace = stackTrace or trace
498 # useRepr = useRepr or repr
499 background = background or bg
500 throwError = throw or throwError or raiseError
501 file = file or display_file
502 func = func or display_func
503 path = path or display_path
504 useColor = ('default' if clr is Ellipsis else clr) if color is Ellipsis else color
505
506 if isinstance(var, Warning):
507 useColor = 'warn'
508 elif isinstance(var, Exception):
509 useColor = 'error'
510
511 # if maxItems < 0 or maxItems is None:
512 # maxItems = 1000000
513
514 # +1 call because we don't want to get this line, but the one before it
515 metadata = get_metadata(calls+1)
516
517 _print_context = lambda: print(get_context(metadata, func, file, path), end='', style='context')
518
519 #* First see if we're being called as a decorator
520 # inspect.
521 if callable(var) and called_as_decorator('debug', metadata):
522 def wrap(*args, **kwargs):
523 # +1 call because we don't want to get this line, but the one before it
524 metadata = get_metadata(2)
525 print_debug_count()
526
527 if stackTrace:
528 print_stack_trace(2, func, file, path)
529
530 _print_context() # was of style='note'
531 print(f'{var.__name__}() called!', style='note')
532 return var(*args, **kwargs)
533
534 return wrap
535
536 print_debug_count()
537
538 if stackTrace:
539 print_stack_trace(calls+1, func, file, path)
540
541 #* Only print the "HERE! HERE!" message
542 if var is undefined:
543 # print(get_context(metadata, func, file, path), end='', style=clr)
544 _print_context()
545
546 if not metadata.function.startswith('<'):
547 print(f'{metadata.function}() called ', end='', style=useColor)
548
549 print('HERE!', style=useColor)
550 return
551
552 #* Print the standard line
553 # print(get_context(metadata, func, file, path), end='', style='metadata')
554 _print_context()
555
556 #* Seperate the variables into a tuple of (typeStr, varString)
557 varType = get_full_typename(var)
558 if show == 'repr':
559 varVal = _repr(var)
560 else:
561 if isinstance(var, (tuple, list, set, dict)):
562 varVal = prettify(var, method=show)
563 else:
564 varVal = str(var)
565
566 #* Actually get the name
567 varName = get_varname(var, calls=calls, metadata=metadata) if name is None else name
568
569 # It's a string literal
570 if varName is None:
571 print('<literal> ', end='', style='type')
572 print(var, style='value')
573 return var
574
575 print(varType, end=' ', style='type')
576 print(varName, end=' ', style='name')
577 print('=', end=' ', style='equals')
578 print(varVal, style='value')
579
580 if isinstance(var, Exception) and throwError:
581 raise var
582
583 # Does the same this as debugged used to
584 return var
585
586# debug = Debug()
587
588
589# A quick and dirty version just to get it working again
590def debug(var=undefined, name=None, color=1, trace=False, calls=1):
591 r, g, b = parse_color(color)
592
593 metadata = get_metadata(calls+1)
594 _print_context = lambda: print('[dark gray]' + str(get_context(metadata, True, True, True)) + '[/]', end='')
595
596 # Called with no arguments
597 if var is undefined:
598 _print_context()
599
600 # if not metadata.function.startswith('<'):
601 # print(f'{metadata.function}() called ', end='', style=useColor)
602
603 print('HERE!')
604 return
605
606 #* Print the standard line
607 # print(get_context(metadata, func, file, path), end='', style='metadata')
608 _print_context()
609
610 #* Seperate the variables into a tuple of (typeStr, varString)
611 varType = get_full_typename(var)
612 # if show == 'repr':
613 # varVal = _repr(var)
614 # else:
615 if isinstance(var, (tuple, list, set, dict)):
616 varVal = prettify(var)
617 else:
618 varVal = str(var)
619
620 #* Actually get the name
621 varName = name or get_varname(var, calls=calls, metadata=metadata)
622
623 # It's a string literal
624 if varName is None:
625 print('[type]', '<literal> ', end='')
626 print('[value]', var)
627 return var
628
629 print('[type]', varType, end=' ')
630 print('[name]', varName, end=' ')
631 print('[equals]', '=', end=' ')
632 print('[value]', varVal)
633
634 # if isinstance(var, Exception) and throwError:
635 # raise var
636
637 # Does the same this as debugged used to
638 return var
"var" __call__(self, var=undefined, str name=None, color=..., bool inspect=False, bool repr=True, bool trace=False, bool throw=False, int calls=1, bool active=True, clr=...)
Print variable names and values for easy debugging.
Definition: debugging.py:449
str get_full_typename(var, bool add_braces=True)
Get the name of the type of var, formatted nicely.
Definition: debugging.py:107
str prettify(Union[tuple, list, set, dict] iterable, Literal['pprint', 'custom'] method='custom', int width=..., int depth=..., int indent=4)
"Cast" a tuple, list, set or dict to a string, automatically shorten it if it's long,...
Definition: debugging.py:57
str get_adjusted_filename(str filename)
Gets the filename of the file given, adjusted to be relative to the appropriate root directory.
Definition: debugging.py:201