Bundle a CLI script in your Python project

08 October 2017

## Introduction

A few days ago, I was walking through the code base of the Flask Python microframework and I stumbled upon a __main__.py file inside the flask module.

After I look it up in the Python doc, I decided to write this short post about different ways of creating CLI feature inside a Python project.

This article is not focused on how to handle arguments and options in the script (things like argparse, getopt and so on) but how to provide the executable CLI script.

First solution : a basic python script

A python script can become an executable easily by leveraging the Shebang feature under a UNIX / Linux system.

For example, create a Python script basicpythonscript (without the py extension to hide the fact that it is based on Python) :

#!/usr/bin/env python

import sys

if __name__ == "__main__":
    print(sys.version)

Make it executable : chmod a+x basicpythonscript, put it in your PATH. You can now execute it from anywhere you want :

$ basicpythonscript
2.7.12+ (default, Sep 17 2016, 12:08:02)
[GCC 6.2.0 20160914]

Beware of the Python interpreter that would be used because of the env Shebang. Most recent distributions are switching to Python 3.

On a side note, this line if __name__ == "__main__": ensures that the following code block is only executed when you are in the top level execution scope (read from stdout, executed as a script or in the interactive terminal). So it won’t be executed if you import this python file as a module in another part of your program.

Second solution : python -m

This is the solution that prompt me to write this article. It is explained in the Python documentation : main — Top-level script environment.

Any module or package can be executed from the python interpreter with the command python -m <module or package name>. The module or package needs to be in Python sys.path to be found and on execution, the __name__ of the executing module will be set to __main__

With a module

Create a basicpythonmodule.py file with this content :

#!/usr/bin/env python

import sys

if __name__ == "__main__":
    print(sys.version)

Then :

$ python -m basicpythonmodule
2.7.12+ (default, Sep 17 2016, 12:08:02)
[GCC 6.2.0 20160914]

With a package

Let’s create a python package and execute it using the -m interpreter option:

$ mkdir basicpythonpackage
$ echo "print('__init__ executed')" > basicpythonpackage/__init__.py
$ python -m basicpythonpackage
__init__ executed
/usr/bin/python: No module named basicpythonpackage.__main__; 'basicpythonpackage' is a package and cannot be directly executed

We are missing something here when we use a package instead of a module. The documentation says : For a package, the same effect can be achieved by including a main.py module, the contents of which will be executed when the module is run with -m.

So that’s where the __main__.py file comes from in the Flask project. Let’s try it for our basic package.

$ cat >basicpythonpackage/__main__.py <<EOL
import sys

if __name__ == "__main__":
    print(sys.version)
EOL

$ python -m basicpythonpackage
__init__ executed
2.7.12+ (default, Sep 17 2016, 12:08:02)
[GCC 6.2.0 20160914]

Now it works.

Third solution : setuptools console scripts

Setuptools provides a feature to generate script automatically. It is provided as a key console_scripts or gui_scripts in the entry_points section of the setup call in your project setup.py.

Each console script is defined with a string that follows this convention :

<name of the executable> = <fullpath to a python module>:<name of the function to execute in the module>

An example is worth all explanation, let’s continue to work with our previous basicpythonpackage:

$ cat >setup.py <<EOL
from setuptools import setup, find_packages
setup(
    name="basicpythonpackage",
    version="0.1",
    packages=find_packages(),
    entry_points={
        'console_scripts': [
            'foo = basicpythonpackage.foo:my_func'
        ]
    }
)
EOL

$ cat >basicpythonpackage/foo.py <<EOL
import sys

def my_func(*args, **kwargs):
   print('args', args)
   print('kwargs', kwargs)
EOL

$ pip install .
Processing ...
Installing collected packages: basicpythonpackage
  Running setup.py install for basicpythonpackage ... done
Successfully installed basicpythonpackage-0.1

$ foo
__init__ executed
('args', ())
('kwargs', {})

$ which foo
/usr/local/bin/foo

$ cat /usr/local/bin/foo
#!/usr/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'basicpythonpackage==0.1','console_scripts','foo'
__requires__ = 'basicpythonpackage==0.1'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
  sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
  sys.exit(
      load_entry_point('basicpythonpackage==0.1', 'console_scripts', 'foo')()
  )

As you can see, we got a global executable command foo. On execution, it loads the python function you configured, executes it without any arguments and exits with a status code equal to the return of this function.

Note : as it writes to /user/local/bin, the pip install command has to be executed as root or with a sudo. You can run it in a virtualenv if you want, the script will be available in the bin folder of the virtualenv.

I invite you to read the full documentation because this feature is much more powerful and complete : Setuptools Automatic Script Creation

Conclusion

We went over 3 ways to create CLI entry point in a Python program. My preference goes to the setuptools solution, it seems cleaner, the executable is available in the system without indicating it is a Python script and it can work with Windows too (if needed).

If you know of any other way, don’t hesitate to reach out to me in order to update this article.