Click: detection of program name when run with `python -m`

Created on 25 Jun 2020  路  5Comments  路  Source: pallets/click

The program name (referred to as prog_name or info_name) is the name Click uses in usage help text. If it's not specified, Click tries to detect it by looking at sys.argv[0].

https://github.com/pallets/click/blob/dfb842d95a7c146d8fe8140c8100065410d95c4d/src/click/core.py#L797-L800

This works when the CLI is run as a script, either directly, or indirectly as an entry point, or as a module when the program is a single file, but fails when the program is a package and run with python -m. In that case, Python converts the -m name to the Python file within the package that would be executed. (The use of __file__ there is also incorrect, as that will always point to click/core.py. There should never be a case where sys.argv is empty. make_str is probably not needed either.)

This leads to the following names:

  • python example.py, executing a script directly - example.py
  • python -m example, the single file module example.py - example.py
  • python -m example, a package like example/__main__.py - __main__.py
  • python -m example.cli, a submodule like example/cli.py - cli.py
  • example, an entry point, with any of the layouts above - example

What's usually expected is that executing a script will print the Python file name (example.py), executing an entry point will print the entry point name (example), and executing a module or package will print the Python command (python -m example, python -m example.cli)

This leads to projects that expect to be run as either an entry point or -m writing their own wrapper around the CLI to control what name gets used. For example, Flask uses a wrapper that the entry point will call with no args, while __main__.py or __name__ == "__main__" will pass as_module=True.

def main(as_module=False):
    cli.main(prog_name="python -m flask" if as_module else None)

Unfortunately, detecting whether python -m was used is really, really complicated. Luckily, I did the work already in Werkzeug's reloader: https://github.com/pallets/werkzeug/blob/102bcda52176db448d383a9ef1ccf7e406a379eb/src/werkzeug/_reloader.py#L59. It won't be quite the same in Click because we're not concerned with the exact path to the python command or script file, or with arguments, but that should be a good start. Write a function detect_program_name() to detect this and call it in place of the existing code.

Most helpful comment

Here's a simple project to test out the commands with. example.tar.gz

$ tar xf example.tar.gz
$ cd example
$ python3 -m venv venv
$ . venv/bin/activate
$ pip install -e ../click  # wherever your click repo is
$ pip install -e .

Now you can do different commands and changes you make to your click repo will be reflected. The command echos the detected program name.

$ python -m example
$ python -m example.cli
$ example
$ python single.py
$ python -m single

All 5 comments

I'll look into this!

One more note, there is Windows-specific behavior here, so remember to test the fix against the different cases listed above on both Linux/Mac and Windows.

Here's a simple project to test out the commands with. example.tar.gz

$ tar xf example.tar.gz
$ cd example
$ python3 -m venv venv
$ . venv/bin/activate
$ pip install -e ../click  # wherever your click repo is
$ pip install -e .

Now you can do different commands and changes you make to your click repo will be reflected. The command echos the detected program name.

$ python -m example
$ python -m example.cli
$ example
$ python single.py
$ python -m single

I'll look into this!

One more note, there is Windows-specific behavior here, so remember to test the fix against the different cases listed above on both Linux/Mac and Windows.

I was reading the issue and saw that there may be a need for testing on windows. If you need someone with a windows machine, let me know. I know a lot of people use linux/mac.

@ovezovs thanks! @kbanc set up a windows VM and got it done :)

Was this page helpful?
0 / 5 - 0 ratings