2
votes

Avec python click, comment éviter de dupliquer le code d'argument utilisé par plusieurs sous-commandes

J'ai un groupe de sous-commandes qui fonctionnent toutes sur une liste d'URL qui peuvent éventuellement être passées en argument. Comment puis-je attribuer cet argument au groupe à la place pour éviter de dupliquer la définition de l'argument sur chaque sous-commande?

Code actuel:

$ python sites.py subcommand_one
Usage: sites.py [OPTIONS] [SITES] COMMAND [ARGS]...
Try "sites.py --help" for help.

Error: Missing command.

Exemple d'invocation:

@click.group()
@click.argument('sites', nargs=-1)
def cli(sites):
    if sites:
        site_list = sites

J'ai essayé de déplacer le décorateur d'arguments vers le groupe comme ceci:

$ python sites.py subcommand_one www.example.com www.example2.com

Mais alors j'obtiendrais cette erreur:

from config import site_list

@click.group()
def cli():
    pass

@cli.command()
@cli.argument('sites', nargs=-1)
def subcommand_one():
    if sites:
        site_list = sites
    etc...

@cli.command()
@cli.argument('sites', nargs=-1)
def subcommand_two():
    if sites:
        site_list = sites
    etc...


0 commentaires

3 Réponses :


3
votes

click.argument renvoie simplement un décorateur comme n'importe quel autre, vous pouvez donc l'assigner à une variable:

import click

@click.group()
def cli():
    pass

sites_argument = click.argument('sites', nargs=-1)

@cli.command()
@sites_argument
def subcommand_one(sites):
    ...

@cli.command()
@sites_argument
def subcommand_two(sites):
    ...


2 commentaires

Je veux rendre cet argument facultatif et fournir une valeur par défaut, mais lorsque j'essaye sites_argument = click.argument ('sites', nargs = -1, default = site_list) j'obtiens cette erreur TypeError : nargs = -1 en combinaison avec une valeur par défaut n'est pas pris en charge. Quelle est la meilleure façon d'attribuer une valeur par défaut ici?


Vous pouvez soit ne pas avoir de valeur par défaut et tester à l'intérieur de votre fonction une liste vide, soit utiliser un rappel.



1
votes

S'il y a un argument nargs = -1 spécifique que vous souhaitez décorer uniquement sur le groupe, mais applicable à toutes les commandes au besoin, vous pouvez le faire avec une plomberie supplémentaire comme:

Cette réponse est inspirée de ceci une> réponse.

Classe personnalisée

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> command_one site1 site2
cli group
command_one: ('site1', 'site2')
-----------
> command_one site1
cli group
command_one: ('site1',)
-----------
> command_one
cli group
command_one: ()
-----------
> subcommand
cli group
Usage: test.py subcommand [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  one
  two
-----------
> subcommand one site1 site2
Usage: test.py subcommand one [OPTIONS]

Error: Got unexpected extra arguments (site1 site2)
cli group
subcommand group
-----------
> subcommand one site1
cli group
subcommand group
Usage: test.py subcommand one [OPTIONS]

Error: Got unexpected extra argument (site1)
-----------
> subcommand one
cli group
subcommand group
subcommand_one
-----------
> subcommand two site1 site2
cli group
subcommand group
subcommand_two: ('site1', 'site2')
-----------
> subcommand two site1
cli group
subcommand group
subcommand_two: ('site1',)
-----------
> subcommand two
cli group
subcommand group
subcommand_two: ()
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  command_one
  subcommand
-----------
> command_one --help
cli group
Usage: test.py command_one [OPTIONS] [SITES]...

Options:
  --help  Show this message and exit.
-----------
> subcommand --help
cli group
Usage: test.py subcommand [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  one
  two
-----------
> subcommand one --help
cli group
subcommand group
Usage: test.py subcommand one [OPTIONS]

Options:
  --help  Show this message and exit.
-----------
> subcommand two --help
cli group
subcommand group
Usage: test.py subcommand two [OPTIONS] [SITES]...

Options:
  --help  Show this message and exit.
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  command_one
  subcommand

Utilisation de la classe personnalisée:

Pour utiliser la classe personnalisée, transmettez le cls au décorateur click.group () , utilisez le @ GroupNArgsForCommands.command_argument pour l'argument spécial, puis ajoutez un paramètre du même nom que l'argument spécial de toute commande selon les besoins.

import click

@click.group(cls=GroupNArgsForCommands)
@GroupNArgsForCommands.command_argument('sites', nargs=-1)
def cli():
    click.echo("cli group")

@cli.command()
def command_one(sites):
    click.echo("command_one: {}".format(sites))

@cli.group()
def subcommand():
    click.echo("subcommand group")

@subcommand.command()
def one():
    click.echo("subcommand_one")

@subcommand.command()
def two(sites):
    click.echo("subcommand_two: {}".format(sites))

if __name__ == "__main__":
    commands = (
        'command_one site1 site2',
        'command_one site1',
        'command_one',
        'subcommand',
        'subcommand one site1 site2',
        'subcommand one site1',
        'subcommand one',
        'subcommand two site1 site2',
        'subcommand two site1',
        'subcommand two',
        '--help',
        'command_one --help',
        'subcommand --help',
        'subcommand one --help',
        'subcommand two --help',
        '',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for command in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + command)
            time.sleep(0.1)
            cli(command.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Comment cela fonctionne?

Cela fonctionne car cliquez sur est un framework OO bien conçu. Le décorateur @ click.group () généralement instancie un objet click.Group mais permet de surcharger ce comportement avec le paramètre cls .
Il est donc relativement facile d’hériter de click.Group dans notre propre classe et de remplacer les méthodes souhaitées.

Dans ce cas, nous surpassons click.Group.add_command () de sorte que lorsqu'une commande est ajoutée, nous pouvons examiner les paramètres de rappel de commande pour voir s'ils ont le même nom que l'un de nos arguments spéciaux. S'ils correspondent, l'argument est ajouté aux arguments de la commande comme s'il avait été décoré directement.

De plus, GroupNArgsForCommands implémente un command_argument () méthode. Cette méthode est utilisée comme un décorateur lors de l'ajout de l'argument spécial au lieu d'utiliser click.argument()

Classe de test

@click.group(cls=GroupNArgsForCommands)
@GroupNArgsForCommands.command_argument('special', nargs=-1)
def a_group():
    """My project description"""

@a_group.command()
def a_command(special):
    """a command under the group"""

Résultats: h3>
class GroupNArgsForCommands(click.Group):
    """Add special arguments on group"""

    def __init__(self, *args, **kwargs):
        super(GroupNArgsForCommands, self).__init__(*args, **kwargs)
        cls = GroupNArgsForCommands.CommandArgument

        # gather the special arguments for later
        self._cmd_args = {
            a.name: a for a in self.params if isinstance(a, cls)}

        # strip out the special arguments from self
        self.params = [a for a in self.params if not isinstance(a, cls)]

    class CommandArgument(click.Argument):
        """class to allow us to find our special arguments"""

    @staticmethod
    def command_argument(*param_decls, **attrs):
        """turn argument type into type we can find later"""

        assert 'cls' not in attrs, "Not designed for custom arguments"
        attrs['cls'] = GroupNArgsForCommands.CommandArgument

        def decorator(f):
            click.argument(*param_decls, **attrs)(f)
            return f

        return decorator

    def group(self, *args, **kwargs):
        # any derived groups need to be the same type
        kwargs['cls'] = GroupNArgsForCommands

        def decorator(f):
            grp = super(GroupNArgsForCommands, self).group(
                *args, **kwargs)(f)
            self.add_command(grp)

            # any sub commands need to hook the same special args
            grp._cmd_args = self._cmd_args

            return grp

        return decorator

    def add_command(self, cmd, name=None):

        # call original add_command
        super(GroupNArgsForCommands, self).add_command(cmd, name)

        # if this command's callback has desired parameters add them
        import inspect
        args = inspect.signature(cmd.callback)
        if len(args.parameters):
            for arg_name in reversed(list(args.parameters)):
                if arg_name in self._cmd_args:
                    cmd.params[:] = [self._cmd_args[arg_name]] + cmd.params


1 commentaires

C'est la meilleure réponse à ce que j'ai demandé. Cependant, la réponse de L3viathan pourrait en fait être une meilleure solution pour moi. J'essaie de comprendre comment je pourrais fournir une valeur par défaut pour l'argument dans sa solution.



0
votes

Je pense qu'il existe une solution réelle prise en charge par Click en utilisant le @ click.pass_context .

Lorsque vous souhaitez définir un groupe de commandes qui partagent toutes par exemple un argument commun et un commun option, vous pouvez les définir au niveau du groupe et les ajouter à un objet de contexte comme décrit dans la documentation Click .

@cli.command("create")
@click.pass_context
def create(ctx):
    create_semantics_json_from_csv(ctx.obj["DIRECTORY"], ctx.obj["SPLITS"])


@cli.command("tokenize")
@click.pass_context
def tokenize(ctx):
    preprocess_tokenize_semantics_json(ctx.obj["DIRECTORY"], ctx.obj["SPLITS"])

Ensuite, les commandes individuelles de ce groupe peuvent obtenir le contexte et utiliser les valeurs de l'objet de contexte au lieu de définir ses propres arguments et options.

@click.group(chain=True)
@click.argument("dataset_directory", type=click.Path(exists=True))
@click.option("-s", "--split-names", help="The splits to preprocess.", required=True,
              default=["trainset", "devset", "testset"], show_default=True)
@click.pass_context
def cli(ctx, dataset_directory, split_names):
    """
    Prepare the dataset for training

    DATASET_DIRECTORY The absolute path to the data directory.
    """
    ctx.ensure_object(dict)
    ctx.obj["DIRECTORY"] = dataset_directory
    ctx.obj["SPLITS"] = split_names

L'appel des commandes est alors possible comme:

my-cli -app / chemin / vers / data crée tokenize


0 commentaires