Spaces:
Running
Running
| 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") |