$ ls
links published categories alts content
$ cat links
- Home
- Author
$ cat published
$ cat categories
- Python
- Reference
$ cat alts
- Gopher Mirror
- text/plain; width=40
- text/plain; width=72
- text/plain; width=120
- application/x-troff
$ cat content
The setup.py Guide I Wish I had
________________________________________________________________________
I'm a big proponent of using all the built-in tools in Python. This
manifests in weird ways: I like the http.server module; I abuse multi-
processing.Manager; and I like using pure distutils.
The latter gives me problems sometimes because no one seems to use it
like I do. I often find myself getting lost in Python's lackluster doc-
umentation for this module trying to just find the name of the argument
I need. It's starting to get a little silly how often and how long I
spend on this problem.
Of course, one solution to this problem is "just use an IDE with auto-
completion." Another is "just use setuptools which has better documen-
tation." For various reasons, I prefer to use simpler editors and also
to use standard library functions.
To that end, this document is a consolidation of the things I'm usually
looking for. It mostly serves as a reference for myself, but maybe it
will be helpful to someone else.
Before the document gets into the details, here are some links that are
helpful.
API documentation for distutils.core.setup
[0]: https://docs.python.org/3/distutils/apiref.html
Official examples for distutils.core.setup
[1]: https://docs.python.org/3/distutils/setupscript.html
List of valid classifier values
[2]: https://pypi.org/classifiers/
API documentation for pkgutil.get_data
[3]: https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
The Simplest Script
________________________________________________________________________
If you are creating a package called MYPACKAGE, your script would look
like this.
--------8<--------------------------------------------------------------
from distutils.core import setup
setup(
name='MYPACKAGE',
version='0.1.0',
packages=[
'MYPACKAGE',
],
)
-------->8--------------------------------------------------------------
You should have a directory called MYPACKAGE with at least an
__init__.py file inside.
--------8<--------------------------------------------------------------
/
/setup.py
/MYPACKAGE/
/MYPACKAGE/__init__.py
-------->8--------------------------------------------------------------
Adding Dependencies
________________________________________________________________________
If you need certain dependencies to be installed, you can specify them
with the requires keyword. In this example, we require at least version
2.20.0 of requests.
--------8<--------------------------------------------------------------
from distutils.core import setup
setup(
name='MYPACKAGE',
version='0.1.0',
packages=[
'MYPACKAGE',
],
requires=[
'requests>=2.20.0',
],
)
-------->8--------------------------------------------------------------
Adding an Executable
________________________________________________________________________
Edit: Oops. This one does actually depend on setuptools. If you in-
stall the package with "pip install ." then it will automatically use
setuptools for you, hence my confusion.
Sometimes you aren't trying to just create a library but also need an
actual executable. Somewhat confusingly, in distutils land, this is
called an "entry point." If you want an executable called MYEXEC for
the package MYPACKAGE, then you'll want this.
--------8<--------------------------------------------------------------
# File: setup.py
from distutils.core import setup
setup(
name='MYPACKAGE',
version='0.1.0',
packages=[
'MYPACKAGE',
],
entry_points={
'console_scripts': [
'MYEXEC=MYPACKAGE.__main__:cli',
],
},
)
-------->8--------------------------------------------------------------
You should have a __main__.py script with a cli function inside. A sim-
ple __main__.py script looks like:
--------8<--------------------------------------------------------------
# File: __main__.py
from . import hello
def main(name):
print(f'The output of hello({name!r}) is {hello(name)!r}')
def cli():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('name')
args = vars(parser.parse_args())
main(**args)
if __name__ == '__main__':
cli()
-------->8--------------------------------------------------------------
The corresponding __init__.py might look like:
--------8<--------------------------------------------------------------
# File: __init__.py
def hello(name):
return name.upper()
-------->8--------------------------------------------------------------
Now after you install the package, you can use MYEXEC as a normal script
and pass it arguments like: MYEXEC George.
The directory structure here is:
--------8<--------------------------------------------------------------
/
/setup.py
/MYPACKAGE/
/MYPACKAGE/__init__.py
/MYPACKAGE/__main__.py
-------->8--------------------------------------------------------------
Changing the Package Directory
________________________________________________________________________
Sometimes you want to put your code in a different directory than Python
expects. By default, Python wants MYPACKAGE to be located at ./MYPACK-
AGE, but you can change this to look at ./src/MYPACKAGE or any other di-
rectory instead.
--------8<--------------------------------------------------------------
# File: setup.py
from distutils.core import setup
setup(
name='MYPACKAGE',
version='0.1.0',
packages=[
'MYPACKAGE',
],
package_dir={
'MYPACKAGE': 'src/MYPACKAGE',
},
)
-------->8--------------------------------------------------------------
The directory structure now looks like:
--------8<--------------------------------------------------------------
/
/setup.py
/src/
/src/MYPACKAGE/
/src/MYPACKAGE/__init__.py
-------->8--------------------------------------------------------------
Including Extra Data Files
________________________________________________________________________
You may want to include some extra files with your package. For in-
stance, I like to include an index.html page with my web server pack-
ages.
--------8<--------------------------------------------------------------
# File: setup.py
from distutils.core import setup
setup(
name='MYPACKAGE',
version='0.1.0',
packages=[
'MYPACKAGE',
],
package_data={
'MYPACKAGE': [
'static/*',
],
},
)
-------->8--------------------------------------------------------------
Now any file in the static directory in your package will be included.
--------8<--------------------------------------------------------------
/
/setup.py
/MYPACKAGE/
/MYPACKAGE/__init__.py
/MYPACKAGE/static/
/MYPACKAGE/static/index.html
-------->8--------------------------------------------------------------
To retrieve this file at runtime, you can use pkgutil.get_data
--------8<--------------------------------------------------------------
# File: __init__.py
import pkgutil
index_html = pkgutil.get_data('MYPACKAGE', 'static/index.html')
print(type(index_html))
# => bytes
-------->8--------------------------------------------------------------
A More Complete Script
________________________________________________________________________
There's lots of parameters, but I suspect that the ones that are most
useful for actually distributing a package are as follows.
--------8<--------------------------------------------------------------
# File: setup.py
from distutils.core import setup
long_description = """\
This is the long description for my package.
It can be pretty long.
You can either use reStructuredText or GitHub Flavored Markdown.
"""
setup(
name='MYPACKAGE',
version='0.1.0',
description="One line description",
long_description=long_description,
author='John Smith',
author_email='johnsmith@example.com',
url='https://github.com/example/MYPACKAGE',
license='MIT',
keywords=[
'cool',
'useful',
'whatever',
],
classifiers=[
'Development Status :: 1 - Planning',
'Programming Language :: Python :: 3.8',
],
packages=[
'MYPACKAGE',
],
requires=[
'requests>=2.20.0',
],
)
-------->8--------------------------------------------------------------