diff --git a/CHANGES b/CHANGES index a46eb1b9..134bfbd9 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,16 @@ You can test the unpublished version of cihai-cli before its released, see - _Insert changes/features/fixes for next release here_ +### Internal + +- Move from click to {mod}`argparse` (#286) + + Click was more difficult to control and work with, ironically. + +### Packaging + +- Drop click dependency (#286) + ## cihai-cli 0.14.0 (2022-10-01) ### Packages diff --git a/docs/cli/index.md b/docs/cli/index.md index 7b4c7ede..4c8be1c5 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -2,9 +2,24 @@ # Commands +## Sub-commands + ```{toctree} :caption: General info reverse ``` + +## Command: `cihai` + +```{eval-rst} +.. argparse:: + :module: cihai_cli.cli + :func: create_parser + :prog: cihai + :nosubcommands: + + subparser_name : @replace + See :ref:`cli-info`, :ref:`cli-reverse` +``` diff --git a/docs/cli/info.md b/docs/cli/info.md index 8b4f0a39..081efe35 100644 --- a/docs/cli/info.md +++ b/docs/cli/info.md @@ -1,5 +1,15 @@ +(cli-info)= + +(cihai-info)= + +# cihai info + +## Command + ```{eval-rst} -.. click:: cihai_cli.cli:command_info - :prog: cihai info - :show-nested: +.. argparse:: + :module: cihai_cli.cli + :func: create_parser + :prog: cihai + :path: info ``` diff --git a/docs/cli/reverse.md b/docs/cli/reverse.md index a9b55206..f28778a9 100644 --- a/docs/cli/reverse.md +++ b/docs/cli/reverse.md @@ -1,5 +1,15 @@ +(cli-reverse)= + +(cihai-reverse)= + +# cihai reverse + +## Command + ```{eval-rst} -.. click:: cihai_cli.cli:command_reverse - :prog: cihai reverse - :show-nested: +.. argparse:: + :module: cihai_cli.cli + :func: create_parser + :prog: cihai + :path: reverse ``` diff --git a/docs/conf.py b/docs/conf.py index a313cf24..09925c05 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.linkcode", "sphinx.ext.napoleon", - "sphinx_click.ext", # sphinx-click + "sphinxarg.ext", # sphinx-argparse "sphinx_inline_tabs", "sphinx_copybutton", "sphinxext.opengraph", diff --git a/poetry.lock b/poetry.lock index 20f82b3b..5cbe2770 100644 --- a/poetry.lock +++ b/poetry.lock @@ -114,7 +114,7 @@ unihan-etl = ">=0.18.0,<0.19.0" name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -138,7 +138,7 @@ requests = ">=2.7.9" name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -251,7 +251,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "4.12.0" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -678,6 +678,20 @@ docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] +[[package]] +name = "sphinx-argparse" +version = "0.3.1" +description = "A sphinx extension that automatically documents argparse commands and options" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +sphinx = ">=1.2.0" + +[package.extras] +markdown = ["CommonMark (>=0.5.6)"] + [[package]] name = "sphinx-autobuild" version = "2021.3.14" @@ -724,19 +738,6 @@ sphinx = ">=4.0" [package.extras] docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] -[[package]] -name = "sphinx-click" -version = "4.3.0" -description = "Sphinx extension that automatically documents click applications" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=7.0" -docutils = "*" -sphinx = ">=2.0" - [[package]] name = "sphinx-copybutton" version = "0.5.0" @@ -916,7 +917,7 @@ python-versions = "*" name = "typing-extensions" version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -978,7 +979,7 @@ python-versions = "*" name = "zipp" version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -996,7 +997,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "9d2705c0c7bc904a9a510589258863327024e5b83455cef9708bc67c5504c112" +content-hash = "d185c570193dc1e36ea579d2cefef068599a75d04ea7bc6974c18747d72f0436" [metadata.files] alabaster = [ @@ -1378,6 +1379,10 @@ Sphinx = [ {file = "Sphinx-5.2.3.tar.gz", hash = "sha256:5b10cb1022dac8c035f75767799c39217a05fc0fe2d6fe5597560d38e44f0363"}, {file = "sphinx-5.2.3-py3-none-any.whl", hash = "sha256:7abf6fabd7b58d0727b7317d5e2650ef68765bbe0ccb63c8795fa8683477eaa2"}, ] +sphinx-argparse = [ + {file = "sphinx-argparse-0.3.1.tar.gz", hash = "sha256:82151cbd43ccec94a1530155f4ad34f251aaca6a0ffd5516d7fadf952d32dc1e"}, + {file = "sphinx_argparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:295ccae425874630b6a3b47254854027345d786bab2c3ffd5e9a0407bc6856b2"}, +] sphinx-autobuild = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, @@ -1390,10 +1395,6 @@ sphinx-basic-ng = [ {file = "sphinx_basic_ng-1.0.0b1-py3-none-any.whl", hash = "sha256:ade597a3029c7865b24ad0eda88318766bcc2f9f4cef60df7e28126fde94db2a"}, {file = "sphinx_basic_ng-1.0.0b1.tar.gz", hash = "sha256:89374bd3ccd9452a301786781e28c8718e99960f2d4f411845ea75fc7bb5a9b0"}, ] -sphinx-click = [ - {file = "sphinx-click-4.3.0.tar.gz", hash = "sha256:bd4db5d3c1bec345f07af07b8e28a76cfc5006d997984e38ae246bbf8b9a3b38"}, - {file = "sphinx_click-4.3.0-py3-none-any.whl", hash = "sha256:23e85a3cb0b728a421ea773699f6acadefae171d1a764a51dd8ec5981503ccbe"}, -] sphinx-copybutton = [ {file = "sphinx-copybutton-0.5.0.tar.gz", hash = "sha256:a0c059daadd03c27ba750da534a92a63e7a36a7736dcf684f26ee346199787f6"}, {file = "sphinx_copybutton-0.5.0-py3-none-any.whl", hash = "sha256:9684dec7434bd73f0eea58dda93f9bb879d24bff2d8b187b1f2ec08dfe7b5f48"}, diff --git a/pyproject.toml b/pyproject.toml index 3f342aac..f9a9cbdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,6 @@ Repository = "https://github.com/cihai/cihai-cli" [tool.poetry.dependencies] python = "^3.7" -click = ">=7,<8.2" cihai = "~0.18.1" PyYAML = "*" @@ -68,7 +67,7 @@ gp-libs = "0.0.1a16" furo = "*" sphinx-autobuild = "*" sphinx-autodoc-typehints = "*" -sphinx-click = "*" +sphinx-argparse = "*" sphinx-inline-tabs = { version = "*", python = "^3.7" } sphinxext-opengraph = "*" sphinx-copybutton = "*" @@ -104,7 +103,7 @@ cihai = "cihai_cli.cli:cli" docs = [ "docutils", "sphinx", - "sphinx-click", + "sphinx-argparse", "sphinx-autodoc-typehints", "sphinx-autobuild", "sphinx-copybutton", diff --git a/src/cihai_cli/cli.py b/src/cihai_cli/cli.py index a212937b..05876f8f 100644 --- a/src/cihai_cli/cli.py +++ b/src/cihai_cli/cli.py @@ -1,14 +1,14 @@ +import argparse import logging import sys -import click import yaml -import cihai +from cihai.__about__ import __version__ as cihai_version from cihai.core import Cihai -from unihan_etl.__about__ import __version__ as __unihan_etl_version__ +from unihan_etl.__about__ import __version__ as unihan_etl_version -from .__about__ import __title__, __version__ +from .__about__ import __version__ #: fields which are human friendly HUMAN_UNIHAN_FIELDS = [ @@ -25,69 +25,93 @@ "kTotalStrokes", ] +INFO_SHORT_HELP = 'Get details on a CJK character, e.g. "好"' + + +def create_parser(): + parser = argparse.ArgumentParser(prog="cihai") + parser.add_argument( + "--version", + "-V", + action="version", + version=( + f"%(prog)s cihai-cli {__version__}, " + f"cihai {cihai_version}, unihan-etl {unihan_etl_version}" + ), + ) + parser.add_argument( + "--log-level", + action="store", + default="INFO", + help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", + ) + parser.add_argument( + "--config", + "-c", + action="store", + help="Path to custom config file", + ) + subparsers = parser.add_subparsers(dest="subparser_name") + info_parser = subparsers.add_parser("info", help=INFO_SHORT_HELP) + create_info_subparser(info_parser) + reverse_parser = subparsers.add_parser( + "reverse", help='Search all info for character matches, e.g. "good"' + ) + create_reverse_subparser(reverse_parser) + + return parser + -@click.group(context_settings={"obj": {}}) -@click.version_option( - __version__, - "-V", - "--version", - message=""" -{prog} %(version)s, cihai {cihai_version}, unihan-etl {unihan_etl_version} -""".format( - prog=__title__, - cihai_version=cihai.__version__, - unihan_etl_version=__unihan_etl_version__, - ).strip(), -) -@click.option( - "-c", - "--config", - type=click.Path(exists=True), - metavar="", - help="path to custom config file", -) -@click.option( - "--log_level", - default="INFO", - metavar="", - help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", -) -@click.pass_context -def cli(ctx, config, log_level): +def cli(args=None): """Retrieve CJK information via CLI. For help and example usage, see documentation: https://cihai-cli.git-pull.com and https://cihai.git-pull.com""" - setup_logger(level=log_level.upper()) - if config: - c = Cihai.from_file(config) + + parser = create_parser() + args = parser.parse_args(args) + + setup_logger(level=args.log_level.upper()) + + if args.config: + c = Cihai.from_file(args.config) else: c = Cihai() if not c.unihan.is_bootstrapped: - click.echo("Bootstrapping Unihan database") + print("Bootstrapping Unihan database") c.unihan.bootstrap(options=c.config.get("unihan_options", {})) - ctx.obj["c"] = c # pass Cihai object down to other commands + if args.subparser_name is None: + parser.print_help() + return + elif args.subparser_name == "info": + command_info(c=c, char=args.char, show_all=args.show_all) + elif args.subparser_name == "reverse": + command_reverse(c=c, char=args.char, show_all=args.show_all) + else: + print(f"No subparser for {args.subparser_name}") -INFO_SHORT_HELP = 'Get details on a CJK character, e.g. "好"' +def create_info_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: + parser.add_argument("char", metavar="", help="Specify config") + parser.add_argument( + "--all", + "-a", + dest="show_all", + action="store_true", + help="Show all character details", + ) + return parser -@cli.command(name="info", short_help=INFO_SHORT_HELP) -@click.argument("char", metavar="") -@click.option( - "-a", "--all", "show_all", is_flag=True, help="Show all character details" -) -@click.pass_context -def command_info(ctx, char, show_all): +def command_info(c: Cihai, char: str, show_all: bool): """Look up a definition by term.""" - c = ctx.obj["c"] query = c.unihan.lookup_char(char).first() attrs = {} if not query: - click.echo("No records found for %s" % char, err=True) + print("No records found for %s" % char) sys.exit() for c in query.__table__.columns._data.keys(): value = getattr(query, c) @@ -95,25 +119,30 @@ def command_info(ctx, char, show_all): if not show_all and str(c) not in HUMAN_UNIHAN_FIELDS: continue attrs[str(c)] = value - click.echo( + print( yaml.safe_dump(attrs, allow_unicode=True, default_flow_style=False).strip("\n") ) -@cli.command( - name="reverse", short_help='Search all info for character matches, e.g. "good"' -) -@click.argument("char", metavar="") -@click.option( - "-a", "--all", "show_all", is_flag=True, help="Show all character details" -) -@click.pass_context -def command_reverse(ctx, char, show_all): +def create_reverse_subparser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + parser.add_argument("char", metavar="", help="Specify config") + parser.add_argument( + "--all", + "-a", + dest="show_all", + action="store_true", + help="Show all character details", + ) + return parser + + +def command_reverse(c: Cihai, char: str, show_all: bool): """Lookup a word or phrase by searching definitions.""" - c = ctx.obj["c"] query = c.unihan.reverse_char([char]) if not query.count(): - click.echo("No records found for %s" % char, err=True) + print("No records found for %s" % char) sys.exit() for k in query: attrs = {} @@ -123,12 +152,12 @@ def command_reverse(ctx, char, show_all): if not show_all and str(c) not in HUMAN_UNIHAN_FIELDS: continue attrs[str(c)] = value - click.echo( + print( yaml.safe_dump(attrs, allow_unicode=True, default_flow_style=False).strip( "\n" ) ) - click.echo("--------") + print("--------") def setup_logger(logger=None, level="INFO"): diff --git a/tests/test_cli.py b/tests/test_cli.py index c386df9d..31b10546 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,24 +3,40 @@ import pytest import yaml -from click.testing import CliRunner -from cihai_cli import cli +from cihai_cli.cli import cli -def test_cli(test_config_file): - runner = CliRunner() - result = runner.invoke(cli.cli) - assert result.exit_code == 0 - result = runner.invoke(cli.cli, "-c", test_config_file) - result = runner.invoke(cli.cli, "info") - assert result.exit_code == 2 - # result = runner.invoke(cli, ['info', '好']) - # assert result.exit_code == 0 +def test_cli( + test_config_file: pathlib.Path, + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.chdir(tmp_path) + + try: + cli() + except SystemExit: + pass + + try: + cli(["-c", test_config_file]) + except SystemExit: + pass + + try: + cli(["info"]) + except SystemExit: + pass def test_cli_reflects_after_bootstrap( - tmp_path: pathlib.Path, tmpdb_file, unihan_options + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, + tmpdb_file, + unihan_options, ): config = { "database": {"url": f"sqlite:///{tmpdb_file}s"}, @@ -30,18 +46,34 @@ def test_cli_reflects_after_bootstrap( config_file.write_text( yaml.dump(config, default_flow_style=False), encoding="utf-8" ) - runner = CliRunner() - result = runner.invoke(cli.cli, ["-c", str(config_file), "info", "㐀"]) - assert "Bootstrapping Unihan database" in result.output - assert result.exit_code == 0 - result = runner.invoke(cli.cli, ["-c", str(config_file)], "info") + try: + cli(["-c", str(config_file), "info", "㐀"]) + except SystemExit: + result = capsys.readouterr() + output = "".join(list(result.out)) + assert "Bootstrapping Unihan database" in output + + try: + cli(["-c", str(config_file), "info"]) + except SystemExit: + result = capsys.readouterr() + output = "".join(list(result.out)) + assert "Bootstrapping" in output @pytest.mark.parametrize("flag", ["-V", "--version"]) -def test_cli_version(flag): - runner = CliRunner() - result = runner.invoke(cli.cli, [flag]) - assert "cihai-cli" in result.output - assert "cihai" in result.output - assert "unihan-etl" in result.output +def test_cli_version( + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, + flag: str, +): + try: + result = cli([flag]) + except SystemExit: + result = capsys.readouterr() + output = "".join(list(result.out)) + assert "cihai-cli" in output + assert "cihai" in output + assert "unihan-etl" in output