diff --git a/Makefile b/Makefile index 2efa2e1..c989997 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ test: pytest -vv --capture=tee-sys clean: - rm -rf build/ dist/ *.egg-info .*_cache + rm -rf build/ dist/ *.egg-info .*_cache test-builds test-manifest-builds find . -name '*.pyc' -type f -exec rm -rf {} + find . -name '__pycache__' -exec rm -rf {} + diff --git a/cliffy/cli.py b/cliffy/cli.py index 167bda3..ffea580 100644 --- a/cliffy/cli.py +++ b/cliffy/cli.py @@ -1,5 +1,4 @@ ## CLI to generate CLIs -import contextlib from io import TextIOWrapper import os from typing import IO, Any, Optional, TextIO, Union, cast @@ -8,7 +7,6 @@ from click.types import _is_file_like from .rich import click, Console, print_rich_table -from .rich import ClickGroup # type: ignore from .builder import build_cli, build_cli_from_manifest, run_cli from .helper import ( @@ -27,13 +25,14 @@ from .reloader import CLIManifestReloader CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -class AliasedGroup(ClickGroup): - def get_command(self, ctx: click.Context, cmd_name: Optional[str]) -> Optional[click.Command]: - with contextlib.suppress(KeyError): - cmd_name = ALIASES[cmd_name].name # type: ignore - return super().get_command(ctx, cmd_name or "") +ALIASES = { + "ls": "list", + "add": "load", + "reload": "update", + "rm": "remove", + "rm-all": "remove-all", + "rmall": "remove-all", +} class ManifestOrCLI(click.File): @@ -51,13 +50,21 @@ def convert( # type: ignore[override] return value -@click.group(context_settings=CONTEXT_SETTINGS, cls=AliasedGroup) # type: ignore[arg-type] +def show_aliases_callback(ctx: Any, param: Any, val: bool): + if val: + out("Aliases:") + for alias, command in ALIASES.items(): + out(f" {alias.ljust(10)} Alias for {command}") + ctx.exit() + + +@click.group(context_settings=CONTEXT_SETTINGS) @click.version_option() -def cli() -> None: +@click.option("--aliases", type=bool, is_flag=True, is_eager=True, callback=show_aliases_callback) +def cli(aliases: bool) -> None: pass -@cli.command() # type: ignore[arg-type] @click.argument("manifests", type=click.File("rb"), nargs=-1) def load(manifests: list[TextIO]) -> None: """Load CLI for given manifest(s)""" @@ -70,7 +77,6 @@ def load(manifests: list[TextIO]) -> None: out(f" {T.cli.name} -h") -@cli.command() # type: ignore[arg-type] @click.argument("cli_names", type=str, nargs=-1) def update(cli_names: list[str]) -> None: """Reloads CLI by name""" @@ -86,7 +92,6 @@ def update(cli_names: list[str]) -> None: out_err(f"~ {cli_name} not found") -@cli.command() # type: ignore[arg-type] @click.argument("manifest", type=click.File("rb")) def render(manifest: TextIO) -> None: """Render the CLI manifest generation as code""" @@ -96,7 +101,6 @@ def render(manifest: TextIO) -> None: out(f"# Rendered {T.cli.name} CLI v{T.cli.version} ~", fg="green") -@cli.command("run") # type: ignore[arg-type] @click.argument("manifest", type=click.File("rb")) @click.argument("cli_args", type=str, nargs=-1) def cliffy_run(manifest: TextIO, cli_args: tuple[str]) -> None: @@ -105,7 +109,6 @@ def cliffy_run(manifest: TextIO, cli_args: tuple[str]) -> None: run_cli(T.cli.name, T.cli.code, cli_args) -@cli.command() # type: ignore[arg-type] @click.argument("cli_name", type=str, default="cliffy") @click.option("--version", "-v", type=str, show_default=True, default="v1", help="Manifest version") @click.option("--render", is_flag=True, show_default=True, default=False, help="Render template to terminal directly") @@ -133,7 +136,6 @@ def init(cli_name: str, version: str, render: bool, raw: bool) -> None: out(f"+ {cli_name}.yaml", fg="green") -@cli.command("list") # type: ignore[arg-type] def cliffy_list() -> None: """List all CLIs loaded""" cols = ["Name", "Version", "Age", "Manifest"] @@ -144,7 +146,6 @@ def cliffy_list() -> None: print_rich_table(cols, rows, styles=["cyan", "magenta", "green", "blue"]) -@cli.command() # type: ignore[arg-type] @click.argument("cli_names", type=str, nargs=-1) def remove(cli_names: list[str]) -> None: """Remove a loaded CLI by name""" @@ -157,7 +158,6 @@ def remove(cli_names: list[str]) -> None: out_err(f"~ {cli_name} not loaded") -@cli.command() # type: ignore[arg-type] def remove_all() -> None: """Remove all loaded CLIs""" for metadata in get_clis(): @@ -166,7 +166,6 @@ def remove_all() -> None: out(f"~ {metadata.cli_name} removed 💥", fg="green") -@cli.command() # type: ignore[arg-type] @click.argument("cli_or_manifests", type=ManifestOrCLI(), nargs=-1) @click.option("--output-dir", "-o", type=click.Path(file_okay=False, dir_okay=True, writable=True), help="Output dir") @click.option( @@ -205,7 +204,6 @@ def build(cli_or_manifests: list[Union[TextIOWrapper, str]], output_dir: str, py out(f"+ {cli_name} built 📦", fg="green") -@cli.command() # type: ignore[arg-type] @click.argument("cli_name", type=str) def info(cli_name: str): """Display CLI info""" @@ -217,7 +215,6 @@ def info(cli_name: str): out(f"{click.style('manifest:', fg='blue')}\n{indent_block(metadata.manifest, spaces=2)}") -@cli.command() # type: ignore[arg-type] @click.argument("manifest", type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True)) @click.option( "--run-cli", @@ -242,4 +239,23 @@ def dev(manifest: str, run_cli: bool, run_cli_args: tuple[str]) -> None: CLIManifestReloader.watch(manifest, run_cli, run_cli_args) -ALIASES = {"add": load, "rm": remove, "rm-all": remove_all, "ls": cliffy_list, "reload": update} +# register commands +load_command = cli.command("load")(load) +build_command = cli.command("build")(build) +dev_command = cli.command("dev")(dev) +info_command = cli.command("info")(info) +init_command = cli.command("init")(init) +list_command = cli.command("list")(cliffy_list) +render_command = cli.command("render")(render) +remove_command = cli.command("remove")(remove) +remove_all_command = cli.command("remove-all")(remove_all) +run_command = cli.command("run")(cliffy_run) +update_command = cli.command("update")(update) + +# register aliases +cli.command("add", hidden=True, epilog="Alias for load")(load) +cli.command("ls", hidden=True, epilog="Alias for list")(cliffy_list) +cli.command("rm", hidden=True, epilog="Alias for remove")(remove) +cli.command("rm-all", hidden=True, epilog="Alias for remove-all")(remove_all) +cli.command("rmall", hidden=True, epilog="Alias for remove-all")(remove_all) +cli.command("reload", hidden=True, epilog="Alias for update")(update) diff --git a/cliffy/commander.py b/cliffy/commander.py index 0594ec7..3f37204 100644 --- a/cliffy/commander.py +++ b/cliffy/commander.py @@ -4,7 +4,7 @@ from typing import DefaultDict from pybash.transformer import transform as transform_bash -from pydantic import BaseModel +from pydantic import BaseModel, Field from .manifests import CommandBlock, Manifest from .parser import Parser @@ -13,6 +13,7 @@ class Command(BaseModel): name: str script: CommandBlock + aliases: list[str] = Field(default_factory=list) @classmethod def from_greedy_make_lazy(cls, greedy_command: Command, group: str) -> Command: @@ -47,7 +48,18 @@ class Group(BaseModel): class Commander: """Generates commands based on the command config""" - __slots__ = ("manifest", "parser", "cli", "groups", "greedy", "commands", "root_commands") + __slots__ = ( + "manifest", + "parser", + "cli", + "groups", + "greedy", + "commands", + "root_commands", + "command_aliases", + "base_imports", + "aliases_by_commands", + ) def __init__(self, manifest: Manifest) -> None: self.manifest = manifest @@ -59,11 +71,29 @@ def __init__(self, manifest: Manifest) -> None: Command(name=name, script=script) for name, script in self.manifest.commands.items() ] self.root_commands: list[Command] = [command for command in self.commands if "." not in command.name] + self.base_imports: set[str] = set() + self.aliases_by_commands: dict[str, list[str]] = defaultdict(list) self.build_groups() + self.setup_command_aliases() + + def setup_command_aliases(self) -> None: + for command in self.commands: + if "|" in command.name: + aliases = command.name.split("|") + + # skip group command aliases + if "." in aliases[0]: + continue + + command.name = aliases[0] # update command.name without the alias part + for alias in aliases[1:]: + command.aliases.append(alias) + self.aliases_by_commands[command.name].append(alias) def build_groups(self) -> None: groups: DefaultDict[str, list[Command]] = defaultdict(list) group_help_dict: dict[str, str] = {} + for command in self.commands: # Check for greedy commands- evaluate them at the end if self.is_greedy(command.name): @@ -72,9 +102,19 @@ def build_groups(self) -> None: if "." in command.name: group_name = command.name.split(".")[:-1][-1] + + if "|" in command.name: + command_aliases = command.name.rsplit(".", 1)[1].split("|") + command_name_sub_alias = command.name.split("|", 1)[0] + for alias in command_aliases[1:]: + command.aliases.append(alias) + self.aliases_by_commands[command_name_sub_alias].append(alias) + + command.name = command_name_sub_alias + groups[group_name].append(command) else: - group_help_dict = { + group_help_dict |= { command.name: script_block["help"] for script_block in command.script if isinstance(script_block, dict) and script_block.get("help") @@ -82,7 +122,9 @@ def build_groups(self) -> None: for group_name, commands in groups.items(): self.groups[group_name] = Group( - name=group_name, commands=commands, help=group_help_dict.get(group_name, "") + name=group_name, + commands=commands, + help=group_help_dict.get(group_name, ""), ) def generate_cli(self) -> None: diff --git a/cliffy/commanders/typer.py b/cliffy/commanders/typer.py index 70f9703..e00cf6a 100644 --- a/cliffy/commanders/typer.py +++ b/cliffy/commanders/typer.py @@ -1,50 +1,88 @@ import datetime from ..commander import Command, Commander, Group +from ..manifests import Manifest class TyperCommander(Commander): """Generates commands based on the command config""" + def __init__(self, manifest: Manifest) -> None: + super().__init__(manifest) + self.base_imports.add("import typer") + self.base_imports.add("import subprocess") + self.base_imports.add("from typing import Optional, Any") + def add_base_imports(self): - self.cli = f"""## Generated {self.manifest.name} on {datetime.datetime.now()} -import typer -import subprocess -from typing import Optional -""" + self.cli = f"""## Generated {self.manifest.name} on {datetime.datetime.now()}\n""" + for imp in self.base_imports: + self.cli += imp + "\n" def add_base_cli(self) -> None: self.cli += """ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) cli = typer.Typer(context_settings=CONTEXT_SETTINGS""" - if self.manifest.cli_options: self.cli += f",{self.parser.to_args(self.manifest.cli_options)}" if self.manifest.help: self.cli += f', help="""{self.manifest.help}"""' - self.cli += f""") __version__ = '{self.manifest.version}' __cli_name__ = '{self.manifest.name}' +""" + self.cli += """ def version_callback(value: bool): if value: - print(f"{{__cli_name__}}, {{__version__}}") + print(f"{__cli_name__}, {__version__}") raise typer.Exit() +""" - + if self.aliases_by_commands: + self.cli += """ +def aliases_callback(value: bool): + if value: + print(\"\"\"""" + max_command_length = max(len(x) for x in self.aliases_by_commands.keys()) + self.cli += f""" +{"Command".ljust(max_command_length + 7)}Aliases +{"--------".ljust(max_command_length + 7)}-------- +""" + for command, alias_list in self.aliases_by_commands.items(): + self.cli += f"{command.ljust(max_command_length + 7)}" + self.cli += ", ".join(alias_list) + self.cli += "\n" + self.cli += """\"\"\") + raise typer.Exit() +""" + self.cli += """ @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main(""" + if self.aliases_by_commands: + self.cli += """ + aliases: Optional[bool] = typer.Option(None, '--aliases', callback=aliases_callback, is_eager=True),""" + + self.cli += """ + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass """ def add_root_command(self, command: Command) -> None: + parsed_command_func_name = self.parser.get_command_func_name(command) + parsed_command_name = self.parser.get_parsed_command_name(command) self.cli += f""" -@cli.command("{self.parser.get_parsed_command_name(command)}") -def {self.parser.get_command_func_name(command)}({self.parser.parse_args(command)}): +def {parsed_command_func_name}({self.parser.parse_args(command)}): {self.parser.parse_command(command.script)} + +cli.command("{parsed_command_name}")({parsed_command_func_name}) +""" + + for alias in command.aliases: + self.cli += f""" +cli.command("{alias}", hidden=True, epilog="Alias for {parsed_command_name}")({parsed_command_func_name}) """ def add_group(self, group: Group) -> None: @@ -53,10 +91,18 @@ def add_group(self, group: Group) -> None: """ def add_sub_command(self, command: Command, group: Group) -> None: + parsed_command_func_name = self.parser.get_command_func_name(command) + parsed_command_name = self.parser.get_parsed_command_name(command) self.cli += f""" -@{group.name}_app.command("{self.parser.get_parsed_command_name(command)}") -def {self.parser.get_command_func_name(command)}({self.parser.parse_args(command)}): +def {parsed_command_func_name}({self.parser.parse_args(command)}): {self.parser.parse_command(command.script)} + +{group.name}_app.command("{parsed_command_name}")({parsed_command_func_name}) +""" + + for alias in command.aliases: + self.cli += f""" +{group.name}_app.command("{alias}", hidden=True, epilog="Alias for {parsed_command_name}")({parsed_command_func_name}) """ def add_main_block(self) -> None: diff --git a/cliffy/manifests/v1.py b/cliffy/manifests/v1.py index 2ae0b2c..83e5ef3 100644 --- a/cliffy/manifests/v1.py +++ b/cliffy/manifests/v1.py @@ -45,6 +45,7 @@ class CLIManifest(BaseModel): description="A mapping containing the command definitions for the CLI. " "Each command should have a unique key- which can be either a group command or nested subcommands. " "Nested subcommands are joined by '.' in between each level. " + "Aliases for commands can be separated in the key by '|'. " "A special (*) wildcard can be used to spread the subcommand to all group-level commands. " "The value is the python code to run when the command is called " "OR a list of bash commands to run (prefixed with $).", @@ -110,7 +111,7 @@ def get_template(cls, name: str) -> str: {cls.get_field_description('vars')} vars: default_mood: happy - debug_mode: "{{ env['DEBUG'] or 'False' }}" + debug_mode: "{{{{ env['DEBUG'] or 'False' }}}}" {cls.get_field_description('imports')} imports: @@ -139,8 +140,8 @@ def greet_name(name: str): {cls.get_field_description('commands')} commands: - # this is a parent command that will get invoked with: hello world - world: + # this is a parent command that will get invoked with: hello world or hello wld + world|wld: - | \"\"\" Help text for list diff --git a/cliffy/parser.py b/cliffy/parser.py index 36e63dc..f582e82 100644 --- a/cliffy/parser.py +++ b/cliffy/parser.py @@ -130,8 +130,8 @@ def parse_args(self, command) -> str: return parsed_command_args[:-2] def get_command_func_name(self, command) -> str: - """a -> a, a.b -> a_b, a-b -> a_b""" - return command.name.replace(".", "_").replace("-", "_") + """a -> a, a.b -> a_b, a-b -> a_b, a|b -> a_b""" + return command.name.replace(".", "_").replace("-", "_").replace("|", "_") def get_parsed_command_name(self, command) -> str: """a -> a, a.b -> b""" diff --git a/examples/db.yaml b/examples/db.yaml index 307e08b..61eec45 100644 --- a/examples/db.yaml +++ b/examples/db.yaml @@ -13,20 +13,20 @@ imports: | console = Console() commands: - create: | + create|mk: | """Create a new database""" console.print(f"Creating database {name}", style="green") - delete: | + delete|rm: | """Delete a database""" sure = typer.confirm("Are you really really really sure?") if sure: console.print(f"Deleting database {name}", style="red") else: console.print(f"Back to safety!", style="green") - list: | + list|ls: | """List databases""" print("Listing all databases") - view: | + view|v: | """View database table""" console.print(f"Viewing {table} table for {name} DB") diff --git a/examples/generated/db.py b/examples/generated/db.py index c080c1d..d77cf8e 100644 --- a/examples/generated/db.py +++ b/examples/generated/db.py @@ -1,14 +1,22 @@ -## Generated db on 2024-08-02 14:08:54.018361 +## Generated db on 2024-08-06 21:09:20.229514 +from typing import Optional, Any import typer +from typer.core import TyperGroup import subprocess -from typing import Optional from rich.console import Console console = Console() +BASE_ALIASES = {'mk': 'create', 'rm': 'delete', 'ls': 'list', 'v': 'view'} +class BaseAliasGroup(TyperGroup): + def get_command(self, ctx: Any, cmd_name: str) -> Optional[Any]: + if cmd_name in BASE_ALIASES: + return self.commands.get(BASE_ALIASES[cmd_name]) + + return super().get_command(ctx, cmd_name) CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -cli = typer.Typer(context_settings=CONTEXT_SETTINGS, add_completion=False, help="""Database CLI""") +cli = typer.Typer(context_settings=CONTEXT_SETTINGS, add_completion=False, help="""Database CLI""", cls=BaseAliasGroup) __version__ = '0.1.0' __cli_name__ = 'db' @@ -18,9 +26,21 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() +def aliases_callback(value: bool): + if value: + print(""" +create: mk +delete: rm +list: ls +view: v +""") + raise typer.Exit() @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + aliases: Optional[bool] = typer.Option(None, '--aliases', callback=aliases_callback, is_eager=True), + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass diff --git a/examples/generated/environ.py b/examples/generated/environ.py index 3771346..0100b1b 100644 --- a/examples/generated/environ.py +++ b/examples/generated/environ.py @@ -1,7 +1,7 @@ -## Generated environ on 2024-08-02 14:08:54.024887 +## Generated environ on 2024-08-06 21:09:20.234970 +from typing import Optional, Any import typer import subprocess -from typing import Optional import os default_env_var = 'hello' @@ -18,9 +18,10 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() - @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass diff --git a/examples/generated/hello.py b/examples/generated/hello.py index 21ddb8b..b70ed73 100644 --- a/examples/generated/hello.py +++ b/examples/generated/hello.py @@ -1,7 +1,7 @@ -## Generated hello on 2024-08-02 14:08:54.027610 +## Generated hello on 2024-08-06 21:09:20.239310 +from typing import Optional, Any import typer import subprocess -from typing import Optional CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) cli = typer.Typer(context_settings=CONTEXT_SETTINGS, help="""Hello world!""") @@ -14,9 +14,10 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() - @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass diff --git a/examples/generated/penv.py b/examples/generated/penv.py index a95e96e..83b799e 100644 --- a/examples/generated/penv.py +++ b/examples/generated/penv.py @@ -1,7 +1,7 @@ -## Generated penv on 2024-08-02 14:08:54.032251 +## Generated penv on 2024-08-06 21:09:20.243667 +from typing import Optional, Any import typer import subprocess -from typing import Optional import os from pathlib import Path from shutil import rmtree @@ -24,9 +24,10 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() - @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass def get_venv_path(name: str) -> str: diff --git a/examples/generated/pydev.py b/examples/generated/pydev.py index b3e61ef..b11213d 100644 --- a/examples/generated/pydev.py +++ b/examples/generated/pydev.py @@ -1,7 +1,7 @@ -## Generated pydev on 2024-08-02 14:08:54.038822 +## Generated pydev on 2024-08-06 21:09:20.248496 +from typing import Optional, Any import typer import subprocess -from typing import Optional import sys @@ -17,9 +17,10 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() - @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass def run_cmd(cmd: str): diff --git a/examples/generated/requires.py b/examples/generated/requires.py index 33f8524..e2b9391 100644 --- a/examples/generated/requires.py +++ b/examples/generated/requires.py @@ -1,7 +1,7 @@ -## Generated requires on 2024-08-02 14:08:54.271747 +## Generated requires on 2024-08-06 21:09:20.457028 +from typing import Optional, Any import typer import subprocess -from typing import Optional import six @@ -16,9 +16,10 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() - @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass diff --git a/examples/generated/template.py b/examples/generated/template.py index c83f5e3..346195e 100644 --- a/examples/generated/template.py +++ b/examples/generated/template.py @@ -1,7 +1,7 @@ -## Generated template on 2024-08-02 14:08:54.276078 +## Generated template on 2024-08-06 21:09:20.460271 +from typing import Optional, Any import typer import subprocess -from typing import Optional GLOBAL_VAR = 'hello' @@ -16,9 +16,10 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() - @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass diff --git a/examples/generated/town.py b/examples/generated/town.py index 05f9927..f854efc 100644 --- a/examples/generated/town.py +++ b/examples/generated/town.py @@ -1,7 +1,8 @@ -## Generated town on 2024-08-02 14:08:54.283122 +## Generated town on 2024-08-06 21:09:20.467366 +from typing import Optional, Any import typer +from typer.core import TyperGroup import subprocess -from typing import Optional import re import time @@ -18,9 +19,20 @@ def version_callback(value: bool): print(f"{__cli_name__}, {__version__}") raise typer.Exit() +def aliases_callback(value: bool): + if value: + print(""" +home.build: bu +home.sell: s +home.buy: b +""") + raise typer.Exit() @cli.callback() -def main(version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True)): +def main( + aliases: Optional[bool] = typer.Option(None, '--aliases', callback=aliases_callback, is_eager=True), + version: Optional[bool] = typer.Option(None, '--version', callback=version_callback, is_eager=True) +): pass def format_money(money: float): @@ -69,7 +81,7 @@ def land_buy(name: str = typer.Argument(..., help="Name"), money: float = typer. print(f"buying land {name} for {format_money(money)}") people_app = typer.Typer() -cli.add_typer(people_app, name="people", help="") +cli.add_typer(people_app, name="people", help="Manage people") @people_app.command("add") def people_add(fullname: str = typer.Argument(...), age: int = typer.Argument(...), home: str = typer.Option(None, "--home", "-h")): @@ -83,7 +95,7 @@ def people_remove(fullname: str = typer.Argument(...)): print(f"removing person {fullname}") shops_app = typer.Typer() -cli.add_typer(shops_app, name="shops", help="") +cli.add_typer(shops_app, name="shops", help="Manage shops") @shops_app.command("build") def shops_build(name: str = typer.Argument(..., help="Name"), land: str = typer.Argument(...), type: str = typer.Option(None, "--type", "-t")): @@ -102,7 +114,16 @@ def shops_buy(name: str = typer.Argument(..., help="Name"), money: float = typer """Buy a shop""" print(f"buying shop {name} for ${money}") -home_app = typer.Typer() + +HOME_ALIASES = {'bu': 'build', 's': 'sell', 'b': 'buy'} +class HomeAliasGroup(TyperGroup): + def get_command(self, ctx: Any, cmd_name: str) -> Optional[Any]: + if cmd_name in HOME_ALIASES: + return self.commands.get(HOME_ALIASES[cmd_name]) + + return super().get_command(ctx, cmd_name) + +home_app = typer.Typer(cls=HomeAliasGroup) cli.add_typer(home_app, name="home", help="Manage homes") @home_app.command("build") diff --git a/examples/town.yaml b/examples/town.yaml index 03d2220..31c1776 100644 --- a/examples/town.yaml +++ b/examples/town.yaml @@ -42,13 +42,13 @@ commands: shops.buy: | """Buy a shop""" print(f"buying shop {name} for ${money}") - home.build: | + home.build|bu: | """Build a home""" print(f"building home at {address} for {owner} on land {land}") - home.sell: | + home.sell|s: | """Sell a home""" print(f"selling home {address} for {format_money(money)}") - home.buy: | + home.buy|b: | """Buy a home""" print(f"buying home {address} for {money}") print("test123") diff --git a/pyproject.toml b/pyproject.toml index 3760e9d..b356bef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cliffy" -version = "0.3.6" +version = "0.3.7" description = "$ cli load from.yaml" authors = ["Jay "] repository = "https://github.com/jaykv/cliffy" diff --git a/tests/test_cli.py b/tests/test_cli.py index b9d0305..14b5cee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,7 @@ from click.testing import CliRunner -from cliffy.cli import cli, cliffy_run, init, render +from cliffy.cli import cli, run_command, init_command, render_command from cliffy.homer import get_metadata ANSI_RE = re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]") @@ -27,14 +27,14 @@ def test_cli_version(): def test_cli_init(): runner = CliRunner() - result = runner.invoke(init, ["hello", "--render"]) + result = runner.invoke(init_command, ["hello", "--render"]) assert result.exit_code == 0 assert "name: hello" in escape_ansi(result.output) def test_cli_render(): runner = CliRunner() - result = runner.invoke(render, ["examples/town.yaml"]) + result = runner.invoke(render_command, ["examples/town.yaml"]) assert result.exit_code == 0 assert "cli = typer.Typer" in escape_ansi(result.output) assert get_metadata("town") is None @@ -42,7 +42,7 @@ def test_cli_render(): def test_cli_run(): runner = CliRunner() - result = runner.invoke(cliffy_run, ["examples/hello.yaml", "--", "-h"]) + result = runner.invoke(run_command, ["examples/hello.yaml", "--", "-h"]) assert result.exit_code == 0 assert "Hello world!" in escape_ansi(result.output) assert get_metadata("hello") is None diff --git a/tests/test_examples.py b/tests/test_examples.py index dff78d7..d946ab1 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -8,7 +8,7 @@ import pytest from click.testing import CliRunner -from cliffy.cli import build, load, remove +from cliffy.cli import build_command, load_command, remove_command from cliffy.homer import get_clis, get_metadata try: @@ -30,6 +30,9 @@ {"args": "land build test123 202str", "resp": "building land"}, {"args": "land sell test123 --money 50", "resp": "selling"}, {"args": "land list", "resp": "listing land"}, + {"args": "home build test123 202str", "resp": "building home at test123 for None on land 202str"}, + {"args": "home bu test123 202str", "resp": "building home at test123 for None on land 202str"}, + {"args": "home s test123 --money 123", "resp": "selling home test123 for $123.00"}, ], "template": [ {"args": "hello bash", "resp": "hello from bash"}, @@ -43,6 +46,9 @@ ], "db": [ {"args": "list", "resp": "Listing all databases"}, + {"args": "ls", "resp": "Listing all databases"}, + {"args": "mk --name test", "resp": "Creating database test"}, + {"args": "v --name test --table test", "resp": "Viewing test table for test DB"}, ], } @@ -66,7 +72,7 @@ def setup_module(): def teardown_module(cls): runner = CliRunner() for cli in pytest.installed_clis: # type: ignore - runner.invoke(remove, cli) + runner.invoke(remove_command, cli) clis = get_clis() for cli in clis: @@ -79,7 +85,7 @@ def teardown_module(cls): @pytest.mark.parametrize("cli_name", CLI_LOADS) def test_cli_loads(cli_name): runner = CliRunner() - result = runner.invoke(load, [f"examples/{cli_name}.yaml"]) + result = runner.invoke(load_command, [f"examples/{cli_name}.yaml"]) assert result.exit_code == 0 assert get_metadata(cli_name) is not None pytest.installed_clis.append(cli_name) # type: ignore @@ -88,7 +94,7 @@ def test_cli_loads(cli_name): @pytest.mark.parametrize("cli_name", CLI_LOAD_FAILS) def test_cli_load_fails(cli_name): runner = CliRunner() - result = runner.invoke(load, [f"examples/{cli_name}.yaml"]) + result = runner.invoke(load_command, [f"examples/{cli_name}.yaml"]) assert result.exit_code == 1 assert get_metadata(cli_name) is None @@ -96,7 +102,7 @@ def test_cli_load_fails(cli_name): @pytest.mark.parametrize("cli_name", CLI_BUILDS) def test_cli_builds(cli_name): runner = CliRunner() - result = runner.invoke(build, [f"{cli_name}", "-o", "test-builds"]) + result = runner.invoke(build_command, [f"{cli_name}", "-o", "test-builds"]) assert result.exit_code == 0 assert f"+ {cli_name} built" in result.stdout @@ -104,7 +110,7 @@ def test_cli_builds(cli_name): @pytest.mark.parametrize("cli_name", CLI_BUILD_FAILS) def test_cli_build_fails(cli_name): runner = CliRunner() - result = runner.invoke(build, [f"{cli_name}", "-o", "test-builds"]) + result = runner.invoke(build_command, [f"{cli_name}", "-o", "test-builds"]) assert result.exit_code == 0 assert f"~ {cli_name} not loaded" in result.stdout @@ -112,7 +118,7 @@ def test_cli_build_fails(cli_name): @pytest.mark.parametrize("cli_name", CLI_MANIFEST_BUILDS) def test_cli_builds_from_manifests(cli_name): runner = CliRunner() - result = runner.invoke(build, [f"examples/{cli_name}.yaml", "-o", "test-manifest-builds"]) + result = runner.invoke(build_command, [f"examples/{cli_name}.yaml", "-o", "test-manifest-builds"]) assert result.exit_code == 0