37

I have built a cli application using the click library in python. There is no documentation on how to debug commands.

Without click, its easy to just debug python files in IDE, but when we use click, the commands need to be run through console_scripts setup in setup.py.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
vishal
  • 3,993
  • 14
  • 59
  • 102

6 Answers6

48

This is not well documented, but you can call your command functions directly, and thus can run the code in a debugger:

Sample Code:

import click

@click.command()
@click.option('--my_arg', default=1, help='a number')
def my_command(my_arg):
    click.echo("my_arg='%d'" % my_arg)

if __name__ == '__main__':
    my_command(['--my_arg', '3'])

Result:

my_arg='3'
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
  • How to pass multiple arguments? Especially if one of them is a flag? What if there is another @click.option("--full", is_flag=True). What would be the correct syntax for it to be passed to my_command? If I add another array with ["--full", True] the execution fails. I would need this in order to debug. – Alen Siljak Feb 17 '18 at 21:32
  • 1
    It is just a list. Basically, think like cmdlinestr.split() – Stephen Rauch Feb 18 '18 at 01:32
  • Stephen, thanks! Lint complains that the second parameter is missing and that was misleading, but it works! – Alen Siljak Feb 18 '18 at 13:00
  • Does this invocation remain trivial if the click application is using subcommands and other advanced patterns? The need to debug within an IDE correlates to developing non-trivial applications. – Jordan Stefanelli Feb 28 '19 at 12:09
  • @JordanStefanelli, Yes, it stays the same. – Stephen Rauch Feb 28 '19 at 13:54
  • Taking the approach from the answer and provided that you add the `import sys`, the call to the click Command in the last conditional, by simply adding `sys.argv[1:]` as parameter, command would receive the complete set of parameters as it had been invoked from the shell. – blamblam Jun 19 '19 at 09:47
  • 1
    @blamblam, This is test code. Much better to be complete here... Additionally, in an actual program you can leave the `sys.argv[1:]` off completely and click will do that for you. – Stephen Rauch Jun 19 '19 at 13:20
  • I'm using Typer and I focused on the decorators so much that I didn't notice you're just directly calling your function in the __main__ condition. Obviously, this also works for Typer –  Oct 14 '21 at 17:07
8

I'm a couple years late to the party, but in case anyone else comes here looking for an answer like I just did:

Calling your function with CliRunner.invoke() will return a "Result" object with an "exc_info" attribute. You can feed that to traceback.print_exception() like so:

runner = CliRunner()
result = runner.invoke(my_command)
traceback.print_exception(*result.exc_info)
Jacob Jurmain
  • 81
  • 1
  • 2
4

The setup.py generates:

  • console_script.exe
  • console_script-script.py

from cmdline:

console_app --help

expands out to the IDE configuration cmd:

python <absolute path to>\console_app-script.py --help

Tested in PyCharm 2018.2 - you can set and hit breakpoints and preserve the expected cmdline/arg paradigm.

Jordan Stefanelli
  • 1,446
  • 1
  • 13
  • 13
3

I have found that writing tests is a great way to debug Click CLI applications: http://click.pocoo.org/5/testing/

Start really simple with your function and a test, then add to it, making sure the test tells you what you need it to...

Also, setting defaults helps:

def run_files(input_file='/path/to/input_file', output_file='/path/to/output_file'): click.echo(input_file, output_file)

I also generally set up logging and log everything when I begin:

logging.basicConfig(format='%(levelname)s %(message)s', level=logging.DEBUG)

Then sprinkle these throughout (IU have utilities with timestamp stuff, but this is not required):

logging.info('[{0}] Blah blah selected...'.format(
             utils.get_timestamp()))

You can do this with print or click.echo too.

nicorellius
  • 3,715
  • 4
  • 48
  • 79
3

You can use pdb like this, put inside the code you want to debug:

import pdb
pdb.set_trace()

For example:

import click

@click.command()
def hello():
    msg = "hi"
    num = 3

    import pdb
    pdb.set_trace()

    click.echo('Hello1!')
    click.echo('Hello2!')
    click.echo(msg + num)

if __name__ == '__main__':
    hello()

Then you can use pdb:

$ python hello.py 
> /home/eduardo/w/ifpb/cozer/hello.py(11)hello()
-> click.echo('Hello1!')
(Pdb) msg
'hi'
(Pdb) num
3
(Pdb) msg = hello
(Pdb) b 13
Breakpoint 1 at /home/eduardo/w/ifpb/cozer/hello.py:13
(Pdb) c
Hello1!
Hello2!
> /home/eduardo/w/ifpb/cozer/hello.py(13)hello()
-> click.echo(msg + num)
(Pdb) c
Traceback (most recent call last):
  File "hello.py", line 16, in <module>
    hello()
  File "/home/eduardo/w/ifpb/cozer/venv/lib/python3.5/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/home/eduardo/w/ifpb/cozer/venv/lib/python3.5/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/home/eduardo/w/ifpb/cozer/venv/lib/python3.5/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/eduardo/w/ifpb/cozer/venv/lib/python3.5/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "hello.py", line 13, in hello
    click.echo(msg + num)
TypeError: unsupported operand type(s) for +: 'Command' and 'int'

After the error we can try again a solution:

$ python hello.py 
> /home/eduardo/w/ifpb/cozer/hello.py(11)hello()
-> click.echo('Hello1!')
(Pdb) msg
'hi'
(Pdb) num
3
(Pdb) b 13
Breakpoint 1 at /home/eduardo/w/ifpb/cozer/hello.py:13
(Pdb) c
Hello1!
Hello2!
> /home/eduardo/w/ifpb/cozer/hello.py(13)hello()
-> click.echo(msg + num)
(Pdb) num = str(num)
(Pdb) c
hi3
Eduardo Santana
  • 5,780
  • 3
  • 19
  • 21
-2

If you are looking to debug the modules/classes that are executed after click command functions.

An affordable solution is to create a script that simulates the flow that is performed after executing the command.

Then by placing debug points in the required lines you can achieve the debug functionality.

This is what I figured to do after a brief web search.

Mic
  • 345
  • 1
  • 3
  • 9