1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
|
import configparser
import os
import asyncclick as click
class Config:
"""The config in this example only holds aliases."""
def __init__(self):
self.path = os.getcwd()
self.aliases = {}
def add_alias(self, alias, cmd):
self.aliases.update({alias: cmd})
def read_config(self, filename):
parser = configparser.RawConfigParser()
parser.read([filename])
try:
self.aliases.update(parser.items("aliases"))
except configparser.NoSectionError:
pass
def write_config(self, filename):
parser = configparser.RawConfigParser()
parser.add_section("aliases")
for key, value in self.aliases.items():
parser.set("aliases", key, value)
with open(filename, "wb") as file:
parser.write(file)
pass_config = click.make_pass_decorator(Config, ensure=True)
class AliasedGroup(click.Group):
"""This subclass of a group supports looking up aliases in a config
file and with a bit of magic.
"""
def get_command(self, ctx, cmd_name):
# Step one: bulitin commands as normal
rv = click.Group.get_command(self, ctx, cmd_name)
if rv is not None:
return rv
# Step two: find the config object and ensure it's there. This
# will create the config object is missing.
cfg = ctx.ensure_object(Config)
# Step three: look up an explicit command alias in the config
if cmd_name in cfg.aliases:
actual_cmd = cfg.aliases[cmd_name]
return click.Group.get_command(self, ctx, actual_cmd)
# Alternative option: if we did not find an explicit alias we
# allow automatic abbreviation of the command. "status" for
# instance will match "st". We only allow that however if
# there is only one command.
matches = [
x for x in self.list_commands(ctx) if x.lower().startswith(cmd_name.lower())
]
if not matches:
return None
elif len(matches) == 1:
return click.Group.get_command(self, ctx, matches[0])
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
def resolve_command(self, ctx, args):
# always return the command's name, not the alias
_, cmd, args = super().resolve_command(ctx, args)
return cmd.name, cmd, args
def read_config(ctx, param, value):
"""Callback that is used whenever --config is passed. We use this to
always load the correct config. This means that the config is loaded
even if the group itself never executes so our aliases stay always
available.
"""
cfg = ctx.ensure_object(Config)
if value is None:
value = os.path.join(os.path.dirname(__file__), "aliases.ini")
cfg.read_config(value)
return value
@click.command(cls=AliasedGroup)
@click.option(
"--config",
type=click.Path(exists=True, dir_okay=False),
callback=read_config,
expose_value=False,
help="The config file to use instead of the default.",
)
def cli():
"""An example application that supports aliases."""
@cli.command()
def push():
"""Pushes changes."""
click.echo("Push")
@cli.command()
def pull():
"""Pulls changes."""
click.echo("Pull")
@cli.command()
def clone():
"""Clones a repository."""
click.echo("Clone")
@cli.command()
def commit():
"""Commits pending changes."""
click.echo("Commit")
@cli.command()
@pass_config
def status(config):
"""Shows the status."""
click.echo(f"Status for {config.path}")
@cli.command()
@pass_config
@click.argument("alias_", metavar="ALIAS", type=click.STRING)
@click.argument("cmd", type=click.STRING)
@click.option(
"--config_file", type=click.Path(exists=True, dir_okay=False), default="aliases.ini"
)
def alias(config, alias_, cmd, config_file):
"""Adds an alias to the specified configuration file."""
config.add_alias(alias_, cmd)
config.write_config(config_file)
click.echo(f"Added '{alias_}' as alias for '{cmd}'")
|