[Python 3.10] コマンドライン サブコマンド 雛形

#!/usr/bin/env python
# -*- coding:utf-8 -*-

r"""" コマンドラインプログラムの雛形 最小

各コマンドの実装はargparse.Actionの派生クラスで実装できるが
コード量が多くなるのと単純なもの以外では面倒くさい感じなので
ここでは使用しない。
"""
# from typing import override
from abc import ABC, abstractmethod
from argparse import(
    ArgumentParser,
    _SubParsersAction, # type: ignore
    RawDescriptionHelpFormatter,
    Namespace,
)
import locale
import pathlib
import sys
from typing import Any, TypeAlias, TYPE_CHECKING


__version__ = "0.0.1"


if TYPE_CHECKING:
    SubParserType: TypeAlias = _SubParsersAction[ArgumentParser]

else:
    SubParserType: TypeAlias = Any


class AbstractSubCommand(ABC):
    r"""サブコマンドの抽象基底クラス"""

    @classmethod
    def set_argument(cls, parser_: SubParserType) -> None:
        r"""パーサーに登録"""
        parser = cls._set_argument_impl(parser_)
        parser.set_defaults(func=cls._run)

    @classmethod
    @abstractmethod
    def _set_argument_impl(cls, parser_: SubParserType) -> ArgumentParser:
        r"""パーサーに登録 実装"""
        raise NotImplementedError()

    @classmethod
    def _run(cls, args: Namespace) -> int:
        r"""処理内容"""
        return cls._run_impl(args)

    @classmethod
    @abstractmethod
    def _run_impl(cls, args: Namespace) -> int:
        r"""処理内容 実装"""
        raise NotImplementedError()

class CatCommand(AbstractSubCommand):
    r"""サブコマンド cat """

    # @override
    @classmethod
    def _set_argument_impl(cls, parser_: SubParserType) -> ArgumentParser:
        r"""パーサーに登録 実装"""
        parser = parser_.add_parser("cat", help="ファイルを出力する")
        parser.add_argument("inputfile", help="対象ファイル")
        parser.add_argument("--encoding", default="utf-8")
        return parser

    # @override
    @classmethod
    def _run_impl(cls, args: Namespace) -> int:
        r"""処理内容 実装"""
        path = pathlib.Path(args.inputfile)
        if not path.exists():
            print(f"ファイルが存在しません。'{args.inputfile}'"
                  , file=sys.stderr)
            return 1

        if not path.is_file():
            print(f"ファイルを指定して下さい。'{args.inputfile}'"
                  , file=sys.stderr)
            return 1

        text = path.read_text(encoding=args.encoding)
        print(text)

        return 0


def set_argsparse() -> ArgumentParser:
    r"""パーサーの起動とメインコマンドの登録"""
    description ="""説明 1行目
   2行目
   3行目"""
    argparser = ArgumentParser(
        prog="コマンドプログラムの雛形",
        description=description,
        formatter_class=RawDescriptionHelpFormatter,
        add_help=True,
        allow_abbrev=False)

    argparser.add_argument(
        "-v", "--version",
        action="version",
        version=f"%(prog)s {__version__}")

    argparser.add_argument(
        "--env",
        action="store_true",
        help="実行環境の情報を表示する")

    return argparser

def set_subcommand(parser_: ArgumentParser):
    r"""サブコマンドを登録"""
    commands: list[type[AbstractSubCommand]] = [
        CatCommand,
    ]
    subparser = parser_.add_subparsers(title="sub commands")
    for cmd in commands:
        cmd.set_argument(subparser)

def main_command_env() -> int:
    r"""実行環境の出力"""
    print("{}  {}  {}.{}.{} {} {}".format(
        sys.platform,
        sys.implementation.cache_tag,
        *sys.implementation.version))
    print(f"  sys.getrecursionlimit(): {sys.getrecursionlimit()}")
    print(f"  sys.stdout.encoding: {sys.stdout.encoding}")
    print(f"  sys.getdefaultencoding(): {sys.getdefaultencoding()}")
    print(f"  sys.getfilesystemencoding(): {sys.getfilesystemencoding()}")
    print(f"  locale.getpreferredencoding(): {locale.getpreferredencoding()}")
    print("  sys.path: [\n    {}]".format("\n    ".join(sys.path)))

    return 0

def main_command(args: Namespace) -> tuple[int, bool]:
    r"""
    メインコマンドがあるならここに処理を書く
    """
    ret: int = 0
    is_continue: bool = True

    if args.env:
        ret = main_command_env()
        is_continue = False

    return (ret, is_continue)


def main() -> int:
    r"""メイン処理"""
    argparser = set_argsparse()
    set_subcommand(argparser)

    try:
        args = argparser.parse_args()

    except SystemExit:
        return 1

    ret, is_continue = main_command(args)
    if not is_continue:
        return ret

    if hasattr(args, 'func'):
        return args.func(args)

    argparser.print_help()
    return 0


if __name__ == "__main__":
    sys.exit(main())