Suppose you’ve written a Python package that you want to be able to pip install
locally. Additionally, you also want to be able to run one of the scripts in the package via its name, without the .py
extension and without explicitly using the Python interpreter to launch it. In other words, you want to be able to run
1
standalone_script
instead of
1
python /path/to/my/package/standalone_script.py
This post explains how to achieve this using setuptools
.
A world without setuptools
Suppose the package is a simple one, containing only a readme file and a single module core.py
. There is also going to be a __init__.py
file detailing the imports from core.py
that you would want to have available when you import mypackage
. The starting project structure may look something like this:
1
2
3
4
mypackage/
README.md
__init__.py
core.py
For the purpose of illustration, let us suppose that core.py
contains a function capitalize()
that takes a string and returns it in all caps (uppercase):
1
2
3
4
5
6
# core.py
def capitalize(text):
if not isinstance(text, str):
raise ValueError('Need string to capitalize')
return text.upper()
The __init__.py
file imports the only thing it can, i.e. the capitalize()
function:
1
2
3
# __init__.py
from mypackage.core import capitalize
This way, each time you want to use the mypackage
module, capitalize()
becomes available. But you would need to go through the very tedious and error-prone approach of manually adding the path to mypackage
every time you want to use it:
1
2
3
4
5
6
7
# some_script.py
import sys
sys.path.append('/path/to/mypackage')
from mypackage import capitalize
print(capitalize('this'))
That’s pretty lame, but fortunately we can do better.
Enter setuptools
You can package the project and then install it via pip
locally. Then any script that needs the newly installed package can simply import
it:
1
2
3
4
5
# some_script.py
from mypackage import capitalize
print(capitalize('this'))
In order to achieve this, only two steps are involved:
- reorganize the project structure
- create a
setup.py
file
For the project structure, simply create a subdirectory with the same name as the package name and move modules i.e. .py
files inside it:
1
2
3
4
5
6
mypackage/
README.md
setup.py
mypackage/
__init__.py
core.py
The setup.py
file has the following contents:
1
2
3
4
5
6
7
8
9
10
from setuptools import setup, find_packages
setup(
name='mypackage',
version='1.0.0',
description='Capitalize strings',
author='John Doe',
author_email='doe@example.com',
packages=find_packages(),
)
We can then install the package locally using pip
:
1
pip install -e /path/to/mypackage
The /path/to/mypackage
above refers to the top-level mypackage/
directory.
The package may be uninstalled with:
1
pip uninstall mypackage
Standalone script
What if we wanted a standalone script to be installed along with mypackage
that would run the capitalize()
function on any string that we pass through the command line? Here is how the script would be used:
1
2
3
4
$ capitalize
usage: capitalize [-h] [-v] [string [string ...]]
$ capitalize my text
MY TEXT
In order to achieve this, we need to:
- create the script that runs the
capitalize()
function on the string that gets passed to it via the command line - edit
setup.py
to instruct it how to “install” the script (i.e. how to make it accessible system-wide)
We will be creating the standalone script in a file called __main__.py
that we place in the subdirectory containing the other Python modules:
1
2
3
4
5
6
7
mypackage/
README.md
setup.py
mypackage/
__init__.py
__main__.py
core.py
Then we write the script in the main()
method of __main__.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import argparse
import sys
from mypackage import capitalize
def main():
parser = argparse.ArgumentParser(prog='capitalize')
parser.add_argument('string', nargs='*', help='string to capitalize')
parser.add_argument('-v', '--version', help='display version', action='version',
version=f'%(prog)s 1.0.0')
args = parser.parse_args()
if args.string:
text = ' '.join(word for word in args.string)
print(capitalize(text))
else:
parser.print_usage()
sys.exit(1)
if __name__ == '__main__':
sys.exit(main())
At lines 7-11, we add an argument parser. It can either display the program version (through -v
or --version
) or consume all the command line arguments (nargs='*'
) in order to pass them to the capitalize()
function (lines 13-15).
The only thing left to do now is to point setup.py
to the main()
function of the __main__.py
module and to ask it to add it as a console script “entry point” called capitalize
:
1
2
3
4
5
6
7
from setuptools import setup, find_packages
setup(
name="mypackage",
# [snip]
entry_points = {'console_scripts': ['capitalize = mypackage.__main__:main']},
)
That’s it! Now the package may be installed with pip
as shown above and the capitalize
script becomes available system-wide in the current Python environment. You might want to read the next post for a special tricky situation involving the use of the standalone script as a runner.
Accompanying code
The full code accompanying this post can be found on my GitHub repository.
Comments powered by Disqus.