"""
Custom exceptions for graphizy package with enhanced error tracking
.. moduleauthor:: Charles Fosseprez
.. contact:: charles.fosseprez.pro@gmail.com
.. license:: GPL2 or later
.. copyright:: Copyright (C) 2025 Charles Fosseprez
"""
import logging
import traceback
import sys
from typing import Any, Dict, Optional
from datetime import datetime
[docs]
class GraphizyError(Exception):
"""Base exception for all graphizy errors with enhanced debugging"""
def __init__(self, message: str, context: Optional[Dict[str, Any]] = None,
original_exception: Optional[Exception] = None):
self.message = message
self.context = context or {}
self.original_exception = original_exception
self.timestamp = datetime.now()
# Auto-capture frame info
frame = sys._getframe(1)
self.function_name = frame.f_code.co_name
self.module_name = frame.f_globals.get('__name__', 'unknown')
self.line_number = frame.f_lineno
# Capture safe local variables
self._capture_safe_locals(frame.f_locals)
super().__init__(self._format_message())
def _capture_safe_locals(self, locals_dict: Dict[str, Any]) -> None:
"""Safely capture local variables for debugging"""
safe_locals = {}
for name, value in locals_dict.items():
if name.startswith('_'):
continue
try:
if hasattr(value, 'shape') and hasattr(value, 'dtype'):
safe_locals[name] = f"array(shape={value.shape}, dtype={value.dtype})"
elif isinstance(value, (int, float, str, bool)) and len(str(value)) < 100:
safe_locals[name] = str(value)
elif isinstance(value, (list, tuple)) and len(value) < 5:
safe_locals[name] = str(value)[:100]
else:
safe_locals[name] = f"{type(value).__name__}(...)"
except:
safe_locals[name] = "<unprintable>"
self.safe_locals = safe_locals
def _format_message(self) -> str:
"""Format comprehensive error message"""
msg_parts = [f"{self.message}"]
if hasattr(self, 'function_name'):
msg_parts.append(f" Location: {self.module_name}.{self.function_name}:{self.line_number}")
if self.context:
msg_parts.append(" Context:")
for key, value in self.context.items():
msg_parts.append(f" {key}: {value}")
if hasattr(self, 'safe_locals') and self.safe_locals:
msg_parts.append(" Local Variables:")
for key, value in self.safe_locals.items():
msg_parts.append(f" {key}: {value}")
if self.original_exception:
msg_parts.append(f" Caused by: {type(self.original_exception).__name__}: {self.original_exception}")
return "\n".join(msg_parts)
[docs]
def log_error(self) -> None:
"""Log this error with full context"""
logger = logging.getLogger(f'graphizy.{self.module_name}')
logger.error(f"GraphizyError: {self.message}")
logger.debug(f"Full context: {self._format_message()}")
[docs]
class InvalidDimensionError(GraphizyError):
"""Raised when invalid dimensions are provided"""
def __init__(self, message: str, dimensions: Any = None, **kwargs):
context = {}
if dimensions is not None:
context['dimensions'] = str(dimensions)
super().__init__(message, context, **kwargs)
[docs]
class InvalidDataShapeError(GraphizyError):
"""Raised when invalid data shape is provided"""
def __init__(self, message: str, data_shape: Any = None, **kwargs):
context = {}
if data_shape is not None:
context['data_shape'] = str(data_shape)
super().__init__(message, context, **kwargs)
[docs]
class InvalidAspectError(GraphizyError):
"""Raised when invalid aspect is provided"""
pass
[docs]
class InvalidPointArrayError(GraphizyError):
"""Raised when invalid point array is provided"""
def __init__(self, message: str, array_info: Dict[str, Any] = None, **kwargs):
context = array_info or {}
super().__init__(message, context, **kwargs)
[docs]
class SubdivisionError(GraphizyError):
"""Raised when OpenCV subdivision fails"""
def __init__(self, message: str, point_array=None, dimensions=None, **kwargs):
context = {}
# Add point array info
if point_array is not None:
try:
context['point_array_shape'] = str(point_array.shape)
context['point_array_dtype'] = str(point_array.dtype)
context['x_range'] = f"[{point_array[:, 0].min():.1f}, {point_array[:, 0].max():.1f}]"
context['y_range'] = f"[{point_array[:, 1].min():.1f}, {point_array[:, 1].max():.1f}]"
except:
context['point_array'] = "Could not extract array info"
# Add dimensions info
if dimensions is not None:
context['dimensions'] = str(dimensions)
super().__init__(message, context, **kwargs)
[docs]
class TriangulationError(GraphizyError):
"""Raised when Delaunay triangulation fails"""
pass
[docs]
class GraphCreationError(GraphizyError):
"""Raised when graph creation fails"""
pass
[docs]
class DrawingError(GraphizyError):
"""Raised when drawing operations fail"""
def __init__(self, message: str, image=None, point=None, **kwargs):
context = {}
if image is not None:
try:
context['image_shape'] = str(image.shape)
context['image_dtype'] = str(image.dtype)
except:
context['image'] = "Could not extract image info"
if point is not None:
context['point'] = str(point)
super().__init__(message, context, **kwargs)
[docs]
class PositionGenerationError(GraphizyError):
"""Raised when position generation fails"""
def __init__(self, message: str, size_x: int = None, size_y: int = None,
num_particles: int = None, **kwargs):
context = {}
if size_x is not None:
context['size_x'] = size_x
if size_y is not None:
context['size_y'] = size_y
if num_particles is not None:
context['num_particles'] = num_particles
super().__init__(message, context, **kwargs)
[docs]
class IgraphMethodError(GraphizyError):
"""Raised when igraph method execution fails"""
def __init__(self, message: str, method_name: str = None, graph_info: Dict[str, Any] = None, **kwargs):
context = {}
if method_name:
context['method'] = method_name
if graph_info:
context.update(graph_info)
super().__init__(message, context, **kwargs)
[docs]
class ConfigurationError(GraphizyError):
"""Raised when configuration is invalid"""
pass
[docs]
class DependencyError(GraphizyError):
"""Raised when required dependencies are missing"""
pass
# Utility functions for better error creation
[docs]
def handle_subdivision_bounds_error(point_array, dimensions, coordinate_type='x'):
"""Create detailed subdivision bounds error"""
width, height = dimensions
if coordinate_type == 'x':
bad_points = point_array[point_array[:, 0] >= width]
message = f"Found {len(bad_points)} points with X >= {width} (width limit)"
if len(bad_points) > 0:
message += f". Max X: {point_array[:, 0].max():.1f}"
else:
bad_points = point_array[point_array[:, 1] >= height]
message = f"Found {len(bad_points)} points with Y >= {height} (height limit)"
if len(bad_points) > 0:
message += f". Max Y: {point_array[:, 1].max():.1f}"
raise SubdivisionError(message, point_array, dimensions)
[docs]
def log_warning_with_context(message: str, **context):
"""Log warning with context without raising exception"""
logger = logging.getLogger('graphizy.warnings')
logger.warning(message)
if context:
for key, value in context.items():
logger.debug(f" {key}: {value}")