Source code for interpretune.utils.exceptions
import os
import json
import traceback
import logging
import inspect
import tempfile
from pathlib import Path
from datetime import datetime
from typing import Any, Sequence
log = logging.getLogger(__name__)
IT_ANALYSIS_DUMP_DIR_NAME = "it_analysis_debug_dump"
[docs]
class MisconfigurationException(Exception):
"""Exception used to inform users of misuse with interpretune."""
def handle_exception_with_debug_dump(
e: Exception,
context_data: dict[str, Any] | Sequence[Any],
operation_name: str = "operation",
debug_dir_override: str | Path | None = None,
) -> None:
"""Handle an exception by creating a detailed debug dump file and re-raising the exception.
Args:
e: The caught exception
context_data: Either a dictionary with context-specific debug information or
a sequence of variables to be introspected and serialized
operation_name: Description of the operation that failed (for error messages)
debug_dir_override: Optional custom path for debug directory
Raises:
The original exception after creating the debug dump
"""
# Create debug dump file
debug_dir = (
Path(debug_dir_override) if debug_dir_override else Path(tempfile.gettempdir()) / IT_ANALYSIS_DUMP_DIR_NAME
)
os.makedirs(debug_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
dump_file = debug_dir / f"{operation_name}_error_{timestamp}.json"
# Add exception information to debug data
debug_info: dict[str, Any] = {
"error": str(e),
"traceback": traceback.format_exc(),
}
# Process context_data based on its type
if isinstance(context_data, dict):
# If it's already a dictionary, use it directly
debug_info.update(context_data)
elif isinstance(context_data, (list, tuple)):
# For sequences, introspect each item
context_dict = {}
# Try to get variable names from the caller's frame
frame = inspect.currentframe()
try:
if frame and frame.f_back:
# Get the line of code that called this function
frame_info = inspect.getframeinfo(frame.f_back)
if frame_info.code_context:
call_line = frame_info.code_context[0].strip()
# Try to extract the argument name for context_data
if "context_data=" in call_line:
# Get the part after context_data=
context_part = call_line.split("context_data=")[1]
# Check if it's a tuple
if context_part.strip().startswith("("):
# Find the complete tuple by tracking parentheses
open_count = 0
tuple_str = ""
for char in context_part:
tuple_str += char
if char == "(":
open_count += 1
elif char == ")":
open_count -= 1
if open_count == 0:
break
# Extract variable names from the tuple
var_names = [name.strip() for name in tuple_str.strip("()").split(",")]
if len(var_names) == len(context_data):
# We have matching variable names for each item in the tuple
for i, name in enumerate(var_names):
if i < len(context_data):
context_dict[f"var_{i}_{name}"] = _introspect_variable(context_data[i])
# Skip the generic processing below
debug_info.update(context_dict)
finally:
del frame # Avoid reference cycles
# Generic sequence processing if we couldn't get variable names
for i, item in enumerate(context_data):
context_dict[f"var_{i}"] = _introspect_variable(item)
debug_info.update(context_dict)
else:
# Handle single item
debug_info["context"] = _introspect_variable(context_data)
# Save debug info to file
with open(dump_file, "w") as f:
json.dump(debug_info, f, indent=2, default=_json_serializer)
log.error(f"{operation_name.capitalize()} failed: {e}. Debug info saved to {dump_file}")
raise e
def _introspect_variable(var: Any) -> dict[str, Any]:
"""Introspect a variable to create a detailed representation for debugging.
Args:
var: The variable to introspect
Returns:
A dictionary with detailed information about the variable
"""
result: dict[str, Any] = {
"type": str(type(var).__name__),
}
# Handle different types appropriately
if var is None:
result["value"] = None
elif isinstance(var, (str, int, float, bool)):
# Simple scalar types
result["value"] = var
elif isinstance(var, (list, tuple)):
# For sequences, include length and sample of items
result["length"] = len(var)
result["sample"] = var[:10] if len(var) > 10 else var
elif isinstance(var, dict):
# For dictionaries, include keys and sample of values
result["keys"] = list(var.keys())
if len(var) <= 10:
result["content"] = var
else:
# Take a sample of items if dict is large
sample = {k: var[k] for k in list(var.keys())[:10]}
result["sample"] = sample
elif hasattr(var, "__dict__"):
# For objects with attributes
result["class"] = var.__class__.__name__
result["module"] = var.__class__.__module__
# Get public attributes
attrs = {}
for attr_name in dir(var):
if not attr_name.startswith("_"):
try:
if not callable(getattr(var, attr_name, None)):
attr_value = getattr(var, attr_name)
attrs[attr_name] = str(attr_value)
except Exception:
attrs[attr_name] = "<error getting attribute>"
result["attributes"] = attrs
result["repr"] = repr(var)
else:
# Fallback for other types
result["repr"] = repr(var)
return result
def _json_serializer(obj):
"""Custom JSON serializer for objects not serializable by default json code."""
try:
return str(obj)
except Exception:
return "<non-serializable>"