From 2647ad68d747f0515e243fb6e7f2e2f982dbaa21 Mon Sep 17 00:00:00 2001 From: Dinis Cruz Date: Tue, 20 Aug 2024 15:49:05 +0100 Subject: [PATCH] on Trace_Call, added trace_ignore_contains feature, added support for starting and stopping trace in separate threads couple refactorings fixed tests --- osbot_utils/helpers/trace/Trace_Call.py | 44 +++++++++++++++++++ .../helpers/trace/Trace_Call__Config.py | 1 + .../helpers/trace/Trace_Call__Handler.py | 23 +++++++--- osbot_utils/testing/Stderr.py | 14 ++++-- osbot_utils/testing/Stdout.py | 12 +++-- osbot_utils/utils/Env.py | 3 ++ tests/unit/helpers/trace/test_Trace_Call.py | 3 +- .../helpers/trace/test_Trace_Call__Config.py | 1 + tests/unit/helpers/trace/test_Trace_Files.py | 3 +- 9 files changed, 90 insertions(+), 14 deletions(-) diff --git a/osbot_utils/helpers/trace/Trace_Call.py b/osbot_utils/helpers/trace/Trace_Call.py index db3981e1..5e848ec0 100644 --- a/osbot_utils/helpers/trace/Trace_Call.py +++ b/osbot_utils/helpers/trace/Trace_Call.py @@ -1,5 +1,6 @@ import linecache import sys +import threading from functools import wraps from osbot_utils.base_classes.Kwargs_To_Self import Kwargs_To_Self @@ -8,6 +9,8 @@ from osbot_utils.helpers.trace.Trace_Call__Print_Lines import Trace_Call__Print_Lines from osbot_utils.helpers.trace.Trace_Call__Print_Traces import Trace_Call__Print_Traces from osbot_utils.helpers.trace.Trace_Call__View_Model import Trace_Call__View_Model +from osbot_utils.testing.Stdout import Stdout +from osbot_utils.utils.Str import ansi_to_text def trace_calls(title = None , print_traces = True , show_locals = False, source_code = False , @@ -59,6 +62,7 @@ def __init__(self, **kwargs): 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 self.stack = self.trace_call_handler.stack + self.trace_on_thread__data = {} #self.prev_trace_function = None # Stores the previous trace function @@ -94,6 +98,13 @@ def print(self): #self.print_lines() return view_model + def print_to_str(self): + with Stdout() as stdout: + self.print() + trace_data = ansi_to_text(stdout.value()) + return trace_data + + def print_lines(self): print() view_model = self.view_data() @@ -106,6 +117,27 @@ def start(self): self.started = True # set this here so that it does show in the trace sys.settrace(self.trace_call_handler.trace_calls) # Set the new trace function + def start__on_thread(self, root_node=None): + if sys.gettrace() is None: + current_thread = threading.current_thread() + thread_sys_trace = sys.gettrace() + thread_name = current_thread.name + thread_id = current_thread.native_id + thread_nodes = [] + thread_data = dict(thread_name = thread_name , + thread_id = thread_id , + thread_nodes = thread_nodes , + thread_sys_trace = thread_sys_trace) + title = f"Thread: {thread_name} ({thread_id})" + thread_node__for_title = self.trace_call_handler.stack.add_node(title=title) # Add node with name of Thread + + if root_node: # Add node with name of node + thread_node__for_root = self.trace_call_handler.stack.add_node(root_node) + thread_nodes.append(thread_node__for_root) + thread_nodes.append(thread_node__for_title) + sys.settrace(self.trace_call_handler.trace_calls) + self.trace_on_thread__data[thread_id] = thread_data + def stop(self): if self.started: @@ -113,6 +145,18 @@ def stop(self): self.stack.empty_stack() self.started = False + def stop__on_thread(self): + current_thread = threading.current_thread() + thread_id = current_thread.native_id + thread_data = self.trace_on_thread__data.get(thread_id) + if thread_data: # if there trace_call set up in the current thread + thread_sys_trace = thread_data.get('thread_sys_trace') + thread_nodes = thread_data.get('thread_nodes') + for thread_node in thread_nodes: # remove extra nodes added during start__on_thread + self.trace_call_handler.stack.pop(thread_node) + sys.settrace(thread_sys_trace) # restore previous sys.trace value + del self.trace_on_thread__data[thread_id] + def stats(self): return self.trace_call_handler.stats diff --git a/osbot_utils/helpers/trace/Trace_Call__Config.py b/osbot_utils/helpers/trace/Trace_Call__Config.py index 36c09163..ed67810c 100644 --- a/osbot_utils/helpers/trace/Trace_Call__Config.py +++ b/osbot_utils/helpers/trace/Trace_Call__Config.py @@ -33,6 +33,7 @@ class Trace_Call__Config(Kwargs_To_Self): trace_capture_contains : list trace_enabled : bool = True trace_ignore_start_with : list + trace_ignore_contains : list trace_show_internals : bool trace_up_to_depth : int with_duration_bigger_than : float diff --git a/osbot_utils/helpers/trace/Trace_Call__Handler.py b/osbot_utils/helpers/trace/Trace_Call__Handler.py index 4115b37a..fdbc3ace 100644 --- a/osbot_utils/helpers/trace/Trace_Call__Handler.py +++ b/osbot_utils/helpers/trace/Trace_Call__Handler.py @@ -20,11 +20,19 @@ '__default__value__' , '__setattr__' , ''] -GLOBAL_MODULES_TO_IGNORE = ['osbot_utils.helpers.trace.Trace_Call' , # todo: map out and document why exactly these modules are ignore (and what is the side effect) - 'osbot_utils.helpers.CPrint' , # also see if this should be done here or at the print/view stage - 'osbot_utils.helpers.Print_Table' , - 'osbot_utils.decorators.methods.cache_on_self' , - 'codecs'] +GLOBAL_MODULES_TO_IGNORE = ['osbot_utils.helpers.trace.Trace_Call' , # todo: map out and document why exactly these modules are ignore (and what is the side effect) + 'osbot_utils.helpers.trace.Trace_Call__Config' , + 'osbot_utils.helpers.trace.Trace_Call__View_Model' , + 'osbot_utils.helpers.trace.Trace_Call__Print_Traces' , + 'osbot_utils.helpers.trace.Trace_Call__Stack' , + 'osbot_utils.base_classes.Type_Safe' , + 'osbot_utils.helpers.CPrint' , # also see if this should be done here or at the print/view stage + 'osbot_utils.helpers.Print_Table' , + 'osbot_utils.decorators.methods.cache_on_self' , + 'codecs' ] + +#GLOBAL_MODULES_TO_IGNORE = [] +#GLOBAL_FUNCTIONS_TO_IGNORE = [] class Trace_Call__Handler(Kwargs_To_Self): config : Trace_Call__Config @@ -171,6 +179,11 @@ def should_capture(self, frame): if module.startswith(item) or func_name.startswith(item): capture = False break + + for item in self.config.trace_ignore_contains: # Check if the module should be ignored + if item in module or item in func_name: + capture = False + break return capture def stack_json__parse_node(self, stack_node: Trace_Call__Stack_Node): diff --git a/osbot_utils/testing/Stderr.py b/osbot_utils/testing/Stderr.py index 6abcd471..f2af9061 100644 --- a/osbot_utils/testing/Stderr.py +++ b/osbot_utils/testing/Stderr.py @@ -2,17 +2,23 @@ from contextlib import redirect_stderr -class Stderr: +class Stderr: # todo: refactor with Stdout whose code is 90% the same as this one. Add class to capture both at the same time def __init__(self): self.output = io.StringIO() self.redirect_stderr = redirect_stderr(self.output) def __enter__(self): - self.redirect_stderr.__enter__() + self.start() return self - def __exit__(self, exc_type, exc_val, exc_tb): - self.redirect_stderr.__exit__(exc_type, exc_val, exc_tb) + def __exit__(self, *args, **kwargs): + self.stop(*args, **kwargs) + + def start(self): + self.redirect_stderr.__enter__() + + def stop(self, exc_type=None, exc_inst=None, exc_tb=None): + self.redirect_stderr.__exit__(exc_type, exc_inst, exc_tb) def value(self): return self.output.getvalue() diff --git a/osbot_utils/testing/Stdout.py b/osbot_utils/testing/Stdout.py index ee1e61db..25ffb05f 100644 --- a/osbot_utils/testing/Stdout.py +++ b/osbot_utils/testing/Stdout.py @@ -8,11 +8,17 @@ def __init__(self): self.redirect_stdout = redirect_stdout(self.output) def __enter__(self): - self.redirect_stdout.__enter__() + self.start() return self - def __exit__(self, exc_type, exc_val, exc_tb): - self.redirect_stdout.__exit__(exc_type, exc_val, exc_tb) + def __exit__(self, *args, **kwargs): + self.stop(*args, **kwargs) + + def start(self): + self.redirect_stdout.__enter__() + + def stop(self, exc_type=None, exc_inst=None, exc_tb=None): + self.redirect_stdout.__exit__(exc_type, exc_inst, exc_tb) def value(self): return self.output.getvalue() diff --git a/osbot_utils/utils/Env.py b/osbot_utils/utils/Env.py index 31092c3d..15c34292 100644 --- a/osbot_utils/utils/Env.py +++ b/osbot_utils/utils/Env.py @@ -93,6 +93,9 @@ def find_dotenv_file(start_path=None, env_file_to_find='.env'): def in_github_action(): return os.getenv('GITHUB_ACTIONS') == 'true' +def in_pytest_with_coverage(): + return os.getenv('COVERAGE_RUN') == 'true' + def in_python_debugger(): if sys.gettrace() is not None: # Check for a trace function return True diff --git a/tests/unit/helpers/trace/test_Trace_Call.py b/tests/unit/helpers/trace/test_Trace_Call.py index b2246b66..7ea0d3b9 100644 --- a/tests/unit/helpers/trace/test_Trace_Call.py +++ b/tests/unit/helpers/trace/test_Trace_Call.py @@ -52,7 +52,8 @@ def test___init__(self): 'started' : False , 'trace_call_handler' : self.trace_call.trace_call_handler , 'trace_call_view_model' : self.trace_call.trace_call_view_model , - 'trace_call_print_traces': self.trace_call.trace_call_print_traces} + 'trace_call_print_traces': self.trace_call.trace_call_print_traces, + 'trace_on_thread__data' : {} } assert type(self.trace_call.trace_call_handler ) is Trace_Call__Handler assert type(self.trace_call.trace_call_view_model) is Trace_Call__View_Model diff --git a/tests/unit/helpers/trace/test_Trace_Call__Config.py b/tests/unit/helpers/trace/test_Trace_Call__Config.py index 5e648606..903f3390 100644 --- a/tests/unit/helpers/trace/test_Trace_Call__Config.py +++ b/tests/unit/helpers/trace/test_Trace_Call__Config.py @@ -39,6 +39,7 @@ def test__kwargs__(self): 'trace_capture_start_with' : [] , 'trace_capture_contains' : [] , 'trace_enabled' : True , + 'trace_ignore_contains' : [] , 'trace_ignore_start_with' : [] , 'trace_show_internals' : False , 'trace_up_to_depth' : 0 , diff --git a/tests/unit/helpers/trace/test_Trace_Files.py b/tests/unit/helpers/trace/test_Trace_Files.py index cb1a33eb..341ab958 100644 --- a/tests/unit/helpers/trace/test_Trace_Files.py +++ b/tests/unit/helpers/trace/test_Trace_Files.py @@ -21,7 +21,8 @@ def test___default_kwargs__(self): 'stack' : trace_files.stack , 'trace_call_handler' : trace_files.trace_call_handler , 'trace_call_print_traces' : trace_files.trace_call_print_traces, - 'trace_call_view_model' : trace_files.trace_call_view_model } + 'trace_call_view_model' : trace_files.trace_call_view_model , + 'trace_on_thread__data' : {} } trace_files.stack.add_node(DEFAULT_ROOT_NODE_NODE_TITLE) assert len(trace_files.stack) == 1