overture-schema-cli 0.1.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ """Docstring utilities for extracting documentation from modules and models."""
2
+
3
+ import inspect
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ def get_theme_module_docstring(theme_name: str) -> str | None:
9
+ """Get the docstring of a theme module if available."""
10
+ try:
11
+ # Try to import the theme module
12
+ module_name = f"overture.schema.{theme_name}"
13
+ module = __import__(module_name, fromlist=[""])
14
+ return inspect.getdoc(module)
15
+ except (ImportError, AttributeError):
16
+ return None
17
+
18
+
19
+ def get_model_docstring(model_class: type[BaseModel]) -> str | None:
20
+ """Get a clean, formatted docstring from a model class."""
21
+ return inspect.getdoc(model_class)
@@ -0,0 +1,587 @@
1
+ """Error formatting and grouping for validation errors."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+ from rich.console import Console
7
+
8
+ from .data_display import (
9
+ create_feature_display,
10
+ extract_feature_data,
11
+ select_context_fields,
12
+ )
13
+ from .type_analysis import (
14
+ StructuralTuple,
15
+ UnionMetadata,
16
+ extract_discriminator_path,
17
+ get_item_index,
18
+ get_or_create_structural_tuple,
19
+ infer_model_from_error,
20
+ )
21
+ from .types import ErrorLocation, ValidationErrorDict
22
+
23
+
24
+ def group_errors_by_discriminator(
25
+ errors: list[ValidationErrorDict],
26
+ metadata: UnionMetadata,
27
+ structural_cache: dict[ErrorLocation, StructuralTuple] | None = None,
28
+ ) -> dict[ErrorLocation, list[ValidationErrorDict]]:
29
+ """Group validation errors by their discriminator path.
30
+
31
+ Errors are grouped by which model variant they're associated with, as indicated
32
+ by their discriminator path. This allows the CLI to show only errors for the most
33
+ likely intended type, rather than overwhelming the user with errors from all
34
+ possible union variants.
35
+
36
+ Args
37
+ ----
38
+ errors: List of Pydantic validation error dicts
39
+ metadata: Pre-computed UnionMetadata from introspect_union()
40
+ structural_cache: Optional cache for structural tuple computation
41
+
42
+ Returns
43
+ -------
44
+ Dictionary mapping discriminator paths to lists of errors
45
+
46
+ Examples
47
+ --------
48
+ >>> from typing import Annotated, Union
49
+ >>> from pydantic import Field
50
+ >>> from overture.schema.buildings import Building, BuildingPart
51
+ >>> from overture.schema.cli.type_analysis import introspect_union
52
+ >>> BuildingUnion = Annotated[
53
+ ... Union[Building, BuildingPart],
54
+ ... Field(discriminator='type')
55
+ ... ]
56
+ >>> # Errors from validating two buildings with different issues
57
+ >>> errors = [
58
+ ... {'loc': (0, 'tagged-union[type]', 'building', 'height'), 'msg': 'Field required'},
59
+ ... {'loc': (0, 'tagged-union[type]', 'building', 'id'), 'msg': 'Field required'},
60
+ ... {'loc': (1, 'tagged-union[type]', 'building', 'geometry'), 'msg': 'Invalid geometry'},
61
+ ... ]
62
+ >>> metadata = introspect_union(BuildingUnion)
63
+ >>> groups = group_errors_by_discriminator(errors, metadata)
64
+ >>> list(groups.keys())
65
+ [('building',)]
66
+ >>> len(groups[('building',)])
67
+ 3
68
+
69
+ >>> # Errors from ambiguous data matching multiple types
70
+ >>> errors = [
71
+ ... {'loc': ('tagged-union[type]', 'building', 'height'), 'msg': 'Field required'},
72
+ ... {'loc': ('tagged-union[type]', 'building_part', 'building_id'), 'msg': 'Field required'},
73
+ ... ]
74
+ >>> groups = group_errors_by_discriminator(errors, metadata)
75
+ >>> len(groups) # Two groups - one for each potential type
76
+ 2
77
+ >>> ('building',) in groups
78
+ True
79
+ >>> ('building_part',) in groups
80
+ True
81
+ """
82
+ groups: dict[ErrorLocation, list[ValidationErrorDict]] = {}
83
+
84
+ for error in errors:
85
+ loc = error["loc"]
86
+ try:
87
+ structural = get_or_create_structural_tuple(loc, metadata, structural_cache)
88
+ disc_path = extract_discriminator_path(loc, structural)
89
+ if disc_path not in groups:
90
+ groups[disc_path] = []
91
+ groups[disc_path].append(error)
92
+ except (KeyError, TypeError, IndexError):
93
+ # Structural analysis can fail for unexpected error path formats
94
+ # (e.g., new Pydantic union markers). Group under empty path as fallback.
95
+ if () not in groups:
96
+ groups[()] = []
97
+ groups[()].append(error)
98
+
99
+ return groups
100
+
101
+
102
+ def analyze_collection_heterogeneity(
103
+ errors: list[ValidationErrorDict],
104
+ metadata: UnionMetadata,
105
+ structural_cache: dict[ErrorLocation, StructuralTuple] | None = None,
106
+ ) -> tuple[dict[int, type[BaseModel] | None], bool]:
107
+ """Analyze a collection to detect type heterogeneity.
108
+
109
+ Args
110
+ ----
111
+ errors: List of Pydantic validation error dicts
112
+ metadata: Pre-computed UnionMetadata from introspect_union()
113
+ structural_cache: Optional cache for structural tuple computation
114
+
115
+ Returns
116
+ -------
117
+ Tuple of (item_types, is_heterogeneous) where:
118
+ - item_types: Dict mapping item index to inferred model type
119
+ - is_heterogeneous: True if collection contains multiple model types
120
+ """
121
+ # Group errors by item index
122
+ item_errors: dict[int | None, list[ValidationErrorDict]] = {}
123
+ for error in errors:
124
+ item_idx = get_item_index(error["loc"])
125
+ if item_idx not in item_errors:
126
+ item_errors[item_idx] = []
127
+ item_errors[item_idx].append(error)
128
+
129
+ # Infer the most likely type for each item
130
+ # Use heuristic: type with FEWEST errors is most likely correct
131
+ item_types: dict[int, type[BaseModel] | None] = {}
132
+ for item_idx, item_error_list in item_errors.items():
133
+ if item_idx is None:
134
+ continue
135
+
136
+ # Group this item's errors by inferred type
137
+ errors_by_type: dict[type[BaseModel], list[ValidationErrorDict]] = {}
138
+ for error in item_error_list:
139
+ inferred_type = infer_model_from_error(error, metadata, structural_cache)
140
+ if inferred_type is not None:
141
+ if inferred_type not in errors_by_type:
142
+ errors_by_type[inferred_type] = []
143
+ errors_by_type[inferred_type].append(error)
144
+
145
+ if errors_by_type:
146
+ # Select the type with the FEWEST errors (smallest edit distance)
147
+ item_types[item_idx] = min(
148
+ errors_by_type.keys(), key=lambda t: len(errors_by_type[t])
149
+ )
150
+ else:
151
+ item_types[item_idx] = None
152
+
153
+ # Check if the collection is heterogeneous
154
+ unique_types = {t for t in item_types.values() if t is not None}
155
+ is_heterogeneous = len(unique_types) > 1
156
+
157
+ return item_types, is_heterogeneous
158
+
159
+
160
+ def _suppress_sibling_tag_noise(
161
+ error_groups: dict[ErrorLocation, list[ValidationErrorDict]],
162
+ ) -> dict[ErrorLocation, list[ValidationErrorDict]]:
163
+ """Drop `union_tag_not_found` noise for items that already matched a type.
164
+
165
+ When validating against a union that mixes a tagged-union with other
166
+ (also discriminated) members, an item that cleanly matches one branch still
167
+ produces a `union_tag_not_found` error from every *sibling* branch whose
168
+ discriminator it fails to satisfy. Those errors carry an empty discriminator
169
+ path `()`, forming a spurious group that can tie with the real match and
170
+ trigger a false "Ambiguous" warning.
171
+
172
+ This helper removes such noise for any list item that already has a concrete
173
+ (non-`union_tag_not_found`) error. Items whose *only* errors are
174
+ `union_tag_not_found` are left untouched, so genuinely undiscriminatable
175
+ input is still reported. Groups left empty after filtering are dropped.
176
+
177
+ Parameters
178
+ ----------
179
+ error_groups : dict[ErrorLocation, list[ValidationErrorDict]]
180
+ Mapping of discriminator paths to their error lists.
181
+
182
+ Returns
183
+ -------
184
+ dict[ErrorLocation, list[ValidationErrorDict]]
185
+ A new error-groups dict with sibling-branch tag noise removed.
186
+
187
+ Examples
188
+ --------
189
+ >>> # Item 0 matched 'building' but the sibling 'segment' branch emitted
190
+ >>> # a union_tag_not_found error, creating a spurious () group.
191
+ >>> error_groups = {
192
+ ... ('building',): [
193
+ ... {'loc': (0, 'tagged-union[type]', 'building', 'id'),
194
+ ... 'msg': 'Field required', 'type': 'missing'},
195
+ ... ],
196
+ ... (): [
197
+ ... {'loc': (0, 'tagged-union[subtype]'),
198
+ ... 'msg': 'Unable to extract tag', 'type': 'union_tag_not_found'},
199
+ ... ],
200
+ ... }
201
+ >>> cleaned = _suppress_sibling_tag_noise(error_groups)
202
+ >>> list(cleaned.keys()) # the () noise group is gone
203
+ [('building',)]
204
+
205
+ >>> # An item whose ONLY error is union_tag_not_found is preserved.
206
+ >>> error_groups = {
207
+ ... (): [
208
+ ... {'loc': (2, 'tagged-union[type]'),
209
+ ... 'msg': 'Unable to extract tag', 'type': 'union_tag_not_found'},
210
+ ... ],
211
+ ... }
212
+ >>> _suppress_sibling_tag_noise(error_groups) == error_groups
213
+ True
214
+ """
215
+ # Identify which items have at least one concrete (non tag-not-found) error.
216
+ items_with_concrete_error: set[int | None] = set()
217
+ for errors in error_groups.values():
218
+ for error in errors:
219
+ if error.get("type") != "union_tag_not_found":
220
+ items_with_concrete_error.add(get_item_index(error["loc"]))
221
+
222
+ cleaned: dict[ErrorLocation, list[ValidationErrorDict]] = {}
223
+ for disc_path, errors in error_groups.items():
224
+ kept = [
225
+ error
226
+ for error in errors
227
+ if not (
228
+ error.get("type") == "union_tag_not_found"
229
+ and get_item_index(error["loc"]) in items_with_concrete_error
230
+ )
231
+ ]
232
+ if kept:
233
+ cleaned[disc_path] = kept
234
+
235
+ return cleaned
236
+
237
+
238
+ def select_most_likely_errors(
239
+ error_groups: dict[ErrorLocation, list[ValidationErrorDict]],
240
+ metadata: UnionMetadata | None = None,
241
+ all_errors: list[ValidationErrorDict] | None = None,
242
+ structural_cache: dict[ErrorLocation, StructuralTuple] | None = None,
243
+ ) -> tuple[list[ValidationErrorDict], bool, bool, dict[int, type[BaseModel] | None]]:
244
+ """Select the error group(s) most likely to be the intended model.
245
+
246
+ Uses heuristic: the group with the fewest errors is most likely correct,
247
+ as it requires the fewest changes to make the data valid.
248
+
249
+ When multiple groups have the same minimum error count (a tie), returns
250
+ all tied groups to indicate ambiguity to the user.
251
+
252
+ Before tie detection, sibling-branch `union_tag_not_found` noise is
253
+ suppressed for any item that already matched a concrete type (see
254
+ `_suppress_sibling_tag_noise`), so a clean match is not falsely
255
+ reported as ambiguous. Items whose only errors are `union_tag_not_found`
256
+ are left intact.
257
+
258
+ For heterogeneous collections, returns ALL errors since different items
259
+ may have different intended types.
260
+
261
+ Args
262
+ ----
263
+ error_groups: Dictionary mapping discriminator paths to error lists
264
+ metadata: Optional UnionMetadata for heterogeneity detection
265
+ all_errors: Optional list of all errors for heterogeneity analysis
266
+ structural_cache: Optional cache for structural tuple computation
267
+
268
+ Returns
269
+ -------
270
+ Tuple of (errors_list, is_tied, is_heterogeneous, item_types) where:
271
+ - errors_list: List of errors to display
272
+ - is_tied: True if multiple groups had the same minimum error count
273
+ - is_heterogeneous: True if collection contains multiple model types
274
+ - item_types: Dict mapping item index to inferred model type
275
+ """
276
+ if not error_groups:
277
+ return [], False, False, {}
278
+
279
+ # Check for heterogeneous collections
280
+ is_heterogeneous = False
281
+ _item_types: dict[int, type[BaseModel] | None] = {}
282
+ if metadata is not None and all_errors is not None:
283
+ _item_types, is_heterogeneous = analyze_collection_heterogeneity(
284
+ all_errors, metadata, structural_cache
285
+ )
286
+
287
+ # For heterogeneous collections, return only errors matching each item's inferred type
288
+ if is_heterogeneous and all_errors is not None:
289
+ filtered_errors = []
290
+ for error in all_errors:
291
+ item_idx = get_item_index(error["loc"])
292
+ if item_idx is not None and item_idx in _item_types:
293
+ # Only include this error if it matches the inferred type for this item
294
+ if metadata is not None:
295
+ error_type = infer_model_from_error(
296
+ error, metadata, structural_cache
297
+ )
298
+ if error_type == _item_types[item_idx]:
299
+ filtered_errors.append(error)
300
+ else:
301
+ # Non-list errors or items without inferred type - include them
302
+ filtered_errors.append(error)
303
+
304
+ return filtered_errors, False, True, _item_types
305
+
306
+ # Suppress sibling-branch discriminator noise before tie detection.
307
+ error_groups = _suppress_sibling_tag_noise(error_groups)
308
+ if not error_groups:
309
+ return [], False, is_heterogeneous, _item_types
310
+
311
+ # Find the minimum error count
312
+ min_error_count = min(len(errors) for errors in error_groups.values())
313
+
314
+ # Get all groups with the minimum error count
315
+ tied_groups = [
316
+ errors for errors in error_groups.values() if len(errors) == min_error_count
317
+ ]
318
+
319
+ # Flatten all errors from tied groups
320
+ flattened_errors = [error for group in tied_groups for error in group]
321
+
322
+ # Indicate if there was a tie
323
+ is_tied = len(tied_groups) > 1
324
+
325
+ return flattened_errors, is_tied, is_heterogeneous, _item_types
326
+
327
+
328
+ def format_path(filtered_loc: list[str | int]) -> str:
329
+ """Convert filtered location path to dot-separated string.
330
+
331
+ Args
332
+ ----
333
+ filtered_loc: List of path components (strings and integers)
334
+
335
+ Returns
336
+ -------
337
+ Formatted path string (e.g., "properties.name" or "items[0].value")
338
+ """
339
+ path_str = ""
340
+ for i, part in enumerate(filtered_loc):
341
+ if isinstance(part, str):
342
+ if i > 0:
343
+ path_str += "."
344
+ path_str += part
345
+ else:
346
+ path_str += f"[{part}]"
347
+
348
+ if not path_str:
349
+ path_str = "(root)"
350
+
351
+ return path_str
352
+
353
+
354
+ def format_validation_errors_verbose(
355
+ errors: list[ValidationErrorDict],
356
+ console: Console,
357
+ metadata: UnionMetadata | None = None,
358
+ item_type: type[BaseModel] | None = None,
359
+ structural_cache: dict[ErrorLocation, StructuralTuple] | None = None,
360
+ original_data: dict[str, Any] | list[Any] | None = None,
361
+ item_index: int | None = None,
362
+ show_fields: list[str] | None = None,
363
+ ) -> bool:
364
+ """Format and display multiple validation errors for a single item in verbose mode.
365
+
366
+ Args
367
+ ----
368
+ errors: List of validation errors for this item
369
+ console: Rich Console instance for output
370
+ metadata: Pre-computed UnionMetadata from introspect_union() (optional)
371
+ item_type: The inferred type for this item
372
+ structural_cache: Optional cache for structural tuple computation
373
+ original_data: Original input data for extracting feature details
374
+ item_index: Index of item in collection
375
+ show_fields: List of field names to display alongside errors
376
+
377
+ Returns
378
+ -------
379
+ True if errors were displayed, False otherwise
380
+ """
381
+ if not errors or not original_data:
382
+ return False
383
+
384
+ # Extract item index from first error if not provided
385
+ if item_index is None:
386
+ item_index = get_item_index(errors[0]["loc"])
387
+
388
+ # Extract flattened feature data
389
+ feature = extract_feature_data(original_data, item_index)
390
+ if not feature:
391
+ return False
392
+
393
+ # Collect all error paths and messages
394
+ error_tuples: list[tuple[list[str | int], str]] = []
395
+ for error in errors:
396
+ loc = error["loc"]
397
+ msg = error["msg"]
398
+
399
+ # Extract actual error message from context if available
400
+ ctx = error.get("ctx", {})
401
+ if "error" in ctx:
402
+ msg = ctx["error"]
403
+
404
+ # Filter loc to remove union markers
405
+ if metadata is not None:
406
+ try:
407
+ structural = get_or_create_structural_tuple(
408
+ loc, metadata, structural_cache
409
+ )
410
+ filtered_loc = [
411
+ element
412
+ for element, struct_type in zip(loc, structural, strict=False)
413
+ if struct_type in ("list_index", "field")
414
+ ]
415
+ except (KeyError, TypeError, IndexError):
416
+ # Fall back to unfiltered path on unexpected error formats
417
+ filtered_loc = list(loc)
418
+ else:
419
+ filtered_loc = list(loc)
420
+
421
+ # Strip out the list index since we've already extracted that feature
422
+ error_path = list(filtered_loc)
423
+ if error_path and isinstance(error_path[0], int):
424
+ error_path = error_path[1:]
425
+
426
+ error_tuples.append((error_path, msg))
427
+
428
+ # Select context fields for all errors
429
+ # Merge context from all error paths
430
+ context_size = 1
431
+ selected_fields: dict[str, Any] = {}
432
+
433
+ for error_path, _ in error_tuples:
434
+ context = select_context_fields(
435
+ feature, error_path, context_size=context_size, pinned_fields=show_fields
436
+ )
437
+ selected_fields.update(context)
438
+
439
+ if selected_fields:
440
+ # Create and display panel with all errors
441
+ # Get type name from item_type if available
442
+ type_name = item_type.__name__ if item_type else None
443
+ panel = create_feature_display(
444
+ selected_fields,
445
+ error_tuples,
446
+ item_index=item_index,
447
+ item_type=type_name,
448
+ show_fields=show_fields,
449
+ feature=feature,
450
+ )
451
+ console.print(panel)
452
+ console.print()
453
+ return True
454
+ else:
455
+ # No fields to display (e.g., root-level discriminator errors)
456
+ return False
457
+
458
+
459
+ def format_validation_error(
460
+ error: ValidationErrorDict,
461
+ console: Console,
462
+ metadata: UnionMetadata | None = None,
463
+ show_model_hint: bool = False,
464
+ item_type: type[BaseModel] | None = None,
465
+ show_item_type: bool = False,
466
+ structural_cache: dict[ErrorLocation, StructuralTuple] | None = None,
467
+ original_data: dict[str, Any] | list[Any] | None = None,
468
+ show_feature_data: bool = False,
469
+ ) -> None:
470
+ """Format and print a single validation error.
471
+
472
+ Args
473
+ ----
474
+ error: Pydantic validation error dict
475
+ console: Rich Console instance for output
476
+ metadata: Pre-computed UnionMetadata from introspect_union() (optional)
477
+ show_model_hint: Show which model was selected for validation (first error only)
478
+ item_type: The inferred type for this item (always provided if available)
479
+ show_item_type: Whether to display the item type in the path (True for heterogeneous collections)
480
+ structural_cache: Optional cache for structural tuple computation
481
+ original_data: Original input data for extracting feature details (optional)
482
+ show_feature_data: Whether to display feature data with error (verbose mode)
483
+ """
484
+ loc = error["loc"]
485
+
486
+ # Determine which model was selected for this error
487
+ selected_model = None
488
+ if metadata is not None and show_model_hint:
489
+ try:
490
+ structural = get_or_create_structural_tuple(loc, metadata, structural_cache)
491
+
492
+ # Look for discriminator value in the location path
493
+ for element, struct_type in zip(loc, structural, strict=False):
494
+ if struct_type == "discriminator" and isinstance(element, str):
495
+ selected_model = metadata.discriminator_to_model.get(element)
496
+ break
497
+ elif struct_type == "model" and isinstance(element, str):
498
+ selected_model = metadata.model_name_to_model.get(element)
499
+ break
500
+ except (KeyError, TypeError, IndexError):
501
+ # Structural analysis can fail for unexpected error path formats
502
+ pass
503
+
504
+ # Filter out union markers from the path using structural analysis
505
+ if metadata is not None:
506
+ try:
507
+ structural = get_or_create_structural_tuple(loc, metadata, structural_cache)
508
+ # Filter out 'union', 'model', and 'discriminator' markers
509
+ # Keep only 'list_index' and 'field' elements for display
510
+ filtered_loc = [
511
+ element
512
+ for element, struct_type in zip(loc, structural, strict=False)
513
+ if struct_type in ("list_index", "field")
514
+ ]
515
+ except (KeyError, TypeError, IndexError):
516
+ # Fall back to original loc if structural analysis fails
517
+ filtered_loc = list(loc)
518
+ else:
519
+ filtered_loc = list(loc)
520
+
521
+ # Convert to dot-separated path
522
+ path_str = format_path(filtered_loc)
523
+
524
+ # Add item type annotation if requested (for heterogeneous collections)
525
+ if show_item_type and item_type is not None:
526
+ path_str = f"{path_str} [dim]({item_type.__name__})[/dim]"
527
+
528
+ # Show model hint if this is the first error in a group
529
+ if selected_model is not None:
530
+ model_name = selected_model.__name__
531
+ console.print(f" [dim]Probable type:[/dim] {model_name}", style="blue")
532
+ console.print()
533
+
534
+ # Format the error message
535
+ msg = error["msg"]
536
+ input_value = error.get("input")
537
+
538
+ ctx = error.get("ctx", {})
539
+ if "error" in ctx:
540
+ msg = ctx["error"]
541
+ input_value = None
542
+
543
+ # Skip error summary lines in verbose mode
544
+ if not show_feature_data:
545
+ console.print(f" {path_str}", style="cyan")
546
+ console.print(f" → {msg}", style="yellow")
547
+
548
+ # Show input value if present and not too large
549
+ if input_value is not None:
550
+ value_str = (
551
+ repr(input_value)
552
+ if not isinstance(input_value, str)
553
+ else f"'{input_value}'"
554
+ )
555
+ prefix = " → Got: "
556
+ if len(value_str) <= console.width - len(prefix):
557
+ console.print(f"{prefix}{value_str}", style="dim")
558
+ console.print()
559
+
560
+ # Show feature data in verbose mode
561
+ if show_feature_data and original_data is not None:
562
+ # Extract item index from error location
563
+ item_index = get_item_index(loc)
564
+
565
+ # Extract flattened feature data
566
+ feature = extract_feature_data(original_data, item_index)
567
+
568
+ if feature:
569
+ # Convert filtered_loc to error path format (list of str/int)
570
+ # Strip out the list index since we've already extracted that feature
571
+ error_path = list(filtered_loc)
572
+ if error_path and isinstance(error_path[0], int):
573
+ error_path = error_path[1:] # Remove list index
574
+
575
+ # Select context fields
576
+ selected_fields = select_context_fields(feature, error_path, context_size=1)
577
+
578
+ if selected_fields:
579
+ # Create and display panel (with item index for title)
580
+ panel = create_feature_display(
581
+ selected_fields,
582
+ [(error_path, msg)],
583
+ item_index=item_index,
584
+ )
585
+ # Print panel directly (it has its own borders, no extra indentation needed)
586
+ console.print(panel)
587
+ console.print()
@@ -0,0 +1,29 @@
1
+ """Rich console output utilities."""
2
+
3
+ from rich.console import Console
4
+ from rich.text import Text
5
+
6
+
7
+ def rewrap(text: str, console: Console, indent: int = 0, padding_right: int = 0) -> str:
8
+ """Unwrap and re-wrap text at console width with indentation.
9
+
10
+ Args
11
+ ----
12
+ text : str
13
+ The text to rewrap
14
+ console : Console
15
+ Rich Console instance for width and wrapping
16
+ indent : int
17
+ Number of spaces to indent (default: 0)
18
+ padding_right : int
19
+ Right padding to subtract from width (default: 0)
20
+
21
+ Returns
22
+ -------
23
+ str
24
+ Re-wrapped and indented text
25
+ """
26
+ unwrapped = " ".join(text.split())
27
+ text_obj = Text(unwrapped)
28
+ wrapped_lines = text_obj.wrap(console, console.width - indent - padding_right)
29
+ return "\n".join(f"{' ' * indent}{line}" for line in wrapped_lines)
File without changes