Tengo una aplicación de clic grande que he desarrollado, pero navegar a través de los diferentes comandos / subcomandos se está volviendo difícil. ¿Cómo organizo mis comandos en archivos separados? ¿Es posible organizar comandos y sus subcomandos en clases separadas?
Aquí hay un ejemplo de cómo me gustaría separarlo:
en eso
import click
@click.group()
@click.version_option()
def cli():
pass #Entry Point
command_cloudflare.py
@cli.group()
@click.pass_context
def cloudflare(ctx):
pass
@cloudflare.group('zone')
def cloudflare_zone():
pass
@cloudflare_zone.command('add')
@click.option('--jumpstart', '-j', default=True)
@click.option('--organization', '-o', default='')
@click.argument('url')
@click.pass_obj
@__cf_error_handler
def cloudflare_zone_add(ctx, url, jumpstart, organization):
pass
@cloudflare.group('record')
def cloudflare_record():
pass
@cloudflare_record.command('add')
@click.option('--ttl', '-t')
@click.argument('domain')
@click.argument('name')
@click.argument('type')
@click.argument('content')
@click.pass_obj
@__cf_error_handler
def cloudflare_record_add(ctx, domain, name, type, content, ttl):
pass
@cloudflare_record.command('edit')
@click.option('--ttl', '-t')
@click.argument('domain')
@click.argument('name')
@click.argument('type')
@click.argument('content')
@click.pass_obj
@__cf_error_handler
def cloudflare_record_edit(ctx, domain):
pass
command_uptimerobot.py
@cli.group()
@click.pass_context
def uptimerobot(ctx):
pass
@uptimerobot.command('add')
@click.option('--alert', '-a', default=True)
@click.argument('name')
@click.argument('url')
@click.pass_obj
def uptimerobot_add(ctx, name, url, alert):
pass
@uptimerobot.command('delete')
@click.argument('names', nargs=-1, required=True)
@click.pass_obj
def uptimerobot_delete(ctx, names):
pass
@click.pass_context
. Alternativamente, también hay algo llamado Acceso de contexto global : click.pocoo.org/6/advanced/#global-context-access .CommandCollection
. La respuesta de Oscar tiene un ejemplo, y hay uno realmente bueno en la documentación de click: click.palletsprojects.com/en/7.x/commands/… .Suponga que su proyecto tiene la siguiente estructura:
Los grupos no son más que varios comandos y los grupos se pueden anidar. Puede separar sus grupos en módulos e importarlos en su
init.py
archivo y agregarlos alcli
grupo usando el comando add_command.He aquí un
init.py
ejemplo:import click from .commands.cloudflare import cloudflare @click.group() def cli(): pass cli.add_command(cloudflare)
Tienes que importar el grupo cloudflare que vive dentro del archivo cloudflare.py. Tu
commands/cloudflare.py
se vería así:import click @click.group() def cloudflare(): pass @cloudflare.command() def zone(): click.echo('This is the zone subcommand of the cloudflare command')
Entonces puedes ejecutar el comando cloudflare así:
Esta información no es muy explícita en la documentación pero si miras el código fuente, que está muy bien comentado, puedes ver cómo se pueden anidar los grupos.
fuente
@cloudflare.command()
de lazone
función si importozone
desde otro lugar?Estoy buscando algo como esto en este momento, en tu caso es simple porque tienes grupos en cada uno de los archivos, puedes solucionar este problema como se explica en la documentación :
En el
init.py
archivo:import click from command_cloudflare import cloudflare from command_uptimerobot import uptimerobot cli = click.CommandCollection(sources=[cloudflare, uptimerobot]) if __name__ == '__main__': cli()
La mejor parte de esta solución es que es totalmente compatible con pep8 y otros linters porque no necesita importar algo que no usaría y no necesita importar * desde ningún lugar.
fuente
cli
desde init.py, pero esto conduce a importaciones circulares. ¿Podría explicar cómo se hace?click.group
que es el que importa en la CLI de nivel superior.Me tomó un tiempo resolver esto, pero pensé que lo pondría aquí para recordarme a mí mismo cuando me olvide de cómo hacerlo. Creo que parte del problema es que la función add_command se menciona en la página de github de click pero no en la principal. página de ejemplos
primero vamos a crear un archivo python inicial llamado root.py
import click from cli_compile import cli_compile from cli_tools import cli_tools @click.group() def main(): """Demo""" if __name__ == '__main__': main.add_command(cli_tools) main.add_command(cli_compile) main()
A continuación, coloquemos algunos comandos de herramientas en un archivo llamado cli_tools.py
import click # Command Group @click.group(name='tools') def cli_tools(): """Tool related commands""" pass @cli_tools.command(name='install', help='test install') @click.option('--test1', default='1', help='test option') def install_cmd(test1): click.echo('Hello world') @cli_tools.command(name='search', help='test search') @click.option('--test1', default='1', help='test option') def search_cmd(test1): click.echo('Hello world') if __name__ == '__main__': cli_tools()
A continuación, coloquemos algunos comandos de compilación en un archivo llamado cli_compile.py
import click @click.group(name='compile') def cli_compile(): """Commands related to compiling""" pass @cli_compile.command(name='install2', help='test install') def install2_cmd(): click.echo('Hello world') @cli_compile.command(name='search2', help='test search') def search2_cmd(): click.echo('Hello world') if __name__ == '__main__': cli_compile()
ejecutar root.py ahora debería darnos
Usage: root.py [OPTIONS] COMMAND [ARGS]... Demo Options: --help Show this message and exit. Commands: compile Commands related to compiling tools Tool related commands
ejecutar "root.py compile" debería darnos
Usage: root.py compile [OPTIONS] COMMAND [ARGS]... Commands related to compiling Options: --help Show this message and exit. Commands: install2 test install search2 test search
También notará que puede ejecutar cli_tools.py o cli_compile.py directamente y también incluí una declaración principal allí
fuente
No soy un experto en clics, pero debería funcionar simplemente importando sus archivos al principal. Movería todos los comandos en archivos separados y tendría un archivo principal importando los otros. De esa forma es más fácil controlar el orden exacto, en caso de que sea importante para ti. Entonces, su archivo principal se vería así:
import commands_main import commands_cloudflare import commands_uptimerobot
fuente
editar: me acabo de dar cuenta de que mi respuesta / comentario es poco más que una repetición de lo que ofrecen los documentos oficiales de Click en la sección "Comandos múltiples personalizados": https://click.palletsprojects.com/en/7.x/commands/#custom -multi-comandos
Solo para agregar a la excelente respuesta aceptada por @jdno, se me ocurrió una función auxiliar que auto-importa y agrega automáticamente módulos de subcomando, lo que reduce enormemente el texto estándar en mi
cli.py
:La estructura de mi proyecto es esta:
Cada archivo de subcomando se parece a esto:
import click @click.command() def foo(): """foo this is for foos!""" click.secho("FOO", fg="red", bg="white")
(por ahora, solo tengo un subcomando por archivo)
En
cli.py
, he escrito unaadd_subcommand()
función que recorre cada ruta de archivo agrupada por "subcomandos / *. Py" y luego realiza el comando de importación y adición.Esto es lo que se simplifica el cuerpo del script cli.py:
import click import importlib from pathlib import Path import re @click.group() def entry_point(): """whats up, this is the main function""" pass def main(): add_subcommands() entry_point() if __name__ == '__main__': main()
Y así es
add_subcommands()
como se ve la función:SUBCOMMAND_DIR = Path("projectroot/console/subcommands") def add_subcommands(maincommand=entry_point): for modpath in SUBCOMMAND_DIR.glob('*.py'): modname = re.sub(f'/', '.', str(modpath)).rpartition('.py')[0] mod = importlib.import_module(modname) # filter out any things that aren't a click Command for attr in dir(mod): foo = getattr(mod, attr) if callable(foo) and type(foo) is click.core.Command: maincommand.add_command(foo)
No sé qué tan robusto es esto si tuviera que diseñar un comando que tuviera varios niveles de anidación y cambio de contexto. Pero parece funcionar bien por ahora :)
fuente