From 0a984cd9eb954269aa7412e258896cf8f99128e7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 1 Oct 2022 21:15:35 -0500 Subject: [PATCH 1/8] refactor!(cli): click -> argparse --- src/vcspull/cli/__init__.py | 65 ++++---- src/vcspull/cli/sync.py | 131 ++++++---------- tests/test_cli.py | 288 +++++++++++++++++++++--------------- 3 files changed, 246 insertions(+), 238 deletions(-) diff --git a/src/vcspull/cli/__init__.py b/src/vcspull/cli/__init__.py index 1d36ef0c..0ba013f3 100644 --- a/src/vcspull/cli/__init__.py +++ b/src/vcspull/cli/__init__.py @@ -4,39 +4,52 @@ ~~~~~~~~~~~ """ +import argparse import logging -import click - from libvcs.__about__ import __version__ as libvcs_version from ..__about__ import __version__ from ..log import setup_logger -from .sync import sync +from .sync import create_sync_subparser, sync log = logging.getLogger(__name__) -@click.group( - context_settings={ - "obj": {}, - "help_option_names": ["-h", "--help"], - } -) -@click.option( - "--log-level", - default="INFO", - help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", -) -@click.version_option( - __version__, - "-V", - "--version", - message=f"%(prog)s %(version)s, libvcs {libvcs_version}", -) -def cli(log_level): - setup_logger(log=log, level=log_level.upper()) - - -# Register sub-commands here -cli.add_command(sync) +def create_parser(): + parser = argparse.ArgumentParser(prog="vcspull") + parser.add_argument( + "--version", + "-V", + action="version", + version=f"%(prog)s {__version__}, libvcs {libvcs_version}", + ) + parser.add_argument( + "--log-level", + action="store", + default="INFO", + help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", + ) + subparsers = parser.add_subparsers(dest="subparser_name") + sync_parser = subparsers.add_parser("sync") + create_sync_subparser(sync_parser) + + return parser + + +def cli(args=None): + parser = create_parser() + args = parser.parse_args(args) + + setup_logger(log=log, level=args.log_level.upper()) + + if args.subparser_name is None: + parser.print_help() + return + elif args.subparser_name == "sync": + sync( + repo_terms=args.repo_terms, + config=args.config, + exit_on_error=args.exit_on_error, + parser=parser, + ) diff --git a/src/vcspull/cli/sync.py b/src/vcspull/cli/sync.py index 47b9c467..57aef33f 100644 --- a/src/vcspull/cli/sync.py +++ b/src/vcspull/cli/sync.py @@ -1,60 +1,17 @@ +import argparse import logging import sys +import typing as t from copy import deepcopy -import click -import click.shell_completion -from click.shell_completion import CompletionItem - from libvcs._internal.shortcuts import create_project from libvcs.url import registry as url_tools -from vcspull.types import ConfigDict from ..config import filter_repos, find_config_files, load_configs log = logging.getLogger(__name__) -def get_repo_completions( - ctx: click.Context, param: click.Parameter, incomplete: str -) -> list[CompletionItem]: - configs = ( - load_configs(find_config_files(include_home=True)) - if ctx.params["config"] is None - else load_configs(files=[ctx.params["config"]]) - ) - found_repos: list[ConfigDict] = [] - repo_terms = [incomplete] - - for repo_term in repo_terms: - dir, vcs_url, name = None, None, None - if any(repo_term.startswith(n) for n in ["./", "/", "~", "$HOME"]): - dir = dir - elif any(repo_term.startswith(n) for n in ["http", "git", "svn", "hg"]): - vcs_url = repo_term - else: - name = repo_term - - # collect the repos from the config files - found_repos.extend(filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name)) - if len(found_repos) == 0: - found_repos = configs - - return [ - CompletionItem(o["name"]) - for o in found_repos - if o["name"].startswith(incomplete) - ] - - -def get_config_file_completions(ctx, args, incomplete): - return [ - click.shell_completion.CompletionItem(c) - for c in find_config_files(include_home=True) - if str(c).startswith(incomplete) - ] - - def clamp(n, _min, _max): return max(_min, min(n, _max)) @@ -63,60 +20,53 @@ def clamp(n, _min, _max): NO_REPOS_FOR_TERM_MSG = 'No repo found in config(s) for "{name}"' -@click.command(name="sync") -@click.pass_context -@click.argument( - "repo_terms", type=click.STRING, nargs=-1, shell_complete=get_repo_completions -) -@click.option( - "config", - "--config", - "-c", - type=click.Path(exists=True), - help="Specify config", - shell_complete=get_config_file_completions, -) -@click.option( - "exit_on_error", - "--exit-on-error", - "-x", - is_flag=True, - default=False, - help="Exit immediately when encountering an error syncing multiple repos", -) -def sync(ctx, repo_terms, config, exit_on_error: bool) -> None: +def create_sync_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: + parser.add_argument("--config", "-c", help="Specify config") + parser.add_argument("repo_terms", nargs="+", help="Specify config") + parser.add_argument( + "--exit-on-error", + "-x", + action="store_true", + dest="exit_on_error", + help="Specify config", + ) + return parser + + +def sync( + repo_terms, + config, + exit_on_error: bool, + parser: t.Optional[ + argparse.ArgumentParser + ] = None, # optional so sync can be unit tested +) -> None: if config: configs = load_configs([config]) else: configs = load_configs(find_config_files(include_home=True)) found_repos = [] - if repo_terms: - for repo_term in repo_terms: - dir, vcs_url, name = None, None, None - if any(repo_term.startswith(n) for n in ["./", "/", "~", "$HOME"]): - dir = repo_term - elif any(repo_term.startswith(n) for n in ["http", "git", "svn", "hg"]): - vcs_url = repo_term - else: - name = repo_term - - # collect the repos from the config files - found = filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name) - if len(found) == 0: - click.echo(NO_REPOS_FOR_TERM_MSG.format(name=name)) - found_repos.extend( - filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name) - ) - else: - click.echo(ctx.get_help(), color=ctx.color) - ctx.exit() + for repo_term in repo_terms: + dir, vcs_url, name = None, None, None + if any(repo_term.startswith(n) for n in ["./", "/", "~", "$HOME"]): + dir = repo_term + elif any(repo_term.startswith(n) for n in ["http", "git", "svn", "hg"]): + vcs_url = repo_term + else: + name = repo_term + + # collect the repos from the config files + found = filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name) + if len(found) == 0: + print(NO_REPOS_FOR_TERM_MSG.format(name=name)) + found_repos.extend(filter_repos(configs, dir=dir, vcs_url=vcs_url, name=name)) for repo in found_repos: try: update_repo(repo) except Exception: - click.echo( + print( f'Failed syncing {repo.get("name")}', ) if log.isEnabledFor(logging.DEBUG): @@ -124,7 +74,10 @@ def sync(ctx, repo_terms, config, exit_on_error: bool) -> None: traceback.print_exc() if exit_on_error: - raise click.ClickException(EXIT_ON_ERROR_MSG) + if parser is not None: + parser.exit(status=1, message=EXIT_ON_ERROR_MSG) + else: + raise SystemExit(EXIT_ON_ERROR_MSG) def progress_cb(output, timestamp): diff --git a/tests/test_cli.py b/tests/test_cli.py index 4085f772..4fade45e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,7 +5,6 @@ import pytest import yaml -from click.testing import CliRunner from libvcs.sync.git import GitSync from vcspull.__about__ import __version__ @@ -22,8 +21,10 @@ class SyncCLINonExistentRepo(t.NamedTuple): test_id: str sync_args: list[str] expected_exit_code: int - expected_in_output: "ExpectedOutput" = None - expected_not_in_output: "ExpectedOutput" = None + expected_in_out: "ExpectedOutput" = None + expected_not_in_out: "ExpectedOutput" = None + expected_in_err: "ExpectedOutput" = None + expected_not_in_err: "ExpectedOutput" = None SYNC_CLI_EXISTENT_REPO_FIXTURES = [ @@ -31,24 +32,24 @@ class SyncCLINonExistentRepo(t.NamedTuple): test_id="exists", sync_args=["my_git_project"], expected_exit_code=0, - expected_in_output="Already on 'master'", - expected_not_in_output=NO_REPOS_FOR_TERM_MSG.format(name="my_git_repo"), + expected_in_out="Already on 'master'", + expected_not_in_out=NO_REPOS_FOR_TERM_MSG.format(name="my_git_repo"), ), SyncCLINonExistentRepo( test_id="non-existent-only", sync_args=["this_isnt_in_the_config"], expected_exit_code=0, - expected_in_output=NO_REPOS_FOR_TERM_MSG.format(name="this_isnt_in_the_config"), + expected_in_out=NO_REPOS_FOR_TERM_MSG.format(name="this_isnt_in_the_config"), ), SyncCLINonExistentRepo( test_id="non-existent-mixed", sync_args=["this_isnt_in_the_config", "my_git_project", "another"], expected_exit_code=0, - expected_in_output=[ + expected_in_out=[ NO_REPOS_FOR_TERM_MSG.format(name="this_isnt_in_the_config"), NO_REPOS_FOR_TERM_MSG.format(name="another"), ], - expected_not_in_output=NO_REPOS_FOR_TERM_MSG.format(name="my_git_repo"), + expected_not_in_out=NO_REPOS_FOR_TERM_MSG.format(name="my_git_repo"), ), ] @@ -59,15 +60,19 @@ class SyncCLINonExistentRepo(t.NamedTuple): ids=[test.test_id for test in SYNC_CLI_EXISTENT_REPO_FIXTURES], ) def test_sync_cli_repo_term_non_existent( + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path, config_path: pathlib.Path, - tmp_path: pathlib.Path, git_repo: GitSync, test_id: str, sync_args: list[str], expected_exit_code: int, - expected_in_output: "ExpectedOutput", - expected_not_in_output: "ExpectedOutput", + expected_in_out: "ExpectedOutput", + expected_not_in_out: "ExpectedOutput", + expected_in_err: "ExpectedOutput", + expected_not_in_err: "ExpectedOutput", ) -> None: config = { "~/github_projects/": { @@ -81,31 +86,37 @@ def test_sync_cli_repo_term_non_existent( yaml_config_data = yaml.dump(config, default_flow_style=False) yaml_config.write_text(yaml_config_data, encoding="utf-8") - runner = CliRunner() - with runner.isolated_filesystem(temp_dir=tmp_path): - result = runner.invoke(cli, ["sync", *sync_args]) - assert result.exit_code == expected_exit_code - output = "".join(list(result.output)) + monkeypatch.chdir(tmp_path) + + try: + cli(["sync", *sync_args]) + except SystemExit: + pass - if expected_in_output is not None: - if isinstance(expected_in_output, str): - expected_in_output = [expected_in_output] - for needle in expected_in_output: - assert needle in output + result = capsys.readouterr() + output = "".join(list(result.out)) - if expected_not_in_output is not None: - if isinstance(expected_not_in_output, str): - expected_not_in_output = [expected_not_in_output] - for needle in expected_not_in_output: - assert needle not in output + if expected_in_out is not None: + if isinstance(expected_in_out, str): + expected_in_out = [expected_in_out] + for needle in expected_in_out: + assert needle in output + + if expected_not_in_out is not None: + if isinstance(expected_not_in_out, str): + expected_not_in_out = [expected_not_in_out] + for needle in expected_not_in_out: + assert needle not in output class SyncFixture(t.NamedTuple): test_id: str sync_args: list[str] expected_exit_code: int - expected_in_output: "ExpectedOutput" = None - expected_not_in_output: "ExpectedOutput" = None + expected_in_out: "ExpectedOutput" = None + expected_not_in_out: "ExpectedOutput" = None + expected_in_err: "ExpectedOutput" = None + expected_not_in_err: "ExpectedOutput" = None SYNC_REPO_FIXTURES = [ @@ -114,63 +125,65 @@ class SyncFixture(t.NamedTuple): test_id="empty", sync_args=[], expected_exit_code=0, - expected_in_output=["Options:", "Commands:"], + expected_in_out=["options:", "positional arguments:"], ), # Version SyncFixture( test_id="--version", sync_args=["--version"], expected_exit_code=0, - expected_in_output=[__version__, ", libvcs"], + expected_in_out=[__version__, ", libvcs"], ), SyncFixture( test_id="-V", sync_args=["-V"], expected_exit_code=0, - expected_in_output=[__version__, ", libvcs"], + expected_in_out=[__version__, ", libvcs"], ), # Help SyncFixture( test_id="--help", sync_args=["--help"], expected_exit_code=0, - expected_in_output=["Options:", "Commands:"], + expected_in_out=["options:", "positional arguments:"], ), SyncFixture( test_id="-h", sync_args=["-h"], expected_exit_code=0, - expected_in_output=["Options:", "Commands:"], + expected_in_out=["options:", "positional arguments:"], ), # Sync SyncFixture( test_id="sync--empty", sync_args=["sync"], - expected_exit_code=0, - expected_in_output="Options:", - expected_not_in_output="Commands:", + expected_exit_code=1, + expected_in_out=( + "sync: error: the following arguments are required: repo_terms" + ), + expected_not_in_out="positional arguments:", ), # Sync: Help SyncFixture( test_id="sync---help", sync_args=["sync", "--help"], expected_exit_code=0, - expected_in_output="Options:", - expected_not_in_output="Commands:", + expected_in_out=["options:", "repo_terms", "--exit-on-error"], + expected_not_in_out="--version", ), SyncFixture( test_id="sync--h", sync_args=["sync", "-h"], expected_exit_code=0, - expected_in_output="Options:", - expected_not_in_output="Commands:", + expected_in_out=["options:", "repo_terms", "--exit-on-error"], + expected_not_in_out="--version", ), # Sync: Repo terms SyncFixture( test_id="sync--one-repo-term", sync_args=["sync", "my_git_repo"], expected_exit_code=0, - expected_in_output="my_git_repo", + expected_in_out="my_git_repo", ), ] @@ -181,58 +194,66 @@ class SyncFixture(t.NamedTuple): ids=[test.test_id for test in SYNC_REPO_FIXTURES], ) def test_sync( + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path, config_path: pathlib.Path, - tmp_path: pathlib.Path, git_repo: GitSync, test_id: str, sync_args: list[str], expected_exit_code: int, - expected_in_output: "ExpectedOutput", - expected_not_in_output: "ExpectedOutput", + expected_in_out: "ExpectedOutput", + expected_not_in_out: "ExpectedOutput", + expected_in_err: "ExpectedOutput", + expected_not_in_err: "ExpectedOutput", ) -> None: - runner = CliRunner() - with runner.isolated_filesystem(temp_dir=tmp_path): - config = { - "~/github_projects/": { - "my_git_repo": { - "url": f"git+file://{git_repo.dir}", - "remotes": {"test_remote": f"git+file://{git_repo.dir}"}, - }, - "broken_repo": { - "url": f"git+file://{git_repo.dir}", - "remotes": {"test_remote": "git+file://non-existent-remote"}, - }, - } + config = { + "~/github_projects/": { + "my_git_repo": { + "url": f"git+file://{git_repo.dir}", + "remotes": {"test_remote": f"git+file://{git_repo.dir}"}, + }, + "broken_repo": { + "url": f"git+file://{git_repo.dir}", + "remotes": {"test_remote": "git+file://non-existent-remote"}, + }, } - yaml_config = config_path / ".vcspull.yaml" - yaml_config_data = yaml.dump(config, default_flow_style=False) - yaml_config.write_text(yaml_config_data, encoding="utf-8") + } + yaml_config = config_path / ".vcspull.yaml" + yaml_config_data = yaml.dump(config, default_flow_style=False) + yaml_config.write_text(yaml_config_data, encoding="utf-8") + + # CLI can sync + try: + cli(sync_args) + except SystemExit: + pass - # CLI can sync - result = runner.invoke(cli, sync_args) - assert result.exit_code == expected_exit_code - output = "".join(list(result.output)) + result = capsys.readouterr() + output = "".join(list(result.out if expected_exit_code == 0 else result.err)) - if expected_in_output is not None: - if isinstance(expected_in_output, str): - expected_in_output = [expected_in_output] - for needle in expected_in_output: - assert needle in output + if expected_in_out is not None: + if isinstance(expected_in_out, str): + expected_in_out = [expected_in_out] + for needle in expected_in_out: + assert needle in output - if expected_not_in_output is not None: - if isinstance(expected_not_in_output, str): - expected_not_in_output = [expected_not_in_output] - for needle in expected_not_in_output: - assert needle not in output + if expected_not_in_out is not None: + if isinstance(expected_not_in_out, str): + expected_not_in_out = [expected_not_in_out] + for needle in expected_not_in_out: + assert needle not in output class SyncBrokenFixture(t.NamedTuple): test_id: str sync_args: list[str] expected_exit_code: int - expected_in_output: "ExpectedOutput" = None - expected_not_in_output: "ExpectedOutput" = None + expected_in_out: "ExpectedOutput" = None + expected_not_in_out: "ExpectedOutput" = None + expected_in_err: "ExpectedOutput" = None + expected_not_in_err: "ExpectedOutput" = None SYNC_BROKEN_REPO_FIXTURES = [ @@ -240,44 +261,44 @@ class SyncBrokenFixture(t.NamedTuple): test_id="normal-checkout", sync_args=["my_git_repo"], expected_exit_code=0, - expected_in_output="Already on 'master'", + expected_in_out="Already on 'master'", ), SyncBrokenFixture( test_id="normal-checkout--exit-on-error", sync_args=["my_git_repo", "--exit-on-error"], expected_exit_code=0, - expected_in_output="Already on 'master'", + expected_in_out="Already on 'master'", ), SyncBrokenFixture( test_id="normal-checkout--x", sync_args=["my_git_repo", "-x"], expected_exit_code=0, - expected_in_output="Already on 'master'", + expected_in_out="Already on 'master'", ), SyncBrokenFixture( test_id="normal-first-broken", sync_args=["my_git_repo_not_found", "my_git_repo"], expected_exit_code=0, - expected_not_in_output=EXIT_ON_ERROR_MSG, + expected_not_in_out=EXIT_ON_ERROR_MSG, ), SyncBrokenFixture( test_id="normal-last-broken", sync_args=["my_git_repo", "my_git_repo_not_found"], expected_exit_code=0, - expected_not_in_output=EXIT_ON_ERROR_MSG, + expected_not_in_out=EXIT_ON_ERROR_MSG, ), SyncBrokenFixture( test_id="exit-on-error--exit-on-error-first-broken", sync_args=["my_git_repo_not_found", "my_git_repo", "--exit-on-error"], expected_exit_code=1, - expected_in_output=EXIT_ON_ERROR_MSG, + expected_in_err=EXIT_ON_ERROR_MSG, ), SyncBrokenFixture( test_id="exit-on-error--x-first-broken", sync_args=["my_git_repo_not_found", "my_git_repo", "-x"], expected_exit_code=1, - expected_in_output=EXIT_ON_ERROR_MSG, - expected_not_in_output="master", + expected_in_err=EXIT_ON_ERROR_MSG, + expected_not_in_out="master", ), # # Verify ordering @@ -286,13 +307,15 @@ class SyncBrokenFixture(t.NamedTuple): test_id="exit-on-error--exit-on-error-last-broken", sync_args=["my_git_repo", "my_git_repo_not_found", "-x"], expected_exit_code=1, - expected_in_output=[EXIT_ON_ERROR_MSG, "Already on 'master'"], + expected_in_out="Already on 'master'", + expected_in_err=EXIT_ON_ERROR_MSG, ), SyncBrokenFixture( test_id="exit-on-error--x-last-item", sync_args=["my_git_repo", "my_git_repo_not_found", "--exit-on-error"], expected_exit_code=1, - expected_in_output=[EXIT_ON_ERROR_MSG, "Already on 'master'"], + expected_in_out="Already on 'master'", + expected_in_err=EXIT_ON_ERROR_MSG, ), ] @@ -303,53 +326,72 @@ class SyncBrokenFixture(t.NamedTuple): ids=[test.test_id for test in SYNC_BROKEN_REPO_FIXTURES], ) def test_sync_broken( + tmp_path: pathlib.Path, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path, config_path: pathlib.Path, - tmp_path: pathlib.Path, git_repo: GitSync, test_id: str, sync_args: list[str], expected_exit_code: int, - expected_in_output: "ExpectedOutput", - expected_not_in_output: "ExpectedOutput", + expected_in_out: "ExpectedOutput", + expected_not_in_out: "ExpectedOutput", + expected_in_err: "ExpectedOutput", + expected_not_in_err: "ExpectedOutput", ) -> None: - runner = CliRunner() - github_projects = user_path / "github_projects" my_git_repo = github_projects / "my_git_repo" if my_git_repo.is_dir(): shutil.rmtree(my_git_repo) - with runner.isolated_filesystem(temp_dir=tmp_path): - config = { - "~/github_projects/": { - "my_git_repo": { - "url": f"git+file://{git_repo.dir}", - "remotes": {"test_remote": f"git+file://{git_repo.dir}"}, - }, - "my_git_repo_not_found": { - "url": "git+file:///dev/null", - }, - } + config = { + "~/github_projects/": { + "my_git_repo": { + "url": f"git+file://{git_repo.dir}", + "remotes": {"test_remote": f"git+file://{git_repo.dir}"}, + }, + "my_git_repo_not_found": { + "url": "git+file:///dev/null", + }, } - yaml_config = config_path / ".vcspull.yaml" - yaml_config_data = yaml.dump(config, default_flow_style=False) - yaml_config.write_text(yaml_config_data, encoding="utf-8") - - # CLI can sync - assert isinstance(sync_args, list) - result = runner.invoke(cli, ["sync", *sync_args]) - assert result.exit_code == expected_exit_code - output = "".join(list(result.output)) - - if expected_in_output is not None: - if isinstance(expected_in_output, str): - expected_in_output = [expected_in_output] - for needle in expected_in_output: - assert needle in output - - if expected_not_in_output is not None: - if isinstance(expected_not_in_output, str): - expected_not_in_output = [expected_not_in_output] - for needle in expected_not_in_output: - assert needle not in output + } + yaml_config = config_path / ".vcspull.yaml" + yaml_config_data = yaml.dump(config, default_flow_style=False) + yaml_config.write_text(yaml_config_data, encoding="utf-8") + + # CLI can sync + assert isinstance(sync_args, list) + + try: + cli(["sync", *sync_args]) + except SystemExit: + pass + + result = capsys.readouterr() + out = "".join(list(result.out)) + err = "".join(list(result.err)) + + if expected_in_out is not None: + if isinstance(expected_in_out, str): + expected_in_out = [expected_in_out] + for needle in expected_in_out: + assert needle in out + + if expected_not_in_out is not None: + if isinstance(expected_not_in_out, str): + expected_not_in_out = [expected_not_in_out] + for needle in expected_not_in_out: + assert needle not in out + + if expected_in_err is not None: + if isinstance(expected_in_err, str): + expected_in_err = [expected_in_err] + for needle in expected_in_err: + assert needle in err + + if expected_not_in_err is not None: + if isinstance(expected_not_in_err, str): + expected_not_in_err = [expected_not_in_err] + for needle in expected_not_in_err: + assert needle not in err From e6e5f272553d0bdef1763ba57f32a70dc9c34fef Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:12:15 -0500 Subject: [PATCH 2/8] build(deps): Remove sphinx and sphinx-click poetry remove click; poetry remove --dev sphinx-click --- poetry.lock | 21 ++------------------- pyproject.toml | 3 --- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/poetry.lock b/poetry.lock index 49f1127b..f44d046e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -91,7 +91,7 @@ unicode_backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -702,19 +702,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" @@ -943,7 +930,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "267ae1f2fdbdb7fd172ebe01615ec735ea687bcc31a1bd0c461480209cf9e63a" +content-hash = "ccb542517114d9cbd6f813e9aa1db24bad5e976332908ea0917408bd2289b910" [metadata.files] alabaster = [ @@ -1333,10 +1320,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 4e19e1a9..06eae620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,6 @@ vcspull = 'vcspull:cli.cli' [tool.poetry.dependencies] python = "^3.9" -click = "~8" libvcs = "~0.17.0" colorama = ">=0.3.9" @@ -70,7 +69,6 @@ furo = "*" gp-libs = "0.0.1a16" sphinx-autobuild = "*" sphinx-autodoc-typehints = "*" -sphinx-click = "*" sphinx-inline-tabs = "*" sphinxext-opengraph = "*" sphinx-copybutton = "*" @@ -105,7 +103,6 @@ types-colorama = "*" [tool.poetry.extras] docs = [ "sphinx", - "sphinx-click", "sphinx-autodoc-typehints", "sphinx-autobuild", "sphinxext-rediraffe", From 6d3b77fc8fee5bdef65629564ffd68e38eb19375 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:13:29 -0500 Subject: [PATCH 3/8] build(deps): Add sphinx-argparse See also: - https://github.com/alex-rudakov/sphinx-argparse - https://sphinx-argparse.readthedocs.io/ --- poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index f44d046e..aaa8810b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -656,6 +656,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" @@ -930,7 +944,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "ccb542517114d9cbd6f813e9aa1db24bad5e976332908ea0917408bd2289b910" +content-hash = "a6fc173cf05d34ef279cacb759c738f714adc4f014c08d19c4195d94f61fc48a" [metadata.files] alabaster = [ @@ -1308,6 +1322,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"}, diff --git a/pyproject.toml b/pyproject.toml index 06eae620..c5c10d83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,6 +126,9 @@ lint = [ "types-colorama", ] +[tool.poetry.group.dev.dependencies] +sphinx-argparse = "^0.3.1" + [tool.mypy] python_version = 3.9 warn_unused_configs = true From 7b438818f61a9f494f12c69dcf7f96789ed89168 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:25:42 -0500 Subject: [PATCH 4/8] docs(conf): sphinx-click -> sphinx-argparse --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b75415ec..02c0c4a6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ "sphinx.ext.todo", "sphinx.ext.napoleon", "sphinx.ext.linkcode", - "sphinx_click.ext", # sphinx-click + "sphinxarg.ext", # sphinx-argparse "sphinx_inline_tabs", "sphinx_copybutton", "sphinxext.opengraph", From 6125ac0c25861dfbda134484df1b251025ed7916 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:25:52 -0500 Subject: [PATCH 5/8] docs(cli): Move to argparse --- docs/cli/index.md | 7 ------- docs/cli/sync.md | 17 ++++++++++------- docs/cli/vcspull.md | 11 ++++++++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/cli/index.md b/docs/cli/index.md index b75f43fe..484d474e 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -14,10 +14,3 @@ vcspull sync ``` - -```{toctree} -:caption: More -:maxdepth: 1 - -completion -``` diff --git a/docs/cli/sync.md b/docs/cli/sync.md index 81716b91..a0b567a6 100644 --- a/docs/cli/sync.md +++ b/docs/cli/sync.md @@ -4,6 +4,16 @@ # vcspull sync +## Command + +```{eval-rst} +.. argparse:: + :module: vcspull.cli + :func: create_parser + :prog: vcspull + :path: sync +``` + ## Filtering repos As of 1.13.x, `$ vcspull sync` with no args passed will show a help dialog: @@ -80,10 +90,3 @@ Print traceback for errored repos: ```console $ vcspull --log-level DEBUG sync --exit-on-error grako django ``` - -```{eval-rst} -.. click:: vcspull.cli.sync:sync - :prog: vcspull sync - :commands: sync - :nested: full -``` diff --git a/docs/cli/vcspull.md b/docs/cli/vcspull.md index 9c2927ee..d220c548 100644 --- a/docs/cli/vcspull.md +++ b/docs/cli/vcspull.md @@ -5,7 +5,12 @@ # vcspull ```{eval-rst} -.. click:: vcspull.cli:cli - :prog: Usage - :nested: none +.. argparse:: + :module: vcspull.cli + :func: create_parser + :prog: vcspull + :nosubcommands: + + subparser_name : @replace + See :ref:`cli-sync` ``` From dc122a7f647a6b45d5e9addbb581bf80fa15a64b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:26:05 -0500 Subject: [PATCH 6/8] docs(cli): Remove completion page --- docs/cli/completion.md | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 docs/cli/completion.md diff --git a/docs/cli/completion.md b/docs/cli/completion.md deleted file mode 100644 index 48a78168..00000000 --- a/docs/cli/completion.md +++ /dev/null @@ -1,27 +0,0 @@ -(completion)= - -# Completion - -```{note} -See the [click library's documentation on shell completion](https://click.palletsprojects.com/en/8.0.x/shell-completion/). -``` - -:::{tab} bash - -_~/.bashrc_: - -```bash -eval "$(_VCSPULL_COMPLETE=bash_source vcspull)" -``` - -::: - -:::{tab} zsh - -_~/.zshrc`_: - -```zsh -eval "$(_VCSPULL_COMPLETE=zsh_source vscpull)" -``` - -::: From 808c7b835bb8cd479068a3d460ff044df9622080 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:31:58 -0500 Subject: [PATCH 7/8] !see if this fits across python versions --- tests/test_cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 4fade45e..0fee8b09 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -125,7 +125,7 @@ class SyncFixture(t.NamedTuple): test_id="empty", sync_args=[], expected_exit_code=0, - expected_in_out=["options:", "positional arguments:"], + expected_in_out=["{sync", "positional arguments:"], ), # Version SyncFixture( @@ -145,13 +145,13 @@ class SyncFixture(t.NamedTuple): test_id="--help", sync_args=["--help"], expected_exit_code=0, - expected_in_out=["options:", "positional arguments:"], + expected_in_out=["{sync", "positional arguments:"], ), SyncFixture( test_id="-h", sync_args=["-h"], expected_exit_code=0, - expected_in_out=["options:", "positional arguments:"], + expected_in_out=["{sync", "positional arguments:"], ), # Sync SyncFixture( @@ -168,14 +168,14 @@ class SyncFixture(t.NamedTuple): test_id="sync---help", sync_args=["sync", "--help"], expected_exit_code=0, - expected_in_out=["options:", "repo_terms", "--exit-on-error"], + expected_in_out=["repo_terms", "--exit-on-error"], expected_not_in_out="--version", ), SyncFixture( test_id="sync--h", sync_args=["sync", "-h"], expected_exit_code=0, - expected_in_out=["options:", "repo_terms", "--exit-on-error"], + expected_in_out=["repo_terms", "--exit-on-error"], expected_not_in_out="--version", ), # Sync: Repo terms From 9f36207afd0e8754418150678f06464d90b42fff Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 2 Oct 2022 09:33:29 -0500 Subject: [PATCH 8/8] docs(CHANGES): Note updates on argparse --- CHANGES | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 668366b4..a47dc931 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,18 @@ $ pipx install --suffix=@next 'vcspull' --pip-args '\--pre' --force - _Add your latest changes from PRs here_ +**Maintenance release, no features or fixes** + +### Internal + +- Move from click to :mod:{argparse} + + Click was more difficult to control and workwith, ironically. + +### Packaging + +- Drop click dependency (#400) + ## vcspull v1.14.0 (2022-10-01) **Maintenance release, no features or fixes** @@ -230,7 +242,7 @@ Patch branch: [`v1.12.x`](https://github.com/vcs-python/vcspull/tree/v1.12.x) ### Fix -- Tab-completion for repository names and configurations +- Tab-completion for repository names and configurations (retracted in v1.15) ## vcspull 1.11.1 (2022-03-12) @@ -272,7 +284,7 @@ Patch branch: [`v1.12.x`](https://github.com/vcs-python/vcspull/tree/v1.12.x) ### Improvements -- Experimental completion, see {ref}`completion`: +- Experimental completion (retracted in v1.15): - Completion for sync: @@ -281,7 +293,7 @@ Patch branch: [`v1.12.x`](https://github.com/vcs-python/vcspull/tree/v1.12.x) ### Documentation -- Added {ref}`completion`: +- Added completion: ## vcspull 1.9.0 (2022-02-26)