Skip to content

Commit

Permalink
Use argh for dispatch instead of argparse
Browse files Browse the repository at this point in the history
  • Loading branch information
dmerejkowsky committed Aug 13, 2020
1 parent 08f8497 commit 983989d
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 211 deletions.
6 changes: 3 additions & 3 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ That being said:
`--verbose` flag, like so: `tsrc --verbose sync`


# Why argparse?
# Why argh?

See [docopt v argparse](https://dmerej.info/blog/post/docopt-v-argparse/), and
[please don't use click](http://xion.io/post/programming/python-dont-use-click.html).
Because we need (almost) all of `argparse` features, but still want to keep the
code DRY.


# Why YAML?
Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ warn_return_any = true
ignore_missing_imports = false
pretty = true

[mypy-argh]
ignore_missing_imports = true

[mypy-colored_traceback]
ignore_missing_imports = true

Expand Down
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ requests = "^2.22.0"
schema = "^0.7.1"
tabulate = "^0.8.6"
unidecode = "^1.1.1"
argh = "^0.26.2"

[tool.poetry.dev-dependencies]
# Tests
Expand Down
46 changes: 33 additions & 13 deletions tsrc/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
""" Common tools for tsrc commands """

from typing import List
import argparse
from typing import List, Optional
import os

from argh import arg
import cli_ui as ui
from path import Path

Expand All @@ -13,6 +13,24 @@
from tsrc.manifest import Manifest


with_workspace = arg(
"-w",
"--workspace",
dest="workspace_path",
help="path to the current workspace",
type=Path,
)
with_groups = arg(
"-g", "--group", "--groups", nargs="+", dest="groups", help="groups to use"
)
with_all_cloned = arg(
"--all-cloned",
action="store_true",
dest="all_cloned",
help="run on all cloned repos",
)


def find_workspace_path() -> Path:
""" Look for a workspace root somewhere in the upper directories
hierarchy
Expand All @@ -30,32 +48,34 @@ def find_workspace_path() -> Path:
raise tsrc.Error("Could not find current workspace")


def get_workspace(args: argparse.Namespace) -> tsrc.Workspace:
if args.workspace_path:
workspace_path = Path(args.workspace_path)
else:
def get_workspace(workspace_path: Optional[Path]) -> tsrc.Workspace:
if not workspace_path:
workspace_path = find_workspace_path()
return tsrc.Workspace(workspace_path)


def get_workspace_with_repos(args: argparse.Namespace) -> tsrc.Workspace:
workspace = get_workspace(args)
workspace.repos = resolve_repos(workspace, args=args)
def get_workspace_with_repos(
workspace_path: Path, groups: Optional[List[str]], all_cloned: bool
) -> tsrc.Workspace:
workspace = get_workspace(workspace_path)
workspace.repos = resolve_repos(workspace, groups, all_cloned)
return workspace


def resolve_repos(workspace: Workspace, args: argparse.Namespace) -> List[tsrc.Repo]:
def resolve_repos(
workspace: Workspace, groups: Optional[List[str]], all_cloned: bool
) -> List[tsrc.Repo]:
""""
Given a workspace with its config and its local manifest,
and a collection of parsed command line arguments,
return the list of repositories to operate on.
"""
# Handle --all-cloned and --groups
manifest = workspace.get_manifest()
if args.groups:
return manifest.get_repos(groups=args.groups)
if groups:
return manifest.get_repos(groups=groups)

if args.all_cloned:
if all_cloned:
repos = manifest.get_repos(all_=True)
return [repo for repo in repos if (workspace.root_path / repo.dest).exists()]

Expand Down
25 changes: 18 additions & 7 deletions tsrc/cli/apply_manifest.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
""" Entry point for `tsrc apply-manifest` """

import argparse
from typing import Optional
from argh import arg
from path import Path

import cli_ui as ui
import tsrc.cli

import tsrc.manifest
from tsrc.cli import (
with_workspace,
get_workspace,
repos_from_config,
)

def main(args: argparse.Namespace) -> None:
workspace = tsrc.cli.get_workspace(args)
manifest_path = args.manifest_path

ui.info_1("Applying manifest from", args.manifest_path)
@with_workspace # type: ignore
@arg("manifest_path", help="path to the local manifest", type=Path) # type: ignore
def apply_manifest(manifest_path: Path, workspace_path: Optional[Path] = None) -> None:
""" apply a local manifest file """
workspace = get_workspace(workspace_path)

ui.info_1("Applying manifest from", manifest_path)

manifest = tsrc.manifest.load(manifest_path)
workspace.repos = tsrc.cli.repos_from_config(manifest, workspace.config)
workspace.repos = repos_from_config(manifest, workspace.config)
workspace.clone_missing()
workspace.set_remotes()
workspace.perform_filesystem_operations()
94 changes: 80 additions & 14 deletions tsrc/cli/foreach.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
""" Entry point for tsrc foreach """

from typing import List
import argparse
from typing import List, Union, Optional
from argh import arg
import subprocess
import textwrap
import sys

from path import Path
import cli_ui as ui

import tsrc
import tsrc.cli
from tsrc.cli import (
with_workspace,
with_groups,
with_all_cloned,
get_workspace,
resolve_repos,
)

EPILOG = textwrap.dedent(
"""\
Usage:
# Run command directly
tsrc foreach -- some-cmd --with-option
Or:
# Run command through the shell
tsrc foreach -c 'some cmd'
"""
)


Command = Union[str, List[str]]


class CommandFailed(tsrc.Error):
Expand All @@ -21,18 +43,22 @@ class CouldNotStartProcess(tsrc.Error):

class CmdRunner(tsrc.Task[tsrc.Repo]):
def __init__(
self, workspace_path: Path, cmd: List[str], cmd_as_str: str, shell: bool = False
self,
workspace_path: Path,
command: Command,
description: str,
shell: bool = False,
) -> None:
self.workspace_path = workspace_path
self.cmd = cmd
self.cmd_as_str = cmd_as_str
self.command = command
self.description = description
self.shell = shell

def display_item(self, repo: tsrc.Repo) -> str:
return repo.dest

def on_start(self, *, num_items: int) -> None:
ui.info_1(f"Running `{self.cmd_as_str}` on {num_items} repos")
ui.info_1(f"Running `{self.description}` on {num_items} repos")

def on_failure(self, *, num_errors: int) -> None:
ui.error(f"Command failed for {num_errors} repo(s)")
Expand All @@ -45,24 +71,64 @@ def process(self, index: int, count: int, repo: tsrc.Repo) -> None:
# fmt: off
ui.info(
ui.lightgray, "$ ",
ui.reset, ui.bold, self.cmd_as_str,
ui.reset, ui.bold, self.description,
sep=""
)
# fmt: on
full_path = self.workspace_path / repo.dest
try:
rc = subprocess.call(self.cmd, cwd=full_path, shell=self.shell)
rc = subprocess.call(self.command, cwd=full_path, shell=self.shell)
except OSError as e:
raise CouldNotStartProcess("Error when starting process:", e)
if rc != 0:
raise CommandFailed()


def main(args: argparse.Namespace) -> None:
workspace = tsrc.cli.get_workspace_with_repos(args)
cmd_runner = CmdRunner(
workspace.root_path, args.cmd, args.cmd_as_str, shell=args.shell
)
def die(message: str) -> None:
ui.error(message)
print(EPILOG, end="")
sys.exit(1)


@with_workspace # type: ignore
@with_groups # type: ignore
@with_all_cloned # type: ignore
@arg("cmd", help="command to run", nargs="*") # type: ignore
@arg("-c", help="use a shell to run the command", dest="shell") # type: ignore
def foreach(
cmd: List[str],
workspace_path: Optional[Path] = None,
groups: Optional[List[str]] = None,
all_cloned: bool = False,
shell: bool = False,
) -> None:
""" run the same command on several repositories """
# Note:
# we want to support both:
# $ tsrc foreach -c 'shell command'
# and
# $ tsrc foreach -- some-cmd --some-opts
#
# Due to argparse limitations, cmd will always be a list,
# but we need a *string* when using 'shell=True'
#
# So transform use the value from `cmd` and `shell` to build:
# * `subprocess_cmd`, suitable as argument to pass to subprocess.run()
# * `cmd_as_str`, suitable for display purposes
command: Command = []
if shell:
if len(cmd) != 1:
die("foreach -c must be followed by exactly one argument")
command = cmd[0]
description = cmd[0]
else:
if not cmd:
die("needs a command to run")
command = cmd
description = " ".join(cmd)
workspace = get_workspace(workspace_path)
workspace.repos = resolve_repos(workspace, groups=groups, all_cloned=all_cloned)
cmd_runner = CmdRunner(workspace.root_path, command, description, shell=shell)
tsrc.run_sequence(workspace.repos, cmd_runner)
ui.info("OK", ui.check)

Expand Down
39 changes: 27 additions & 12 deletions tsrc/cli/init.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
""" Entry point for `tsrc init` """
import argparse
from typing import List, Optional
import os

from path import Path
from argh import arg
import cli_ui as ui
from path import Path

import tsrc
from tsrc.cli import repos_from_config
from tsrc.cli import repos_from_config, with_workspace, with_groups
from tsrc.workspace import Workspace
from tsrc.workspace.config import WorkspaceConfig


def main(args: argparse.Namespace) -> None:
path_as_str = args.workspace_path or os.getcwd()
remote_help = "only use this remote when cloning repositories"


@with_workspace # type: ignore
@with_groups # type: ignore
@arg("-r", "--singular-remote", help=remote_help) # type: ignore
def init(
url: str,
workspace_path: Optional[Path] = None,
groups: Optional[List[str]] = None,
branch: str = "master",
clone_all_repos: bool = False,
shallow: bool = False,
singular_remote: Optional[str] = None,
) -> None:
""" initialize a new workspace"""
path_as_str = workspace_path or os.getcwd()
workspace_path = Path(path_as_str)
cfg_path = workspace_path / ".tsrc" / "config.yml"

Expand All @@ -22,12 +37,12 @@ def main(args: argparse.Namespace) -> None:
ui.info_1("Configuring workspace in", ui.bold, workspace_path)

workspace_config = WorkspaceConfig(
manifest_url=args.url,
manifest_branch=args.branch,
clone_all_repos=args.clone_all_repos,
repo_groups=args.groups,
shallow_clones=args.shallow,
singular_remote=args.remote,
manifest_url=url,
manifest_branch=branch,
clone_all_repos=clone_all_repos,
repo_groups=groups or [],
shallow_clones=shallow,
singular_remote=singular_remote,
)

workspace_config.save_to_file(cfg_path)
Expand Down
Loading

0 comments on commit 983989d

Please sign in to comment.