import inspect import os import time import re import traceback class Debugger: COLORS = { "CYAN": "\033[96m", "BRIGHT_CYAN": "\033[1;96m", "DIM_CYAN": "\033[2;96m", "YELLOW": "\033[93m", "BRIGHT_YELLOW": "\033[1;93m", "DIM_YELLOW": "\033[2;93m", "MAGENTA": "\033[95m", "BRIGHT_MAGENTA": "\033[1;95m", "DIM_MAGENTA": "\033[2;95m", "RED": "\033[91m", "BRIGHT_RED": "\033[1;91m", "DIM_RED": "\033[2;91m", "GREEN": "\033[92m", "BRIGHT_GREEN": "\033[1;92m", "DIM_GREEN": "\033[2;92m", "BLUE": "\033[94m", "BRIGHT_BLUE": "\033[1;94m", "DIM_BLUE": "\033[2;94m", "RESET": "\033[0m", } LEVELS = { "INFO": 0, "SUCCESS": 0, "DEBUG": 1, "WARNING": 2, "ERROR": 3, "CRITICAL": 4, } COLOR_MAP = { "INFO": "BRIGHT_CYAN", "SUCCESS": "BRIGHT_GREEN", "DEBUG": "BRIGHT_YELLOW", "WARNING": "BRIGHT_MAGENTA", "ERROR": "BRIGHT_RED", "CRITICAL": "BRIGHT_RED", "VAR": "BRIGHT_BLUE", } DIM_COLOR_MAP = { "INFO": "DIM_CYAN", "SUCCESS": "DIM_GREEN", "DEBUG": "DIM_YELLOW", "WARNING": "DIM_MAGENTA", "ERROR": "DIM_RED", "CRITICAL": "DIM_RED", "VAR": "DIM_BLUE", } def __init__(self, enabled=True, level="INFO", show_time=True, show_location=True, column_widths=None): self.enabled = enabled self.level = self.LEVELS.get(level.upper(), 0) self.show_time = show_time self.show_location = show_location self.column_widths = column_widths or {"level": 10, "time": 20, "location": 20, "message": 50} # To ensure consistent spacing, we need to track actual visible text lengths # when ANSI color codes are used self.ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') self._indent_level = 0 # Added for recursive indentation def _get_location(self): """ Get the location of the calling code outside of the Debugger class. """ # Get the frame at the specified depth current_frame = inspect.currentframe() # Find the caller frame (outside of the Debugger class) caller_frame = current_frame for _ in range(10): # Limit to 10 frames to avoid infinite loop if caller_frame.f_back is None: break caller_frame = caller_frame.f_back # Get the code object for the frame code = caller_frame.f_code # If this frame is not from Debugger.py, we've found our caller if os.path.basename(code.co_filename) != "Debugger.py": break filename = os.path.basename(caller_frame.f_code.co_filename) lineno = caller_frame.f_lineno return f"{filename}:{lineno}" def _get_variable_name(self, var, frame_depth=2): """ Dynamically get the variable name by inspecting the source code. Args: var: The variable to find the name for frame_depth: How many frames to go back to find the caller Returns: str: The variable name or "unknown" if not found """ try: # Get the frame that called this function frame = inspect.currentframe() for _ in range(frame_depth): if frame.f_back is None: break frame = frame.f_back # Get the line of code that called this function context = inspect.getframeinfo(frame).code_context if not context: return "unknown" # Extract the line caller_line = context[0].strip() # Regular expression to match common variable inspection patterns # This captures variable names inside function calls like: # debug.var(my_variable), debug.var_dict(my_dict), etc. matches = re.findall(r'(?:var\w*)\s*\(\s*([^,)]+)', caller_line) if matches: return matches[0].strip() # Fall back to inspecting local variables for var_name, var_val in frame.f_locals.items(): if var_val is var: return var_name return "unknown" except Exception: return "unknown" finally: # Ensure we clean up the frame reference to avoid memory leaks del frame def _strip_ansi(self, text): """Remove ANSI escape sequences from text to get true visible length""" return self.ansi_escape.sub('', text) def _ljust_with_ansi(self, text, width): """Left justify text accounting for ANSI color codes""" visible_text = self._strip_ansi(text) padding = max(0, width - len(visible_text)) return text + ' ' * padding def _format(self, level, message): parts = [] # Get the dim color for the level dim_color = self.COLORS.get(self.DIM_COLOR_MAP.get(level, ""), "") # Get the bright color for the message bright_color = self.COLORS.get(self.COLOR_MAP.get(level, ""), "") reset = self.COLORS["RESET"] # Format level with consistent padding regardless of ANSI codes level_text = f"{dim_color}[{level}]{reset}" level_str = self._ljust_with_ansi(level_text, self.column_widths["level"]) # Format other components time_text = time.strftime("[%Y-%m-%d %H:%M:%S]") if self.show_time else "" time_str = self._ljust_with_ansi(time_text, self.column_widths["time"]) location_text = f"[{self._get_location()}]" if self.show_location else "" location_str = self._ljust_with_ansi(location_text, self.column_widths["location"]) # Add indentation for recursive calls indent_prefix = " " * self._indent_level message_str = f"{indent_prefix}{bright_color}{message}{reset}" # Combine into a table-like structure parts.append(f"{level_str} {time_str} {location_str} {message_str}") return "".join(parts) def log(self, message, level="INFO"): if not self.enabled or self.LEVELS.get(level, 0) < self.level: return print(self._format(level, message)) def info(self, message): self.log(message, "INFO") def debug(self, message): self.log(message, "DEBUG") def warning(self, message): self.log(message, "WARNING") def error(self, message): self.log(message, "ERROR") def critical(self, message): self.log(message, "CRITICAL") def success(self, message): self.log(message, "SUCCESS") # --- Variable inspection methods --- def var(self, *args, **kwargs): """ General-purpose variable inspector that accepts multiple variables. Args: *args: Variables to inspect **kwargs: Variables with custom names as keyword arguments Usage: debug.var(x) # Single variable, auto-detected name debug.var(x, y, z) # Multiple variables with auto-detected names debug.var(result=x) # Custom name 'result' for variable x debug.var(x, y, result=z) # Mix of auto-detected and custom names """ # Handle positional arguments (auto-detect names) # We need to adjust frame_depth here because _get_variable_name is called # from var, which is called from the user code. # This will need careful adjustment if the call stack changes significantly. current_frame = inspect.currentframe() outer_frames = [] f = current_frame while f: if os.path.basename(f.f_code.co_filename) != "Debugger.py": break outer_frames.append(f) f = f.f_back # If the call is directly from user code, we need to go back 2 frames # (var -> _inspect_var -> _get_variable_name) # If it's a wrapper like var_dict -> var, then it's different. # For a general `var` call, we want the frame that called `var`. # So we look for the first frame outside of Debugger.py in the stack. # This logic is already handled by _get_location, but for variable naming # we need to be more precise about the frame where the variable is defined. # The frame for var() is at depth 0, its caller is at depth 1 (user code) # The _get_variable_name itself looks back from its own call, so `frame_depth=2` is typically correct for `var` # if called directly like `debug.var(my_var)`. # For nested calls (e.g., from `_inspect_dict`), `_inspect_var` is already adjusting depth. # This is a tricky part for _get_variable_name. Let's assume it works well for the top-level call. # For internal recursive calls, the name is provided explicitly. for arg in args: # When calling from var, we need to look one more frame back # compared to when _get_variable_name is called directly from _inspect_var. # This is because _inspect_var is a helper function to var. # However, the current _get_variable_name implementation already tries to find the # non-Debugger.py frame, which is often what we want. # Let's keep `frame_depth=2` for the top-level `var` call as it's designed to skip `var` and `_get_variable_name` itself. var_name = self._get_variable_name(arg, frame_depth=2) self._inspect_var(arg, var_name) # Handle keyword arguments (custom names) for name, value in kwargs.items(): self._inspect_var(value, name) def _inspect_var(self, variable, var_name, is_nested=False): """ Helper method to inspect a single variable Args: variable: The variable to inspect var_name: Name to display for the variable is_nested: True if this call is from a recursive inspection (for indentation) """ var_type = type(variable).__name__ # Increment indent level for nested calls if is_nested: self._indent_level += 1 try: # For simple types, just show the value if isinstance(variable, (int, float, bool, str, type(None))): # Added NoneType value_str = repr(variable) self.log(f"{var_name} ({var_type}) = {value_str}", "VAR") # For more complex types, use type-specific methods elif isinstance(variable, dict): self._inspect_dict(variable, var_name, is_nested=True) elif isinstance(variable, (list, tuple)): self._inspect_list(variable, var_name, is_nested=True) elif isinstance(variable, set): self._inspect_set(variable, var_name, is_nested=True) else: # For other objects, show the representation value_str = repr(variable) self.log(f"{var_name} ({var_type}) = {value_str}", "VAR") finally: # Decrement indent level after nested calls if is_nested: self._indent_level -= 1 def var_dict(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: if isinstance(arg, dict): # When calling from var_dict directly, adjust frame_depth var_name = self._get_variable_name(arg, frame_depth=2) # Adjust depth for direct var_dict call self._inspect_dict(arg, var_name) else: var_type = type(arg).__name__ var_name = self._get_variable_name(arg, frame_depth=2) self.log(f"{var_name} is not a dictionary (got {var_type})", "WARNING") # Handle keyword arguments (custom names) for name, value in kwargs.items(): if isinstance(value, dict): self._inspect_dict(value, name) else: var_type = type(value).__name__ self.log(f"{name} is not a dictionary (got {var_type})", "WARNING") def _inspect_dict(self, dictionary, var_name, is_nested=False): """ Helper method to inspect a single dictionary Args: dictionary: The dictionary to inspect var_name: Name to display for the dictionary is_nested: True if this call is from a recursive inspection """ if not is_nested: # Only increment on the initial call to this method self._indent_level += 1 self.log(f"{var_name} (dict) with {len(dictionary)} items:", "VAR") if not dictionary: self.log("{}", "VAR") if not is_nested: # Only decrement on the initial call exit self._indent_level -= 1 return self.log("{", "VAR") for key, value in dictionary.items(): key_repr = repr(key) # Recursively inspect the value # The name for the nested item will be its key self._inspect_var(value, key_repr, is_nested=True) self.log("}", "VAR") if not is_nested: # Only decrement on the initial call exit self._indent_level -= 1 def var_list(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: if isinstance(arg, (list, tuple)): var_name = self._get_variable_name(arg, frame_depth=2) # Adjust depth for direct var_list call self._inspect_list(arg, var_name) else: var_type = type(arg).__name__ var_name = self._get_variable_name(arg, frame_depth=2) self.log(f"{var_name} is not a list or tuple (got {var_type})", "WARNING") # Handle keyword arguments (custom names) for name, value in kwargs.items(): if isinstance(value, (list, tuple)): self._inspect_list(value, name) else: var_type = type(value).__name__ self.log(f"{name} is not a list or tuple (got {var_type})", "WARNING") def _inspect_list(self, lst, var_name, is_nested=False): """ Helper method to inspect a single list or tuple Args: lst: The list or tuple to inspect var_name: Name to display for the list or tuple is_nested: True if this call is from a recursive inspection """ container_type = "list" if isinstance(lst, list) else "tuple" if not is_nested: # Only increment on the initial call to this method self._indent_level += 1 self.log(f"{var_name} ({container_type}) with {len(lst)} items:", "VAR") if not lst: self.log(f"{'[]' if container_type == 'list' else '()'} ", "VAR") # Added space for consistency if not is_nested: # Only decrement on the initial call exit self._indent_level -= 1 return opening = "[" if container_type == "list" else "(" closing = "]" if container_type == "list" else ")" self.log(opening, "VAR") for i, item in enumerate(lst): # Recursively inspect the item self._inspect_var(item, f"{var_name}[{i}]", is_nested=True) # Pass index as part of name self.log(closing, "VAR") if not is_nested: # Only decrement on the initial call exit self._indent_level -= 1 def var_set(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: if isinstance(arg, set): var_name = self._get_variable_name(arg, frame_depth=2) # Adjust depth for direct var_set call self._inspect_set(arg, var_name) else: var_type = type(arg).__name__ var_name = self._get_variable_name(arg, frame_depth=2) self.log(f"{var_name} is not a set (got {var_type})", "WARNING") # Handle keyword arguments (custom names) for name, value in kwargs.items(): if isinstance(value, set): self._inspect_set(value, name) else: var_type = type(value).__name__ self.log(f"{name} is not a set (got {var_type})", "WARNING") def _inspect_set(self, s, var_name, is_nested=False): """ Helper method to inspect a single set Args: s: The set to inspect var_name: Name to display for the set is_nested: True if this call is from a recursive inspection """ if not is_nested: # Only increment on the initial call to this method self._indent_level += 1 self.log(f"{var_name} (set) with {len(s)} items:", "VAR") if not s: self.log("{}", "VAR") if not is_nested: # Only decrement on the initial call exit self._indent_level -= 1 return self.log("{", "VAR") for i, item in enumerate(s): # Recursively inspect the item, using a generic name like "item_X" self._inspect_var(item, f"item_{i}", is_nested=True) self.log("}", "VAR") if not is_nested: # Only decrement on the initial call exit self._indent_level -= 1 def var_bool(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: if isinstance(arg, bool): var_name = self._get_variable_name(arg, frame_depth=2) self._inspect_bool(arg, var_name) else: var_type = type(arg).__name__ var_name = self._get_variable_name(arg, frame_depth=2) self.log(f"{var_name} is not a boolean (got {var_type})", "WARNING") # Handle keyword arguments (custom names) for name, value in kwargs.items(): if isinstance(value, bool): self._inspect_bool(value, name) else: var_type = type(value).__name__ self.log(f"{name} is not a boolean (got {var_type})", "WARNING") def _inspect_bool(self, boolean, var_name): value_str = str(boolean) self.log(f"{var_name} (bool) = {value_str}", "VAR") def var_num(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: if isinstance(arg, (int, float)): var_name = self._get_variable_name(arg, frame_depth=2) self._inspect_num(arg, var_name) else: var_type = type(arg).__name__ var_name = self._get_variable_name(arg, frame_depth=2) self.log(f"{var_name} is not a number (got {var_type})", "WARNING") # Handle keyword arguments (custom names) for name, value in kwargs.items(): if isinstance(value, (int, float)): self._inspect_num(value, name) else: var_type = type(value).__name__ self.log(f"{name} is not a number (got {var_type})", "WARNING") def _inspect_num(self, number, var_name): num_type = "int" if isinstance(number, int) else "float" self.log(f"{var_name} ({num_type}) = {number}", "VAR") def var_str(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: if isinstance(arg, str): var_name = self._get_variable_name(arg, frame_depth=2) self._inspect_str(arg, var_name) else: var_type = type(arg).__name__ var_name = self._get_variable_name(arg, frame_depth=2) self.log(f"{var_name} is not a string (got {var_type})", "WARNING") # Handle keyword arguments (custom names) for name, value in kwargs.items(): if isinstance(value, str): self._inspect_str(value, name) else: var_type = type(value).__name__ self.log(f"{name} is not a string (got {var_type})", "WARNING") def _inspect_str(self, string, var_name): self.log(f"{var_name} (str) length={len(string)}", "VAR") self.log(f'"{string}"', "VAR") def var_type(self, *args, **kwargs): # Handle positional arguments (auto-detect names) for arg in args: var_name = self._get_variable_name(arg, frame_depth=2) self._inspect_type(arg, var_name) # Handle keyword arguments (custom names) for name, value in kwargs.items(): self._inspect_type(value, name) def _inspect_type(self, obj, var_name): mro = type(obj).__mro__ type_names = [t.__name__ for t in mro] self.log(f"{var_name} type hierarchy:", "VAR") for i, name in enumerate(type_names): indent = " " * i self.log(f"{indent}└─ {name}", "VAR")