## hatch_ci.cli

src/hatch_ci/cli.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    from __future__ import annotations

    import argparse
    import functools
    import logging
    import sys
    from typing import Any, Callable, Protocol

    from . import tools

    class ErrorFn(Protocol):
        def __call__(self, message: str, explain: str | None, hint: str | None) -> None:
            ...

    class AbortExecutionError(Exception):
        @staticmethod
        def _strip(txt):
            txt = txt or ""
            txt = txt[1:] if txt.startswith("\n") else txt
            txt = tools.indent(txt, pre="")
            return txt[:-1] if txt.endswith("\n") else txt

        def __init__(
            self,
            message: str,
            explain: str | None = None,
            hint: str | None = None,
            usage: str | None = None,
        ):
            self.message = message.strip()
            self.explain = explain
            self.hint = hint
            self.usage = usage

        def __str__(self):
            out = []
            if self.usage:
                out.extend(self.usage.strip().split("\n"))
            if self.message:
                out.extend(self._strip(self.message).split("\n"))
            if self.explain:
                out.append("reason:")
                out.extend(tools.indent(self.explain).split("\n"))
            if self.hint:
                out.append("hint:")
                out.extend(tools.indent(self.hint).split("\n"))
            return "\n".join((line.strip() if not line.strip() else line) for line in out)

    def _add_arguments(
        parser: argparse.ArgumentParser,
    ) -> None:
        """parses args from the command line

        Args:
            args: command line arguments or None to pull from sys.argv
            doc: text to use in cli description
        """
        parser.add_argument("-n", "--dry-run", dest="dryrun", action="store_true")
        parser.add_argument("-v", "--verbose", action="store_true")

    def _process_options(
        options: argparse.Namespace, errorfn: ErrorFn
    ) -> argparse.Namespace | None:
        logging.basicConfig(
            format="%(levelname)s:%(name)s:(dry-run) %(message)s"
            if options.dryrun
            else "%(levelname)s:%(name)s:%(message)s",
            level=logging.DEBUG if options.verbose else logging.INFO,
        )

        for d in [
            "verbose",
        ]:
            delattr(options, d)
        return options

    def cli(
        add_arguments: Callable[[argparse.ArgumentParser], None] | None = None,
        process_options: Callable[[argparse.Namespace, ErrorFn], argparse.Namespace | None]
        | None = None,
        doc: str | None = None,
    ):
        @functools.wraps(cli)
        def _fn(main: Callable[[argparse.Namespace], Any]):
            @functools.wraps(main)
            def _fn1(args: None | list[str] = None) -> Any:
                try:

                    class ParserFormatter(
                        argparse.ArgumentDefaultsHelpFormatter,
                        argparse.RawDescriptionHelpFormatter,
                    ):
                        pass

                    description, _, epilog = (doc or "").partition("\n")
                    parser = argparse.ArgumentParser(
                        formatter_class=ParserFormatter,
                        description=description,
                        epilog=epilog,
                    )
                    _add_arguments(parser)
                    if add_arguments:
                        add_arguments(parser)

                    options = parser.parse_args(args=args)

                    def error(
                        message: str,
                        explain: str = "",
                        hint: str = "",
                        usage: str | None = None,
                    ):
                        raise AbortExecutionError(message, explain, hint, usage)

                    errorfn: ErrorFn = functools.partial(error, usage=parser.format_usage())
                    options.error = errorfn

                    options = _process_options(options, errorfn) or options
                    if process_options:
                        options = process_options(options, errorfn) or options

                    return main(options)
                except AbortExecutionError as err:
                    print(str(err), file=sys.stderr)  # noqa: T201
                    raise SystemExit(2) from None
                except Exception:
                    raise

            return _fn1

        return _fn