1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
.. _setuptools-integration:
Setuptools Integration
======================
When writing command line utilities, it's recommended to write them as
modules that are distributed with setuptools instead of using Unix
shebangs.
Why would you want to do that? There are a bunch of reasons:
1. One of the problems with the traditional approach is that the first
module the Python interpreter loads has an incorrect name. This might
sound like a small issue but it has quite significant implications.
The first module is not called by its actual name, but the
interpreter renames it to ``__main__``. While that is a perfectly
valid name it means that if another piece of code wants to import from
that module it will trigger the import a second time under its real
name and all of a sudden your code is imported twice.
2. Not on all platforms are things that easy to execute. On Linux and OS
X you can add a comment to the beginning of the file (``#!/usr/bin/env
python``) and your script works like an executable (assuming it has
the executable bit set). This however does not work on Windows.
While on Windows you can associate interpreters with file extensions
(like having everything ending in ``.py`` execute through the Python
interpreter) you will then run into issues if you want to use the
script in a virtualenv.
In fact running a script in a virtualenv is an issue with OS X and
Linux as well. With the traditional approach you need to have the
whole virtualenv activated so that the correct Python interpreter is
used. Not very user friendly.
3. The main trick only works if the script is a Python module. If your
application grows too large and you want to start using a package you
will run into issues.
Introduction
------------
To bundle your script with setuptools, all you need is the script in a
Python package and a ``setup.py`` file.
Imagine this directory structure:
.. code-block:: text
yourscript.py
setup.py
Contents of ``yourscript.py``:
.. click:example::
import click
@click.command()
def cli():
"""Example script."""
click.echo('Hello World!')
Contents of ``setup.py``:
.. code-block:: python
from setuptools import setup
setup(
name='yourscript',
version='0.1.0',
py_modules=['yourscript'],
install_requires=[
'Click',
],
entry_points={
'console_scripts': [
'yourscript = yourscript:cli',
],
},
)
The magic is in the ``entry_points`` parameter. Read the full
`entry_points <https://packaging.python.org/en/latest/specifications/entry-points/>`_
specification for more details. Below ``console_scripts``, each
line identifies one console script. The first part before the
equals sign (``=``) is the name of the script that should be
generated, the second part is the import path followed by a colon
(``:``) with the Click command.
That's it.
Testing The Script
------------------
To test the script, you can make a new virtualenv and then install your
package:
.. code-block:: console
$ python3 -m venv .venv
$ . .venv/bin/activate
$ pip install --editable .
Afterwards, your command should be available:
.. click:run::
invoke(cli, prog_name='yourscript')
Scripts in Packages
-------------------
If your script is growing and you want to switch over to your script being
contained in a Python package the changes necessary are minimal. Let's
assume your directory structure changed to this:
.. code-block:: text
project/
yourpackage/
__init__.py
main.py
utils.py
scripts/
__init__.py
yourscript.py
setup.py
In this case instead of using ``py_modules`` in your ``setup.py`` file you
can use ``packages`` and the automatic package finding support of
setuptools. In addition to that it's also recommended to include other
package data.
These would be the modified contents of ``setup.py``:
.. code-block:: python
from setuptools import setup, find_packages
setup(
name='yourpackage',
version='0.1.0',
packages=find_packages(),
include_package_data=True,
install_requires=[
'Click',
],
entry_points={
'console_scripts': [
'yourscript = yourpackage.scripts.yourscript:cli',
],
},
)
|