Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_output.py273 linesDownload Raw Back to cli
1# Copyright 2026 The HuggingFace Team. All rights reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14"""Output framework for the `hf` CLI."""15 16import dataclasses17import datetime18import json19import re20import sys21from collections.abc import Sequence22from enum import Enum23from typing import Any24 25import typer26 27from huggingface_hub.errors import ConfirmationError28from huggingface_hub.utils import ANSI, StatusLine, disable_progress_bars, is_agent, tabulate29 30 31# TODO: remove OutputFormat in _cli_utils.py once all commands are migrated to OutputFormatWithAuto.32class OutputFormatWithAuto(str, Enum):33    """Output format for CLI commands with auto detection of agent/human mode."""34 35    agent = "agent"36    auto = "auto"37    human = "human"38    json = "json"39    quiet = "quiet"40 41 42class Output:43    """Output sink for the `hf` CLI.44 45    Mode is resolved once at init time based on `is_agent()` auto-detection46    and can be overridden per-command via `set_mode()`.47    """48 49    mode: OutputFormatWithAuto50 51    def __init__(self) -> None:52        self.set_mode()53 54    def set_mode(self, mode: OutputFormatWithAuto = OutputFormatWithAuto.auto) -> None:55        """Override the output mode (called once at startup and again per '--format' flag)."""56        if mode == OutputFormatWithAuto.auto:57            mode = OutputFormatWithAuto.agent if is_agent() else OutputFormatWithAuto.human58        self.mode = mode59        if mode != OutputFormatWithAuto.human:60            disable_progress_bars()61 62    def is_quiet(self) -> bool:63        return self.mode == OutputFormatWithAuto.quiet64 65    def text(self, msg: str | None = None, *, human: str | None = None, agent: str | None = None) -> None:66        """Print a free-form text message to stdout."""67        if msg is not None:68            if human is not None or agent is not None:69                raise ValueError("Cannot mix 'msg' with 'human'/'agent'.")70            human = msg71            agent = _strip_ansi(msg)72 73        match self.mode:74            case OutputFormatWithAuto.human:75                if human is not None:76                    print(human)77            case OutputFormatWithAuto.agent:78                if agent is not None:79                    print(agent)80            # json/quiet: no-op81 82    def table(83        self,84        items: Sequence[dict[str, Any]],85        *,86        headers: list[str] | None = None,87        id_key: str | None = None,88        alignments: dict[str, str] | None = None,89    ) -> None:90        """Print tabular data to stdout.91 92        Args:93            items: List of dicts. Headers are auto-detected from keys if not provided.94            headers: Explicit column names. If None, derived from dict keys (all-None columns filtered).95            id_key: Key to print in quiet mode. If None, uses the first header.96            alignments: Optional mapping of header name to "left" or "right". Defaults to "left".97        """98        if not items:99            match self.mode:100                case OutputFormatWithAuto.agent | OutputFormatWithAuto.human:101                    print("No results found.")102                case OutputFormatWithAuto.json:103                    print("[]")104            return105 106        if headers is None:107            all_columns = list(items[0].keys())108            headers = [col for col in all_columns if any(item.get(col) is not None for item in items)]109        rows = [[item.get(h) for h in headers] for item in items]110 111        match self.mode:112            case OutputFormatWithAuto.human:  # padded table, truncated cells, SCREAMING_SNAKE headers113                formatted_rows: list[list[str | int]] = [[_format_table_cell_human(v) for v in row] for row in rows]114                screaming_headers = [_to_header(h) for h in headers]115                screaming_alignments = {_to_header(k): v for k, v in (alignments or {}).items()}116                print(tabulate(formatted_rows, headers=screaming_headers, alignments=screaming_alignments))117            case OutputFormatWithAuto.agent:  # TSV, no truncation, full timestamps118                print("\t".join(headers))119                for row in rows:120                    print("\t".join(_format_table_cell_agent(v) for v in row))121            case OutputFormatWithAuto.json:  # compact JSON array122                print(json.dumps(list(items), default=str))123            case OutputFormatWithAuto.quiet:  # id_key column (or first column), one per line124                quiet_key = id_key or headers[0]125                for item in items:126                    print(item.get(quiet_key, ""))127 128    def dict(self, data: Any, *, id_key: str | None = None) -> None:129        """Print structured data as JSON in all modes (indented for human, compact otherwise).130 131        Accepts a dict or a dataclass.132        """133        if dataclasses.is_dataclass(data) and not isinstance(data, type):134            data = _dataclass_to_dict(data)135        if self.mode == OutputFormatWithAuto.quiet and id_key is not None:136            print(data.get(id_key, ""))137            return138        indent = 2 if self.mode == OutputFormatWithAuto.human else None139        print(json.dumps(data, indent=indent, default=str))140 141    def result(self, message: str, **data: Any) -> None:142        """Print a success summary to stdout."""143        match self.mode:144            case OutputFormatWithAuto.human:  # ✓ message + key: value lines145                parts = [ANSI.green(f"✓ {message}")]146                for k, v in data.items():147                    if v is not None:148                        parts.append(f"  {k}: {v}")149                print("\n".join(parts))150            case OutputFormatWithAuto.agent:  # key=val pairs, space-separated151                parts = [f"{k}={v}" for k, v in data.items() if v is not None]152                print(" ".join(parts) if parts else message)153            case OutputFormatWithAuto.json:  # json.dumps(data), message ignored154                print(json.dumps(data, default=str) if data else "")155            case OutputFormatWithAuto.quiet:  # first value only156                values = list(data.values())157                if values:158                    print(values[0])159 160    def confirm(self, message: str, *, default: bool = False, yes: bool = False) -> None:161        """162        Ask for confirmation. Raises `ConfirmationError` in non-human modes.163        """164        if yes:165            return166        if self.mode != OutputFormatWithAuto.human:167            raise ConfirmationError(f"{message} Use --yes to skip confirmation.")168        typer.confirm(message, default=default, abort=True)169 170    def status(self, message: str | None = None) -> StatusLine:171        """Return a status line that emits only in human mode (no-op otherwise)."""172        status = StatusLine(enabled=self.mode == OutputFormatWithAuto.human)173        if message is not None:174            status.update(message)175        return status176 177    def warning(self, message: str) -> None:178        """Print a non-fatal warning to stderr (all modes)."""179        if self.mode == OutputFormatWithAuto.human:180            print(ANSI.yellow(f"Warning: {message}"), file=sys.stderr)181        else:182            print(f"Warning: {message}", file=sys.stderr)183 184    def error(self, message: str) -> None:185        """Print an error to stderr (all modes)."""186        if self.mode == OutputFormatWithAuto.human:187            print(ANSI.red(f"Error: {message}"), file=sys.stderr)188        else:189            print(f"Error: {message}", file=sys.stderr)190 191    def hint(self, message: str) -> None:192        """Print a helpful hint to stderr (human: gray, agent/json: plain text)."""193        if self.mode == OutputFormatWithAuto.human:194            print(ANSI.gray(f"Hint: {message}"), file=sys.stderr)195        else:196            print(f"Hint: {message}", file=sys.stderr)197 198 199# HELPERS200 201 202def _serialize_value(v: object) -> object:203    """Recursively serialize a value to be JSON-compatible."""204    if isinstance(v, datetime.datetime):205        return v.isoformat()206    elif isinstance(v, dict):207        return {key: _serialize_value(val) for key, val in v.items() if val is not None}208    elif isinstance(v, list):209        return [_serialize_value(item) for item in v]210    return v211 212 213def _dataclass_to_dict(info: Any) -> dict[str, Any]:214    """Convert a dataclass to a json-serializable dict."""215    return {k: _serialize_value(v) for k, v in dataclasses.asdict(info).items() if v is not None}216 217 218_ANSI_RE = re.compile(r"\033\[[0-9;]*m")219_MAX_CELL_LENGTH = 35220 221 222def _strip_ansi(text: str) -> str:223    return _ANSI_RE.sub("", text)224 225 226def _single_line(text: str) -> str:227    return " ".join(text.split())228 229 230def _to_header(name: str) -> str:231    """Convert a camelCase or PascalCase string to SCREAMING_SNAKE_CASE."""232    s = re.sub(r"([a-z])([A-Z])", r"\1_\2", name)233    return s.upper()234 235 236def _format_table_value_human(value: Any) -> str:237    """Convert a value to string for terminal display."""238    if value is None:239        return ""240    if isinstance(value, bool):241        return "✔" if value else ""242    if isinstance(value, datetime.datetime):243        return value.strftime("%Y-%m-%d")244    if isinstance(value, str) and re.match(r"^\d{4}-\d{2}-\d{2}T", value):245        return value[:10]246    if isinstance(value, str):247        return _single_line(value)248    if isinstance(value, list):249        return ", ".join(_format_table_value_human(v) for v in value)250    elif isinstance(value, dict):251        if "name" in value:  # Likely to be a user or org => print name252            return _single_line(str(value["name"]))253        return _single_line(json.dumps(value))254    return _single_line(str(value))255 256 257def _format_table_cell_human(value: Any, max_len: int = _MAX_CELL_LENGTH) -> str:258    """Format a value + truncate it for table display."""259    cell = _format_table_value_human(value)260    if len(cell) > max_len:261        cell = cell[: max_len - 3] + "..."262    return cell263 264 265def _format_table_cell_agent(value: Any) -> str:266    """Format a cell value for agent TSV output (ISO timestamps, tabs escaped)."""267    if isinstance(value, datetime.datetime):268        return value.isoformat()269    return _single_line(str(value))270 271 272out = Output()273 
codekingpro/portable-devtools · Team Ai