updating and refactoring codebase for improved performance and maintainability

This commit is contained in:
2026-05-03 17:29:32 -07:00
parent b7d3dc5f2d
commit 77cab1bd27
17 changed files with 590 additions and 294 deletions
+38 -11
View File
@@ -10,18 +10,45 @@ import re
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from SYS.logger import debug
# Prompt-toolkit lexer types are optional at import time; fall back to lightweight
# stubs if prompt_toolkit is not available so imports remain safe for testing.
try:
from prompt_toolkit.document import Document
from prompt_toolkit.lexers import Lexer as _PTK_Lexer
except Exception: # pragma: no cover - optional dependency
Document = object # type: ignore
# Fallback to a simple object when prompt_toolkit is not available
_PTK_Lexer = object # type: ignore
# Prompt-toolkit lexer types are optional and expensive (~300ms). Use find_spec
# to detect availability without importing, then lazy-load on first use.
import importlib.util as _importlib_util
_PTK_AVAILABLE: bool = _importlib_util.find_spec("prompt_toolkit") is not None
# Expose a stable name used by the rest of the module
Lexer = _PTK_Lexer
_ptk_Document: Any = None
_ptk_Lexer: Any = None
def _get_ptk_Document() -> Any:
global _ptk_Document
if _ptk_Document is None:
if _PTK_AVAILABLE:
from prompt_toolkit.document import Document as _Doc
_ptk_Document = _Doc
else:
_ptk_Document = object
return _ptk_Document
def _get_ptk_Lexer() -> Any:
global _ptk_Lexer
if _ptk_Lexer is None:
if _PTK_AVAILABLE:
from prompt_toolkit.lexers import Lexer as _Lex
_ptk_Lexer = _Lex
else:
_ptk_Lexer = object
return _ptk_Lexer
# Stable aliases: these resolve lazily the first time they are accessed.
# Code that does `isinstance(x, Document)` or `class Foo(Lexer)` at class-body
# time needs the real object, so we keep module-level names that proxy to the
# lazy getters via __getattr__ on the module. Callers that reference
# Document/Lexer INSIDE functions will always get the real class.
# Populate the module-level names now so that class bodies below can inherit.
Document: Any = _get_ptk_Document()
Lexer: Any = _get_ptk_Lexer()
# Pre-compiled regexes for the lexer (avoid recompiling on every call)
TOKEN_PATTERN = re.compile(
+23 -12
View File
@@ -1,13 +1,21 @@
"""Unified logging utility for automatic file and function name tracking."""
import sys
import inspect
import logging
import threading
from pathlib import Path
from typing import Any, Optional, Sequence
from SYS.rich_display import console_for
# SYS.rich_display deferred: rich (~100ms) loaded lazily on first log output.
_rich_display_mod: Any = None
def _console_for(file):
global _rich_display_mod
if _rich_display_mod is None:
import SYS.rich_display as _m
_rich_display_mod = _m
return _rich_display_mod.console_for(file)
logger = logging.getLogger(__name__)
@@ -73,7 +81,8 @@ def _is_rich_renderable(value: Any) -> bool:
def _caller_location(depth: int = 1) -> tuple[str, str]:
frame = inspect.currentframe()
import inspect as _inspect
frame = _inspect.currentframe()
current = frame
try:
for _ in range(max(0, int(depth))):
@@ -160,7 +169,7 @@ def debug(*args, **kwargs) -> None:
if len(args) == 1 and _is_rich_renderable(args[0]):
renderable = args[0]
console_for(target_file).print(renderable)
_console_for(target_file).print(renderable)
file_name, func_name = _caller_location(depth=1)
caller_name = f"{file_name}.{func_name}" if file_name and func_name else ""
_debug_db_log(caller_name=caller_name, message=f"<rich:{type(renderable).__name__}>")
@@ -200,7 +209,8 @@ def debug_inspect(
# Compute caller prefix (same as log()).
prefix = None
frame = inspect.currentframe()
import inspect as _inspect
frame = _inspect.currentframe()
if frame is not None and frame.f_back is not None:
caller_frame = frame.f_back
try:
@@ -215,7 +225,7 @@ def debug_inspect(
# Render.
from rich import inspect as rich_inspect
console = console_for(file)
console = _console_for(file)
# If the caller provides a title, treat it as authoritative.
# Only fall back to the automatic [file.func] prefix when no title is supplied.
effective_title = title
@@ -266,12 +276,13 @@ def log(*args, **kwargs) -> None:
add_prefix = _DEBUG_ENABLED
# Get the calling frame
frame = inspect.currentframe()
import inspect as _inspect
frame = _inspect.currentframe()
if frame is None:
file = kwargs.pop("file", sys.stdout)
sep = kwargs.pop("sep", " ")
end = kwargs.pop("end", "\n")
console_for(file).print(*args, sep=sep, end=end)
_console_for(file).print(*args, sep=sep, end=end)
return
caller_frame = frame.f_back
@@ -279,7 +290,7 @@ def log(*args, **kwargs) -> None:
file = kwargs.pop("file", sys.stdout)
sep = kwargs.pop("sep", " ")
end = kwargs.pop("end", "\n")
console_for(file).print(*args, sep=sep, end=end)
_console_for(file).print(*args, sep=sep, end=end)
return
try:
@@ -302,9 +313,9 @@ def log(*args, **kwargs) -> None:
end = kwargs.pop("end", "\n")
if add_prefix:
prefix = f"[{file_name}.{func_name}]"
console_for(file).print(prefix, *args, sep=sep, end=end)
_console_for(file).print(prefix, *args, sep=sep, end=end)
else:
console_for(file).print(*args, sep=sep, end=end)
_console_for(file).print(*args, sep=sep, end=end)
# Log to database if available
if _DB_LOGGER:
@@ -316,4 +327,4 @@ def log(*args, **kwargs) -> None:
pass
finally:
del frame
del caller_frame
del caller_frame
+80 -56
View File
@@ -1,5 +1,7 @@
"""Data models for the pipeline."""
from __future__ import annotations
import datetime
import hashlib
import inspect
@@ -16,23 +18,9 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Protocol, TextIO
from rich.console import Console
from rich.console import ConsoleOptions
from rich.console import Group
from rich.live import Live
from rich.panel import Panel
from rich.progress import (
BarColumn,
DownloadColumn,
Progress,
SpinnerColumn,
TaskID,
TaskProgressColumn,
TextColumn,
TimeRemainingColumn,
TimeElapsedColumn,
TransferSpeedColumn,
)
# rich imports are deferred to avoid ~100ms startup cost.
# Classes in this module that use rich types (ProgressBar, PipelineLiveProgress)
# import them lazily inside their method bodies at first use.
@dataclass(slots=True)
@@ -440,6 +428,42 @@ def _sanitise_for_json(
return repr(value)
def _import_rich() -> Any:
"""Lazy-load rich types used by ProgressBar and PipelineLiveProgress."""
import rich.console as _rc
import rich.live as _rl
import rich.panel as _rp
import rich.progress as _rprog
# Return a namespace-like object with the types we need
class _Rich:
Console = _rc.Console
ConsoleOptions = _rc.ConsoleOptions
Group = _rc.Group
Live = _rl.Live
Panel = _rp.Panel
Progress = _rprog.Progress
BarColumn = _rprog.BarColumn
DownloadColumn = _rprog.DownloadColumn
SpinnerColumn = _rprog.SpinnerColumn
TaskID = _rprog.TaskID
TaskProgressColumn = _rprog.TaskProgressColumn
TextColumn = _rprog.TextColumn
TimeRemainingColumn = _rprog.TimeRemainingColumn
TimeElapsedColumn = _rprog.TimeElapsedColumn
TransferSpeedColumn = _rprog.TransferSpeedColumn
return _Rich
_rich: Any = None # cached after first call
def _r() -> Any:
global _rich
if _rich is None:
_rich = _import_rich()
return _rich
class ProgressBar:
"""Rich progress helper for byte-based transfers.
@@ -521,16 +545,16 @@ class ProgressBar:
console = stderr_console()
except Exception:
logger.exception("Failed to acquire shared stderr Console from SYS.rich_display; using fallback Console")
console = Console(file=stream)
console = _r().Console(file=stream)
else:
console = Console(file=stream)
progress = Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
console = _r().Console(file=stream)
progress = _r().Progress(
_r().TextColumn("[progress.description]{task.description}"),
_r().BarColumn(),
_r().TaskProgressColumn(),
_r().DownloadColumn(),
_r().TransferSpeedColumn(),
_r().TimeRemainingColumn(),
console=console,
transient=True,
)
@@ -867,7 +891,7 @@ class PipelineLiveProgress:
overall = self._overall
if pipe_progress is None or transfers is None or overall is None:
# Not started (or stopped).
yield Panel("", title="Pipeline", expand=False)
yield _r().Panel("", title="Pipeline", expand=False)
return
body_parts: List[Any] = [pipe_progress]
@@ -875,8 +899,8 @@ class PipelineLiveProgress:
body_parts.append(status)
body_parts.append(transfers)
yield Group(
Panel(Group(*body_parts),
yield _r().Group(
_r().Panel(_r().Group(*body_parts),
title=self._title_text(),
expand=False),
overall
@@ -895,8 +919,8 @@ class PipelineLiveProgress:
if status is not None and self._status_tasks:
body_parts.append(status)
body_parts.append(transfers)
return Group(
Panel(Group(*body_parts),
return _r().Group(
_r().Panel(_r().Group(*body_parts),
title=self._title_text(),
expand=False),
overall
@@ -911,55 +935,55 @@ class PipelineLiveProgress:
# IMPORTANT: use the shared stderr Console instance so that any
# `stderr_console().print(...)` calls from inside cmdlets (e.g. preflight
# tables/prompts in download-file) cooperate with Rich Live rendering.
# If we create a separate Console(file=sys.stderr), output will fight for
# If we create a separate _r().Console(file=sys.stderr), output will fight for
# terminal cursor control and appear "blocked"/truncated.
from SYS.rich_display import stderr_console
self._console = stderr_console()
# Persistent per-pipe bars.
self._pipe_progress = Progress(
TextColumn("{task.description}"),
TimeElapsedColumn(),
BarColumn(),
TaskProgressColumn(),
self._pipe_progress = _r().Progress(
_r().TextColumn("{task.description}"),
_r().TimeElapsedColumn(),
_r().BarColumn(),
_r().TaskProgressColumn(),
console=self._console,
transient=False,
)
# Transient, per-item spinner for the currently-active subtask.
self._subtasks = Progress(
TextColumn(" "),
SpinnerColumn("simpleDots"),
TextColumn("{task.description}"),
self._subtasks = _r().Progress(
_r().TextColumn(" "),
_r().SpinnerColumn("simpleDots"),
_r().TextColumn("{task.description}"),
console=self._console,
transient=False,
)
# Status line below the pipe bars. Kept simple (no extra bar) so it
# doesn't visually offset the main pipe bar columns.
self._status = Progress(
TextColumn(" [bold]└─ {task.description}[/bold]"),
self._status = _r().Progress(
_r().TextColumn(" [bold]└─ {task.description}[/bold]"),
console=self._console,
transient=False,
)
# Byte-based transfer bars (download/upload) integrated into the Live view.
self._transfers = Progress(
TextColumn(" {task.description}"),
BarColumn(),
TaskProgressColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
self._transfers = _r().Progress(
_r().TextColumn(" {task.description}"),
_r().BarColumn(),
_r().TaskProgressColumn(),
_r().DownloadColumn(),
_r().TransferSpeedColumn(),
_r().TimeRemainingColumn(),
console=self._console,
transient=False,
)
self._overall = Progress(
TimeElapsedColumn(),
BarColumn(),
TextColumn("{task.description}"),
self._overall = _r().Progress(
_r().TimeElapsedColumn(),
_r().BarColumn(),
_r().TextColumn("{task.description}"),
console=self._console,
transient=False,
)
@@ -982,7 +1006,7 @@ class PipelineLiveProgress:
len(self._pipe_labels)),
)
self._live = Live(
self._live = _r().Live(
self,
console=self._console,
refresh_per_second=10,
@@ -1011,7 +1035,7 @@ class PipelineLiveProgress:
# Not initialized yet; start fresh.
self.start()
return
self._live = Live(
self._live = _r().Live(
self,
console=self._console,
refresh_per_second=10,
+49 -35
View File
@@ -13,11 +13,41 @@ from SYS.models import PipelineStageContext
from SYS.logger import log, debug, debug_panel, is_debug_enabled
import logging
logger = logging.getLogger(__name__)
from SYS.worker import WorkerManagerRegistry, WorkerStages
from SYS.cli_parsing import SelectionSyntax, SelectionFilterSyntax
from SYS.rich_display import stdout_console
from SYS.background_notifier import ensure_background_notifier
from SYS.result_table import Table
# SYS.worker deferred: ffmpeg+attr+rich (~260ms) loaded lazily on first pipeline run.
_worker_mod: Any = None
# SYS.cli_parsing deferred: prompt_toolkit (~300ms) loaded lazily on first selection.
_cli_parsing_mod: Any = None
def _worker():
global _worker_mod
if _worker_mod is None:
import SYS.worker as _m
_worker_mod = _m
return _worker_mod
def _cli_parsing():
global _cli_parsing_mod
if _cli_parsing_mod is None:
import SYS.cli_parsing as _m
_cli_parsing_mod = _m
return _cli_parsing_mod
# SYS.rich_display deferred: rich (~100ms) loaded lazily on first console output.
# SYS.background_notifier deferred: rich/attr/ffmpeg loaded lazily on first notifier use.
# SYS.result_table deferred: textual (~140ms) loaded lazily on first Table use.
_result_table_mod: Any = None
def _result_table():
global _result_table_mod
if _result_table_mod is None:
from SYS.result_table import Table as _T
_result_table_mod = _T
return _result_table_mod
import re
from datetime import datetime
from SYS.cmdlet_catalog import import_cmd_module
@@ -680,12 +710,14 @@ def set_last_result_table(
"""
state = _get_pipeline_state()
# Push current table to history before replacing
# Push current table to history before replacing.
# No .copy() needed: last_result_items is about to be replaced by reference,
# not mutated in place, so the old list reference is safe to keep in history.
if state.last_result_table is not None:
state.result_table_history.append(
(
state.last_result_table,
state.last_result_items.copy(),
state.last_result_items,
state.last_result_subject,
)
)
@@ -724,26 +756,6 @@ def set_last_result_table(
logger.exception("Failed to sort result_table and reorder items")
if (
result_table is not None
and hasattr(result_table, "sort_by_title")
and not getattr(result_table, "preserve_order", False)
):
try:
result_table.sort_by_title()
# Re-order items list to match the sorted table
if state.display_items and hasattr(result_table, "rows"):
sorted_items: List[Any] = []
for row in result_table.rows:
src_idx = getattr(row, "source_index", None)
if isinstance(src_idx, int) and 0 <= src_idx < len(state.display_items):
sorted_items.append(state.display_items[src_idx])
if len(sorted_items) == len(result_table.rows):
state.display_items = sorted_items
except Exception:
logger.exception("Failed to sort overlay result_table and reorder items")
def set_last_result_table_overlay(
result_table: Optional[Any],
items: Optional[List[Any]] = None,
@@ -1423,7 +1435,7 @@ class PipelineExecutor:
new_first_stage: List[str] = []
for token in first_stage_tokens:
if token.startswith("@"): # selection
selection = SelectionSyntax.parse(token)
selection = _cli_parsing().SelectionSyntax.parse(token)
if selection is not None:
first_stage_selection_indices = sorted(
[i - 1 for i in selection]
@@ -1848,6 +1860,7 @@ class PipelineExecutor:
}
if output_fn:
kwargs["output"] = output_fn
from SYS.background_notifier import ensure_background_notifier
ensure_background_notifier(worker_manager, **kwargs)
except Exception:
logger.exception("Failed to enable background notifier for session_worker_ids=%r", session_worker_ids)
@@ -2633,9 +2646,9 @@ class PipelineExecutor:
)
piped_result: Any = None
worker_manager = WorkerManagerRegistry.ensure(config)
worker_manager = _worker().WorkerManagerRegistry.ensure(config)
pipeline_text = " | ".join(" ".join(stage) for stage in stages)
pipeline_session = WorkerStages.begin_pipeline(
pipeline_session = _worker().WorkerStages.begin_pipeline(
worker_manager,
pipeline_text=pipeline_text,
config=config
@@ -2790,8 +2803,8 @@ class PipelineExecutor:
if cmd_name.startswith("@"): # selection stage
selection_token = raw_stage_name
selection = SelectionSyntax.parse(selection_token)
filter_spec = SelectionFilterSyntax.parse(selection_token)
selection = _cli_parsing().SelectionSyntax.parse(selection_token)
filter_spec = _cli_parsing().SelectionFilterSyntax.parse(selection_token)
is_select_all = selection_token.strip() == "@*"
if selection is None and filter_spec is None and not is_select_all:
print(f"Invalid selection: {selection_token}\n")
@@ -2849,7 +2862,7 @@ class PipelineExecutor:
elif filter_spec is not None:
selected_indices = [
i for i, item in enumerate(items_list)
if SelectionFilterSyntax.matches(item, filter_spec)
if _cli_parsing().SelectionFilterSyntax.matches(item, filter_spec)
]
else:
selected_indices = sorted(
@@ -2894,7 +2907,7 @@ class PipelineExecutor:
if base_table is not None and hasattr(base_table, "copy_with_title"):
new_table = base_table.copy_with_title(getattr(base_table, "title", "") or "Results")
else:
new_table = Table(getattr(base_table, "title", "") if base_table is not None else "Results")
new_table = _result_table()(getattr(base_table, "title", "") if base_table is not None else "Results")
try:
if base_table is not None and getattr(base_table, "table", None):
@@ -2918,6 +2931,7 @@ class PipelineExecutor:
logger.exception("Failed to set last_result_table_overlay for filter selection")
try:
from SYS.rich_display import stdout_console
stdout_console().print()
stdout_console().print(new_table)
except Exception:
@@ -3118,7 +3132,7 @@ class PipelineExecutor:
pipe_idx = pipe_index_by_stage.get(stage_index)
overlay_table: Any | None = None
session = WorkerStages.begin_stage(
session = _worker().WorkerStages.begin_stage(
worker_manager,
cmd_name=cmd_name,
stage_tokens=stage_tokens,
+71 -44
View File
@@ -18,20 +18,47 @@ from pathlib import Path
import json
import re
from rich.box import SIMPLE
from rich.console import Group
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table as RichTable
from rich.text import Text
# rich imports are deferred to avoid ~100ms startup cost.
# All rich types are only needed inside method bodies, so we lazily import on first use.
_rich_mod: Any = None
# Optional Textual imports - graceful fallback if not available
try:
from textual.widgets import Tree
TEXTUAL_AVAILABLE = True
except ImportError:
TEXTUAL_AVAILABLE = False
def _rich():
global _rich_mod
if _rich_mod is None:
import types as _types
_m = _types.SimpleNamespace()
from rich.box import SIMPLE as _SIMPLE
from rich.console import Group as _Group
from rich.panel import Panel as _Panel
from rich.prompt import Prompt as _Prompt
from rich.table import Table as _RichTable
from rich.text import Text as _Text
_m.SIMPLE = _SIMPLE
_m.Group = _Group
_m.Panel = _Panel
_m.Prompt = _Prompt
_m.RichTable = _RichTable
_m.Text = _Text
_rich_mod = _m
return _rich_mod
# Optional Textual imports - lazily loaded to avoid pulling in ~300ms of textual
# at import time when the TUI is not being used.
import importlib.util as _importlib_util
TEXTUAL_AVAILABLE: bool = _importlib_util.find_spec("textual") is not None
# Tree is populated lazily on first call to build_metadata_tree().
_textual_Tree: Any = None
def _get_textual_Tree() -> Any:
global _textual_Tree
if _textual_Tree is None:
from textual.widgets import Tree as _Tree
_textual_Tree = _Tree
return _textual_Tree
# Import ResultModel from the API for typing; avoid runtime redefinition issues
@@ -1591,11 +1618,11 @@ class Table:
panel_style = get_result_table_panel_style({"table_appearance": appearance_mode})
if not self.rows:
empty = Text("No results")
empty = _rich().Text("No results")
return (
Panel(
_rich().Panel(
empty,
title=Text(str(self.title), style=header_style),
title=_rich().Text(str(self.title), style=header_style),
border_style=border_style,
padding=(0, 0),
expand=False,
@@ -1613,7 +1640,7 @@ class Table:
seen.add(col.name)
col_names.append(col.name)
table = RichTable(
table = _rich().RichTable(
show_header=True,
header_style=header_style,
border_style=border_style,
@@ -1661,12 +1688,12 @@ class Table:
)
if self.title or self.header_lines:
header_bits = [Text(line) for line in (self.header_lines or [])]
renderable = Group(*header_bits, table) if header_bits else table
header_bits = [_rich().Text(line) for line in (self.header_lines or [])]
renderable = _rich().Group(*header_bits, table) if header_bits else table
return (
Panel(
_rich().Panel(
renderable,
title=Text(str(self.title), style=header_style),
title=_rich().Text(str(self.title), style=header_style),
border_style=border_style,
padding=(0, 0),
expand=False,
@@ -1777,7 +1804,7 @@ class Table:
from SYS.rich_display import stdout_console
stdout_console().print(self)
stdout_console().print(Panel(Text("Selection is disabled for this table.")))
stdout_console().print(_rich().Panel(_rich().Text("Selection is disabled for this table.")))
return None
# Display the table
@@ -1789,11 +1816,11 @@ class Table:
while True:
try:
if accept_args:
choice = Prompt.ask(
choice = _rich().Prompt.ask(
f"{prompt} (e.g., '5' or '2 -storage hydrus' or 'q' to quit)"
).strip()
else:
choice = Prompt.ask(
choice = _rich().Prompt.ask(
f"{prompt} (e.g., '5' or '3-5' or '1,3,5' or 'q' to quit)"
).strip()
@@ -1806,8 +1833,8 @@ class Table:
if result is not None:
return result
stdout_console().print(
Panel(
Text(
_rich().Panel(
_rich().Text(
"Invalid format. Use: selection (5 or 3-5 or 1,3,5) optionally followed by flags (e.g., '5 -storage hydrus')."
)
)
@@ -1818,8 +1845,8 @@ class Table:
if selected_indices is not None:
return selected_indices
stdout_console().print(
Panel(
Text(
_rich().Panel(
_rich().Text(
"Invalid format. Use: single (5), range (3-5), list (1,3,5), combined (1-3,7,9-11), or 'q' to quit."
)
)
@@ -1827,16 +1854,16 @@ class Table:
except (ValueError, EOFError):
if accept_args:
stdout_console().print(
Panel(
Text(
_rich().Panel(
_rich().Text(
"Invalid format. Use: selection (5 or 3-5 or 1,3,5) optionally followed by flags (e.g., '5 -storage hydrus')."
)
)
)
else:
stdout_console().print(
Panel(
Text(
_rich().Panel(
_rich().Text(
"Invalid format. Use: single (5), range (3-5), list (1,3,5), combined (1-3,7,9-11), or 'q' to quit."
)
)
@@ -2468,12 +2495,12 @@ class ItemDetailView(Table):
from rich.text import Text
# 1. Create Detail Grid (matching rich_display.py style)
details_table = RichTable.grid(expand=True, padding=(0, 2))
details_table = _rich().RichTable.grid(expand=True, padding=(0, 2))
details_table.add_column("Key", style="cyan", justify="right", width=15)
details_table.add_column("Value", style="white")
def _render_tag_text(tag_value: Any) -> Text:
tag_text = Text()
tag_text = _rich().Text()
tag_text.append("#", style="dim")
raw = str(tag_value or "")
@@ -2497,17 +2524,17 @@ class ItemDetailView(Table):
renderables.append(_render_tag_text(tag))
if freeform_tags:
freeform_grid = RichTable.grid(expand=True, padding=(0, 2))
freeform_grid = _rich().RichTable.grid(expand=True, padding=(0, 2))
for _ in range(3):
freeform_grid.add_column(ratio=1)
for row_values in _chunk_detail_tags(freeform_tags, 3):
cells = [_render_tag_text(tag) for tag in row_values]
while len(cells) < 3:
cells.append(Text(""))
cells.append(_rich().Text(""))
freeform_grid.add_row(*cells)
renderables.append(freeform_grid)
return Group(*renderables)
return _rich().Group(*renderables)
def _has_renderable_value(value: Any) -> bool:
if value is None:
@@ -2596,9 +2623,9 @@ class ItemDetailView(Table):
header_style = get_result_table_header_style()
border_style = get_result_table_border_style()
detail_title = str(self.detail_title or "Item Details").strip() or "Item Details"
elements.append(Panel(
elements.append(_rich().Panel(
details_table,
title=Text(detail_title, style=header_style),
title=_rich().Text(detail_title, style=header_style),
border_style=border_style,
padding=(1, 2)
))
@@ -2606,10 +2633,10 @@ class ItemDetailView(Table):
if results_renderable:
# If it's a Panel already (from super().to_rich() with title), use it directly
# but force the border style to the result-table standard for consistency
if isinstance(results_renderable, Panel):
if isinstance(results_renderable, _rich().Panel):
results_renderable.border_style = get_result_table_border_style()
if results_renderable.title:
results_renderable.title = Text(
results_renderable.title = _rich().Text(
str(results_renderable.title),
style=get_result_table_header_style(),
)
@@ -2622,13 +2649,13 @@ class ItemDetailView(Table):
display_title = original_title
# Add a bit of padding
results_group = Group(Text(""), results_renderable, Text(""))
results_group = _rich().Group(_rich().Text(""), results_renderable, _rich().Text(""))
elements.append(
Panel(
_rich().Panel(
results_group,
title=Text(str(display_title), style=get_result_table_header_style()),
title=_rich().Text(str(display_title), style=get_result_table_header_style()),
border_style=get_result_table_border_style(),
)
)
return Group(*elements)
return _rich().Group(*elements)
+30 -21
View File
@@ -13,42 +13,49 @@ import contextlib
import sys
from typing import Any, Iterator, TextIO, List, Dict, Optional, Tuple, cast
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from pathlib import Path
from SYS.utils import expand_path
# Configure Rich pretty-printing to avoid truncating long strings (hashes/paths).
# This is version-safe: older Rich versions may not support the max_* arguments.
try:
from rich.pretty import install as _pretty_install
# rich imports are deferred to first Console use to avoid ~100ms startup cost.
# They are loaded the first time any Console function is called.
try:
_pretty_install(max_string=100_000, max_length=100_000)
except TypeError:
_pretty_install()
except Exception:
from SYS.logger import logger
logger.exception("Failed to configure rich pretty-printing")
_STDOUT_CONSOLE = Console(file=sys.stdout)
_STDERR_CONSOLE = Console(file=sys.stderr)
_STDOUT_CONSOLE: Any = None
_STDERR_CONSOLE: Any = None
def stdout_console() -> Console:
def _ensure_consoles() -> None:
global _STDOUT_CONSOLE, _STDERR_CONSOLE
if _STDOUT_CONSOLE is None:
from rich.console import Console
from rich.pretty import install as _pretty_install
try:
_pretty_install(max_string=100_000, max_length=100_000)
except TypeError:
_pretty_install()
except Exception:
pass
_STDOUT_CONSOLE = Console(file=sys.stdout)
_STDERR_CONSOLE = Console(file=sys.stderr)
def stdout_console() -> Any:
_ensure_consoles()
return _STDOUT_CONSOLE
def stderr_console() -> Console:
def stderr_console() -> Any:
_ensure_consoles()
return _STDERR_CONSOLE
def console_for(file: TextIO | None) -> Console:
def console_for(file: Any) -> Any:
if file is None or file is sys.stdout:
_ensure_consoles()
return _STDOUT_CONSOLE
if file is sys.stderr:
_ensure_consoles()
return _STDERR_CONSOLE
from rich.console import Console
return Console(file=file)
@@ -57,7 +64,7 @@ def rprint(renderable: Any = "", *, file: TextIO | None = None) -> None:
@contextlib.contextmanager
def capture_rich_output(*, stdout: TextIO, stderr: TextIO) -> Iterator[None]:
def capture_rich_output(*, stdout: Any, stderr: Any) -> Iterator[None]:
"""Temporarily redirect Rich output helpers to provided streams.
Note: `stdout_console()` / `stderr_console()` use global Console instances,
@@ -65,9 +72,11 @@ def capture_rich_output(*, stdout: TextIO, stderr: TextIO) -> Iterator[None]:
"""
global _STDOUT_CONSOLE, _STDERR_CONSOLE
_ensure_consoles()
previous_stdout = _STDOUT_CONSOLE
previous_stderr = _STDERR_CONSOLE
from rich.console import Console
try:
_STDOUT_CONSOLE = Console(file=stdout)
_STDERR_CONSOLE = Console(file=stderr)
+19 -7
View File
@@ -4,13 +4,7 @@ from __future__ import annotations
import json
import hashlib
import subprocess
import shutil
try:
import ffmpeg # type: ignore
except Exception:
ffmpeg = None # type: ignore
import os
import base64
import logging
@@ -23,6 +17,22 @@ from urllib.parse import urlparse
from SYS.utils_constant import mime_maps
_ffmpeg_mod: Any = None
_ffmpeg_checked = False
def _get_ffmpeg():
"""Lazily return the ffmpeg module, or None if unavailable."""
global _ffmpeg_mod, _ffmpeg_checked
if not _ffmpeg_checked:
try:
import ffmpeg as _f # type: ignore
_ffmpeg_mod = _f
except Exception:
_ffmpeg_mod = None
_ffmpeg_checked = True
return _ffmpeg_mod
try:
import cbor2
except ImportError:
@@ -191,6 +201,7 @@ def ffprobe(file_path: str) -> dict:
probe = None
# Try python ffmpeg module first
ffmpeg = _get_ffmpeg()
if ffmpeg is not None:
try:
probe = ffmpeg.probe(file_path)
@@ -203,7 +214,8 @@ def ffprobe(file_path: str) -> dict:
ffprobe_cmd = shutil.which("ffprobe")
if ffprobe_cmd:
try:
proc = subprocess.run(
import subprocess as _subprocess
proc = _subprocess.run(
[
ffprobe_cmd,
"-v",