• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IBM / unitxt / 15851684646

24 Jun 2025 01:24PM UTC coverage: 80.21% (+0.01%) from 80.199%
15851684646

Pull #1838

github

web-flow
Merge 81822bb33 into 6f9c0666b
Pull Request #1838: Improved error messages

1705 of 2106 branches covered (80.96%)

Branch coverage included in aggregate %.

10588 of 13220 relevant lines covered (80.09%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.53
src/unitxt/error_utils.py
1
import re
1✔
2
from contextlib import contextmanager
1✔
3
from typing import Any, Optional
1✔
4

5
from .logging_utils import get_logger
1✔
6
from .settings_utils import get_constants
1✔
7

8
constants = get_constants()
1✔
9
logger = get_logger()
1✔
10

11

12
class Documentation:
1✔
13
    URL = "https://www.unitxt.ai/en/latest/"
1✔
14
    HUGGINGFACE_METRICS = "docs/adding_metric.html#adding-a-hugginface-metric"
1✔
15
    ADDING_TASK = "docs/adding_task.html"
1✔
16
    ADDING_TEMPLATE = "docs/adding_template.html"
1✔
17
    POST_PROCESSORS = "docs/adding_template.html#post-processors"
1✔
18
    MULTIPLE_METRICS_OUTPUTS = (
1✔
19
        "docs/adding_metric.html#metric-outputs-with-multiple-metrics"
20
    )
21
    EVALUATION = "docs/evaluating_datasets.html"
1✔
22
    BENCHMARKS = "docs/benchmark.html"
1✔
23
    DATA_CLASSIFICATION_POLICY = "docs/data_classification_policy.html"
1✔
24
    CATALOG = "docs/saving_and_loading_from_catalog.html"
1✔
25
    SETTINGS = "docs/settings.html"
1✔
26

27

28
def additional_info(path: str) -> str:
1✔
29
    return f"\nFor more information: see {Documentation.URL}/{path} \n"
1✔
30

31

32
class UnitxtError(Exception):
1✔
33
    """Exception raised for Unitxt errors.
34

35
    Args:
36
        message (str):
37
            explanation of the error
38
        additional_info_id (Optional[str]):
39
            relative path to additional documentation on web
40
            If set, should be one of the DOCUMENATION_* constants in the error_utils.py file.
41

42
    """
43

44
    def __init__(self, message: str, additional_info_id: Optional[str] = None):
1✔
45
        if additional_info_id is not None:
1✔
46
            message += additional_info(additional_info_id)
1✔
47
        super().__init__(message)
1✔
48

49

50
class UnitxtWarning:
1✔
51
    """Object to format warning message to log.
52

53
    Args:
54
        message (str):
55
            explanation of the warning
56
        additional_info_id (Optional[str]):
57
            relative path to additional documentation on web
58
            If set, should be one of the DOCUMENATION_* constants in the error_utils.py file.
59
    """
60

61
    def __init__(self, message: str, additional_info_id: Optional[str] = None):
1✔
62
        if additional_info_id is not None:
1✔
63
            message += additional_info(additional_info_id)
1✔
64
        logger.warning(message)
1✔
65

66

67
context_block_title = "Unitxt Error Context"
1✔
68

69

70
def _visible_length(text: str) -> int:
1✔
71
    ansi_escape = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]|\x1b\]8;;[^\x1b]*\x1b\\")
1✔
72
    return len(ansi_escape.sub("", text))
1✔
73

74

75
def _make_object_clickable(
1✔
76
    full_obj_name: str, display_name: Optional[str] = None
77
) -> str:
78
    if display_name is None:
1✔
79
        display_name = full_obj_name.split(".")[-1]
×
80

81
    if full_obj_name.startswith("unitxt."):
1✔
82
        parts = full_obj_name.split(".")
1✔
83
        if len(parts) >= 2:
1✔
84
            module_path = ".".join(parts[:2])
1✔
85
            doc_url = f"{Documentation.URL}{module_path}.html#{full_obj_name}"
1✔
86
            return f"\033]8;;{doc_url}\033\\{display_name}\033]8;;\033\\"
1✔
87

88
    return display_name
1✔
89

90

91
def _get_existing_context(error: Exception):
1✔
92
    """Extract existing context from an error if it exists."""
93
    if hasattr(error, "__error_context__"):
1✔
94
        existing = error.__error_context__
1✔
95
        return (
1✔
96
            existing["original_message"],
97
            existing["context_object"],
98
            existing["context"],
99
        )
100
    return str(error), None, {}
1✔
101

102

103
def _format_object_context(obj: Any) -> Optional[str]:
1✔
104
    """Format an object for display in error context."""
105
    if obj is None:
1✔
106
        return None
1✔
107

108
    if hasattr(obj, "__class__"):
1✔
109
        class_name = obj.__class__.__name__
1✔
110
        module_name = getattr(obj.__class__, "__module__", "")
1✔
111
    else:
112
        obj_type = type(obj)
×
113
        class_name = obj_type.__name__
×
114
        module_name = getattr(obj_type, "__module__", "")
×
115

116
    if module_name:
1✔
117
        full_name = f"{module_name}.{class_name}"
1✔
118
        clickable_object = _make_object_clickable(full_name, class_name)
1✔
119
        return f"Object: {clickable_object}"
1✔
120
    return f"Object: {class_name}"
×
121

122

123
def _make_clickable_link(url: str) -> str:
1✔
124
    """Create a clickable terminal link."""
125
    return f"\033]8;;{url}\033\\link\033]8;;\033\\"
1✔
126

127

128
def _format_help_context(help_docs) -> list:
1✔
129
    """Format help documentation into context parts."""
130
    parts = []
1✔
131

132
    if isinstance(help_docs, str):
1✔
133
        parts.append(f"Help: {_make_clickable_link(help_docs)}")
1✔
134
    elif isinstance(help_docs, dict):
×
135
        for label, url in help_docs.items():
×
136
            parts.append(f"Help ({label}): {_make_clickable_link(url)}")
×
137
    elif isinstance(help_docs, list):
×
138
        for item in help_docs:
×
139
            if isinstance(item, dict) and len(item) == 1:
×
140
                label, url = next(iter(item.items()))
×
141
                parts.append(f"Help ({label}): {_make_clickable_link(url)}")
×
142
            elif isinstance(item, str):
×
143
                parts.append(f"Help: {_make_clickable_link(item)}")
×
144

145
    return parts
1✔
146

147

148
def _build_context_parts(context_object: Any, context: dict) -> list:
1✔
149
    """Build the list of context information parts."""
150
    parts = []
1✔
151

152
    # Add object context
153
    obj_context = _format_object_context(context_object)
1✔
154
    if obj_context:
1✔
155
        parts.append(obj_context)
1✔
156

157
    # Add regular context items (skip 'help' as it's handled separately)
158
    for key, value in context.items():
1✔
159
        if key == "help":
1✔
160
            continue
1✔
161
        value = "unknown" if value is None else value
1✔
162
        parts.append(f"{key.replace('_', ' ').title()}: {value}")
1✔
163

164
    # Add help documentation
165
    if "help" in context:
1✔
166
        parts.extend(_format_help_context(context["help"]))
1✔
167
    else:
168
        parts.append(f"Help: {_make_clickable_link(Documentation.URL)}")
1✔
169

170
    return parts
1✔
171

172

173
def _create_context_box(parts: list) -> str:
1✔
174
    """Create a formatted box containing context information."""
175
    if not parts:
1✔
176
        return ""
×
177

178
    max_width = (
1✔
179
        max(len(context_block_title), max(_visible_length(part) for part in parts)) + 4
180
    )
181
    top_line = "┌" + "─" * max_width + "┐"
1✔
182
    bottom_line = "└" + "─" * max_width + "┘"
1✔
183

184
    lines = [top_line]
1✔
185
    lines.append(
1✔
186
        f"│ {context_block_title}{' ' * (max_width - len(context_block_title) - 1)}│"
187
    )
188

189
    for part in parts:
1✔
190
        padding = " " * (max_width - _visible_length(part) - 4)
1✔
191
        lines.append(f"│  - {part}{padding}│")
1✔
192

193
    lines.append(bottom_line)
1✔
194
    return "\n".join(lines)
1✔
195

196

197
def _store_context_attributes(
1✔
198
    error: Exception, context_object: Any, context: dict, original_message: str
199
):
200
    """Store context information in error attributes."""
201
    error.__error_context__ = {
1✔
202
        "context_object": context_object,
203
        "context": context,
204
        "original_message": original_message,
205
    }
206

207
    # Backward compatibility attributes
208
    try:
1✔
209
        error.original_error = type(error)(original_message)
1✔
210
    except (TypeError, ValueError):
1✔
211
        error.original_error = Exception(original_message)
1✔
212

213
    error.context_object = context_object
1✔
214
    error.context = context
1✔
215

216

217
def _add_context_to_exception(
1✔
218
    original_error: Exception, context_object: Any = None, **context
219
):
220
    """Add context information to an exception by modifying its message."""
221
    original_message, existing_object, existing_context = _get_existing_context(
1✔
222
        original_error
223
    )
224

225
    # Use existing context object if available, otherwise use the provided one
226
    final_context_object = existing_object or context_object
1✔
227

228
    # Merge contexts with version info at the top
229
    final_context = {
1✔
230
        "Unitxt": constants.version,
231
        "Python": constants.python,
232
        **existing_context,
233
        **context,
234
    }
235

236
    context_parts = _build_context_parts(final_context_object, final_context)
1✔
237
    context_message = _create_context_box(context_parts)
1✔
238

239
    _store_context_attributes(
1✔
240
        original_error, final_context_object, final_context, original_message
241
    )
242

243
    # Modify error message to include context
244
    if context_parts:
1✔
245
        error_class = type(original_error)
1✔
246
        backspaces = "\b" * (len(error_class.__name__) + 2)
1✔
247
        formatted_message = f"{backspaces}{context_message}\n\n{error_class.__name__}: {original_message}"
1✔
248
        original_error.args = (formatted_message,)
1✔
249
    else:
250
        original_error.args = (original_message,)
×
251

252

253
@contextmanager
1✔
254
def error_context(context_object: Any = None, **context):
1✔
255
    """Context manager that catches exceptions and re-raises them with additional context.
256

257
    Args:
258
        context_object: The object being processed (optional)
259
        **context: Any additional context to include in the error message.
260
                  You can provide any key-value pairs that help identify where the error occurred.
261

262
                  Special context keys:
263
                  - help: Documentation links to help with the error.
264
                    Can be a string (single URL), dict (label: URL), or list of URLs/dicts.
265

266
    Examples:
267
        # Basic usage with object and context
268
        with error_context(self, operation="validation", item_id=42):
269
            result = process_item(item)
270

271
        # With help documentation links
272
        with error_context(operation="schema_validation",
273
                          help="https://docs.example.com/schema"):
274
            validate_schema(data)
275

276
        # With multiple documentation links
277
        with error_context(operation="model_training",
278
                          help={
279
                              "Training Guide": "https://docs.example.com/training",
280
                              "Troubleshooting": "https://docs.example.com/troubleshoot"
281
                          }):
282
            train_model(data)
283

284
        # File processing context
285
        with error_context(input_file="data.json", line_number=156):
286
            data = parse_line(line)
287

288
        # Processing context
289
        with error_context(processor, step="preprocessing", batch_size=32):
290
            results = process_batch(batch)
291
    """
292
    try:
1✔
293
        yield
1✔
294
    except Exception as e:
1✔
295
        # Add context to the original exception by modifying its message
296
        _add_context_to_exception(e, context_object, **context)
1✔
297
        # Re-raise the exception with enhanced context
298
        raise
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc