Skip to content

Commit

Permalink
Added feature to Kwargs_To_Self to lock the attributes (good to detec…
Browse files Browse the repository at this point in the history
…t unpexected changes to the self object)

on Traces_Call:
 - added enabled config setting
 - added option to do a deepcopy of the locals (important when wanting to understand better the value of the locals at a particular execution point)
 - added multiple helpers to config to set the options: all, capture, locals, lines
 - removed trace_title with config.title (which is the correct place to get that value)
on Trace_Call__Print_lines changed the order of item in table to have the line number showing first (since it an be quite helpful)
removed some stats method that were not very stable in CI Pipeline
  • Loading branch information
DinisCruz committed Jan 10, 2024
1 parent d62c198 commit dea3117
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 169 deletions.
18 changes: 18 additions & 0 deletions osbot_utils/base_classes/Kwargs_To_Self.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class MyConfigurableClass(Kwargs_To_Self):
that are already defined in the class to be set.
"""

__lock_attributes__ = False

def __init__(self, **kwargs):
"""
Initialize an instance of the derived class, strictly assigning provided keyword
Expand Down Expand Up @@ -92,6 +94,12 @@ def __init__(self, **kwargs):
def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb): pass

def __setattr__(self, name, value):
if self.__lock_attributes__:
if not hasattr(self, name):
raise AttributeError(f"'[Object Locked] Current object is locked (with __lock_attributes__=True) which prenvents new attributes allocations (i.e. setattr calls). In this case {type(self).__name__}' object has no attribute '{name}'") from None
super().__setattr__(name, value)

@classmethod
def __cls_kwargs__(cls):
"""Return current class dictionary of class level variables and their values."""
Expand Down Expand Up @@ -162,3 +170,13 @@ def __locals__(self):
if not isinstance(v, types.FunctionType):
kwargs[k] = v
return kwargs

def locked(self, value=True):
self.__lock_attributes__ = value
return self

def update_from_kwargs(self, **kwargs):
"""Update instance attributes with values from provided keyword arguments."""
for key, value in kwargs.items():
setattr(self, key, value)
return self
40 changes: 24 additions & 16 deletions osbot_utils/utils/trace/Trace_Call.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@
def trace_calls(title=None, print_traces=True, show_locals=False, source_code=False, ignore=None, include=None,
max_string=None, show_types=False, show_caller=False, show_class=False, show_path=False,
show_duration=False, duration_bigger_than=0, contains=None, show_internals=False,
extra_data=False, show_lines=False, print_lines=False):
extra_data=False, show_lines=False, print_lines=False, enabled=True):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
config_kwargs = dict(title=title, print_traces_on_exit=print_traces, print_locals=show_locals,
trace_capture_source_code=source_code, ignore_start_with=ignore,
capture_start_with=include, print_max_string_length=max_string,
trace_capture_start_with=include, print_max_string_length=max_string,
show_parent_info=show_types, show_method_class=show_class,
show_caller=show_caller, show_source_code_path=show_path,
capture_duration=show_duration, print_duration= show_duration,
with_duration_bigger_than=duration_bigger_than,
trace_capture_contains=contains, trace_show_internals=show_internals,
capture_extra_data=extra_data, trace_capture_lines=show_lines,
print_lines_on_exit=print_lines)
capture_extra_data=extra_data,
print_lines_on_exit=print_lines, trace_enabled=enabled,
trace_capture_lines=show_lines or print_lines)

config = Trace_Call__Config().locked().update_from_kwargs(**config_kwargs)

config = Trace_Call__Config(**config_kwargs)
with Trace_Call(config=config):
result = func(*args, **kwargs)
return result
Expand All @@ -46,7 +48,7 @@ def __init__(self, **kwargs):
self.trace_call_print_traces = Trace_Call__Print_Traces(config=self.config)
self.trace_call_view_model = Trace_Call__View_Model ()
self.config.print_traces_on_exit = self.config.print_traces_on_exit
self.config.trace_capture_start_with = self.config.capture_start_with or [] # todo add a better way to set these to [] when then value is null
#self.config.trace_capture_start_with = self.config.capture_start_with or [] # todo add a better way to set these to [] when then value is null
self.config.trace_ignore_start_with = self.config.ignore_start_with or [] # probablty better done inside Kwargs_To_Self since it doesn't make sense for lists or dicts to have None value
self.config.trace_capture_contains = self.config.trace_capture_contains or [] # and None will be quite common since we can use [] on method's params
self.config.print_max_string_length = self.config.print_max_string_length or PRINT_MAX_STRING_LENGTH
Expand All @@ -61,37 +63,39 @@ def __exit__(self, exc_type, exc_val, exc_tb):
return self.on_exit()

def on_enter(self):
self.start() # Start the tracing
if self.config.trace_enabled:
self.start() # Start the tracing
return self

def on_exit(self):
self.stop() # Stop the tracing
if self.config.print_traces_on_exit:
self.print()
if self.config.print_lines_on_exit:
self.print_lines()
if self.config.trace_enabled:
self.stop() # Stop the tracing
if self.config.print_traces_on_exit:
self.print()
if self.config.print_lines_on_exit:
self.print_lines()

def capture_all(self):
self.config.trace_capture_all = True
return self

def create_view_model(self):
def view_data(self):
return self.trace_call_view_model.create(self.stack)

def print(self):
view_model = self.create_view_model()
view_model = self.view_data()
self.trace_call_print_traces.print_traces(view_model)
#self.print_lines()
return view_model

def print_lines(self):
print()
view_model = self.create_view_model()
view_model = self.view_data()
print_lines = Trace_Call__Print_Lines(config=self.config, view_model=view_model)
print_lines.print_lines()

def start(self):
self.trace_call_handler.stack.add_node(title=self.trace_call_handler.trace_title)
self.trace_call_handler.stack.add_node(title=self.trace_call_handler.config.title)
self.prev_trace_function = sys.gettrace()
sys.settrace(self.trace_call_handler.trace_calls) # Set the new trace function

Expand All @@ -101,3 +105,7 @@ def stop(self):

def stats(self):
return self.trace_call_handler.stats

def stats_data(self):
return self.trace_call_handler.stats.raw_call_stats

38 changes: 36 additions & 2 deletions osbot_utils/utils/trace/Trace_Call__Config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from osbot_utils.utils.Dev import pprint

from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self

PRINT_MAX_STRING_LENGTH = 100
Expand All @@ -9,8 +11,8 @@ class Trace_Call__Config(Kwargs_To_Self):
capture_extra_data : bool
capture_frame : bool = True
capture_frame_stats : bool
deep_copy_locals : bool
trace_capture_lines : bool
capture_start_with : list
ignore_start_with : list
print_duration : bool
print_max_string_length : int = PRINT_MAX_STRING_LENGTH
Expand All @@ -25,6 +27,38 @@ class Trace_Call__Config(Kwargs_To_Self):
trace_capture_source_code : bool
trace_capture_start_with : list
trace_capture_contains : list
trace_enabled : bool = True
trace_show_internals : bool
trace_ignore_start_with : list
with_duration_bigger_than : bool
with_duration_bigger_than : bool

def all(self, print_traces=True):
self.trace_capture_all = True
self.print_traces_on_exit = print_traces

def capture(self, starts_with=None, contains=None, ignore=None):
if starts_with:
if type(starts_with) is str:
starts_with = [starts_with]
self.trace_capture_start_with = starts_with
if contains:
if type(contains) is str:
contains = [contains]
self.trace_capture_contains = contains
if ignore:
if type(ignore) is str:
ignore = [ignore]
self.ignore_start_with = ignore
self.print_traces_on_exit = True

def locals(self):
self.capture_locals = True
self.print_locals = True

def lines(self, print_traces=True, print_lines=True):
self.trace_capture_lines = True
self.print_traces_on_exit = print_traces
self.print_lines_on_exit = print_lines

def print_config(self):
pprint(self.__locals__())
49 changes: 32 additions & 17 deletions osbot_utils/utils/trace/Trace_Call__Handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ class Trace_Call__Handler(Kwargs_To_Self):

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.trace_title = self.config.title or DEFAULT_ROOT_NODE_NODE_TITLE # Title for the trace root node
self.config.title = self.config.title or DEFAULT_ROOT_NODE_NODE_TITLE # Title for the trace root node
self.stack.config = self.config

def add_default_root_node(self):
return self.stack.add_node(title=self.trace_title)
return self.stack.add_node(title=self.config.title)

def add_line(self, frame):
if self.config.trace_capture_lines:
Expand All @@ -43,25 +43,36 @@ def add_line(self, frame):
return False

def add_line_to_node(self, frame, target_node, event):
def source_code_for_frame(function_name):
try:
return inspect.getsource(frame.f_code)
except Exception as error:
return ''
#print(self.stack.line_index, f'def {function_name}() : error] {error}' )
#return f'def {function_name}() : [error] {error}'

if frame and target_node:
func_name = frame.f_code.co_name
module = frame.f_globals.get("__name__", "")
self_local = class_full_name(frame.f_locals.get('self'))
stack_size = len(self.stack)
line =''
line_number=0
if event == 'call': # if this is a call we need to do the code below to get the actual method signature (and decorators)
function_name = frame.f_code.co_name
filename = frame.f_code.co_filename # Get the filename where the function is defined
start_line_number = frame.f_code.co_firstlineno # Get the starting line number
source_lines = inspect.getsource(frame.f_code).split('\n')
def_line_number = start_line_number # Try to find the actual 'def' line
for line in source_lines:
if line.strip().startswith('def ' + function_name):
break
def_line_number += 1
else:
def_line_number = start_line_number # If the 'def' line wasn't found, default to the starting line
line_number = def_line_number
line = linecache.getline(filename, line_number).rstrip() # todo: refactor this to not capture this info here, and to use the Ast_* utils to get a better source code mapping
source_lines = source_code_for_frame(function_name).split('\n')
if source_lines:
def_line_number = start_line_number # Try to find the actual 'def' line
for line in source_lines:
if line.strip().startswith('def ' + function_name):
break
def_line_number += 1
else:
def_line_number = start_line_number # If the 'def' line wasn't found, default to the starting line
line_number = def_line_number
line = linecache.getline(filename, line_number).rstrip() # todo: refactor this to not capture this info here, and to use the Ast_* utils to get a better source code mapping
else:
filename = frame.f_code.co_filename # get the filename
line_number = frame.f_lineno # get the current line number
Expand All @@ -75,6 +86,8 @@ def add_line_to_node(self, frame, target_node, event):
stack_size=stack_size)
target_node.lines.append(line_data)
return True
# else:
# print(f'no line for : {self.stack.line_index}, {module}.{func_name}')
return False

def add_frame(self, frame):
Expand All @@ -93,6 +106,8 @@ def handle_event__call(self, frame):
if self.config.trace_capture_lines:
self.add_line_to_node(frame, new_node,'call')
return new_node
else:
self.stats.calls_skipped += 1

def handle_event__line(self, frame):
return self.add_line(frame)
Expand Down Expand Up @@ -150,17 +165,17 @@ def stack_top(self):

def trace_calls(self, frame, event, arg):
if event == 'call':
self.stats.event_call +=1
self.stats.calls +=1
self.handle_event__call(frame) # todo: handle bug with locals which need to be serialised, since it's value will change
elif event == 'return':
self.stats.event_return += 1
self.stats.returns += 1
self.handle_event__return(frame, arg)
elif event == 'exception':
self.stats.event_exception +=1 # for now don't handle exception events
self.stats.exceptions +=1 # for now don't handle exception events
elif event == 'line':
self.handle_event__line(frame)
self.stats.event_line +=1
self.stats.lines +=1
else:
self.stats.event_unknown += 1
self.stats.unknowns += 1

return self.trace_calls
65 changes: 34 additions & 31 deletions osbot_utils/utils/trace/Trace_Call__Print_Lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,40 +48,43 @@ def max_fields_value(self, items, *fields):
return max_value

def print_lines(self, ):
lines = self.lines()
lines = self.lines()
print("--------- CALL TRACER (Lines)----------")
print(f"Here are the {len(lines)} lines captured\n")

max_length__sig = self.max_fields_length(lines, 'module', 'func_name') + 2
max_length__line = self.max_fields_length(lines, 'line' ) + self.max_fields_value (lines, 'stack_size' ) + 5 # this + 5 helps with the alignment of the larger line (so that it doesn't overflow the table)
max_length__self = self.max_fields_length(lines, 'self_local' )
print( '┌─────┬─' + '─' * max_length__line +'──┬─' + '─' * max_length__sig + '─┬─' + '─' * max_length__self + '─┬──────┬───────┐ ')
print(f"│ # │ {'Source code':<{max_length__line}}{'Method Class and Name':<{max_length__sig}}{'Self object':<{max_length__self}} │ Line │ Depth │ ")
print( '├─────┼─' + '─' * max_length__line +'──┼─' + '─'* max_length__sig + '─┼─' + '─' * max_length__self + '─┼──────┼───────┤ ')
print( '┌─────┬──────┬─' + '─' * max_length__line +'──┬─' + '─' * max_length__sig + '─┬─' + '─' * max_length__self + '─┬───────┐ ')
print(f"│ # │ Line │ {'Source code':<{max_length__line}}{'Method Class and Name':<{max_length__sig}}{'Self object':<{max_length__self}} │ Depth │ ")
print( '├─────┼──────┼─' + '─' * max_length__line +'──┼─' + '─'* max_length__sig + '─┼─' + '─' * max_length__self + '─┼───────┤ ')
for line_data in lines:
index = line_data.get('index')
func_name = line_data.get('func_name')
line_number = line_data.get('line_number')
module = line_data.get('module')
event = line_data.get('event')
line = line_data.get('line')
self_local = line_data.get('self_local') or ''
method_sig = f"{module}.{func_name}"
stack_size = line_data.get('stack_size') -1

text_depth = f'{stack_size:5}'
text_depth_padding = ' ' * ((stack_size-1) * 2)
text_index = f'{text_grey(index):12}'
text_line_no = f'{line_number:4}'
text_method_sig = f'{method_sig:{max_length__sig}}'

if event == 'call':
text_line = f'{text_bold_green(line)}'
else:
text_line = f'{text_light_grey(line)}'

text_line_padding = ' ' * (max_length__line - ansi_text_visible_length(text_line) - len(text_depth_padding))
text_source_code = f'{text_depth_padding}{text_line} {text_line_padding}'

print(f"│ {text_index}{text_source_code}{text_method_sig}{self_local:<{max_length__self}}{text_line_no}{text_depth} │")

print('└─────┴──' + '─' * max_length__line + '─┴─' + '─' * max_length__sig + '─┴──' + '─' * max_length__self + '┴──────┴───────┘')
index = line_data.get('index')
func_name = line_data.get('func_name')
line_number = line_data.get('line_number')
module = line_data.get('module')
event = line_data.get('event')
line = line_data.get('line')
self_local = line_data.get('self_local') or ''
method_sig = f"{module}.{func_name}"
stack_size = line_data.get('stack_size') -1

text_depth = f'{stack_size:5}'
text_depth_padding = ' ' * ((stack_size-1) * 2)
text_index = f'{text_grey(index):12}'
text_line_no = f'{line_number:4}'
text_method_sig = f'{method_sig:{max_length__sig}}'

if event == 'call':
text_line = f'{text_bold_green(line)}'
else:
text_line = f'{text_light_grey(line)}'

text_line_padding = ' ' * (max_length__line - ansi_text_visible_length(text_line) - len(text_depth_padding))
text_source_code = f'{text_depth_padding}{text_line} {text_line_padding}'

print(f"│ {text_index}{text_line_no}{text_source_code}{text_method_sig}{self_local:<{max_length__self}}{text_depth} │")

print('└─────┴──────┴──' + '─' * max_length__line + '─┴─' + '─' * max_length__sig + '─┴──' + '─' * max_length__self + '┴───────┘')


Loading

0 comments on commit dea3117

Please sign in to comment.