2

I would like to implement each subcommand in a different file for a better clarity.
Right now I have only one but the idea will be to add more with the time.

For that I tried 2 ways and it ended with a big failure...

Basically I try to have this result:

$ test_cli
Usage: test_cli [OPTIONS] COMMAND [ARGS]...

  Cli command

Options:
  --help  Show this message and exit.

Commands:
  test  Test command.


$ test_cli test hello
Hello !

Here are files

$ tree
.
├── cli.py
├── setup.py
└── test.py

I'm using virtualenv and I use the following command to test my application:

$ pip install --editable .

The code of setup.py is the same for both :

from setuptools import setup

setup(
    name = 'test_cli',
    version = '1.0',
    py_modules = [ 'cli', 'test' ],
    install_requires = [
        'Click',
    ],
    entry_points = '''
        [console_scripts]
        test_cli=cli:cli
    ''',
)

Try 1 - FAILURE

Code based on this link, but it did not work with me...

Here is the code of each file:

cli.py

import click
import test

@click.group()
def cli():
    ''' Cli command '''
    pass

cli.add_command(test)

test.py

import click


@click.group()
def test():
    ''' Test command. '''
    pass


@test.command()
def hello():
    click.echo('Hello !')

Here is the error I have :

Traceback (most recent call last):
  File "/tmp/myenv/bin/test_cli", line 33, in <module>
    sys.exit(load_entry_point('test-cli', 'console_scripts', 'test_cli')())
  File "/tmp/myenv/bin/test_cli", line 25, in importlib_load_entry_point
    return next(matches).load()
  File "/tmp/myenv/lib/python3.6/site-packages/importlib_metadata/__init__.py", line 105, in load
    module = import_module(match.group('module'))
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/tmp/toto/cli.py", line 9, in <module>
    cli.add_command(test)
  File "/tmp/myenv/lib/python3.6/site-packages/click/core.py", line 1347, in add_command
    name = name or cmd.name
    AttributeError: module 'test' has no attribute 'name'

Try 2 - FAILED

This time I found a code here. I don't have any error but I cannot execute any subcommand :-/

cli.py

import click
import test

@click.group()
def cli():
    ''' Cli command '''
    pass

import click import cli

test.py

import click
import cli

@cli.group()
def test():
    ''' Test command. '''
    pass

@test.command()
def hello():
    click.echo('Hello !')

When I execute try to execute the subcommand I have this issue:

$ test_cli test hello
Usage: test_cli [OPTIONS] COMMAND [ARGS]...
Try 'test_cli --help' for help.

Error: No such command 'test'.

Any idea of the issue ?
Thank you.

Vince
  • 29
  • 3
  • in your first try, you are adding the calling `add_command(test)`, but `test` is the `test` **module**, not the `test` **command - that would be `test.test` (the `test` attribute of the `test` module, which is the command. In other words, try using `cli.add_command(test.test)` :) – iodbh Oct 14 '20 at 07:27

2 Answers2

1

The easiest way to do this is to implement a separate .py file for your CLI and import the functions into it. I've implemented this in my own program with:

console.py

import click
from .main import store, retrieve

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

@cli.command(no_args_is_help=True)
(series of @click.arguments)
def store(application, key, expiration, userdata):
    # sends the arguments directly to the "store" function in main.py
    store(application,key,expiration,userdata)

(other commands)
if __name__ == '__main__':
    cli()

and in main.py

def store(app, key, expiration, userdata):
    """Program for a user to submit a user-generated API key to their database."""
    # The actual function doing its work, in a separate file

This method separates your CLI setup from the actual code, while allowing full access to all of clicks functions including groups, subcommands, etc. It also allows you to call commands from many different modules or even calling external commands within your CLI (for example, if you want a CLI that conglomerates multiple functions from various packages, setting aside the discussion over whether that is appropriate behavior or not).

Chris
  • 126
  • 1
  • 2
  • 9
0

In your first example you need to import the test group into your module.

cli.py

import click
from test import test 


@click.group()
def cli():
    ''' Cli command '''
    pass


cli.add_command(test)

if __name__ == '__main__':
    cli()

test.py

import click


@click.group()
def test():
    ''' Test command. '''
    pass


@test.command()
def hello():
    click.echo('Hello !')
nad87563
  • 3,672
  • 7
  • 32
  • 54