Adding custom commands to setup.py
I’m often using Setuptools to package
and distribute Python modules. Recently I needeed to add a custom command
to setup.py so I can run it like this:
$ python setup.py mycommand --option --another-option value
The official documentation of Setuptools isn’t very specific on how to do it so here is what I came up with after a little research.
Long story short: We need to subclass the class setuptools.Command and
register it with Setuptools.
Create the command class
So we do not work with an overly artificial example, let’s make a command for
converting *.svg image files into PNGs. Let’s call it gen_images.
-
Create the
GenImagesCommandclass by subclassingsetuptools.Command. Put it directly intosetup.py. -
Initialize the class attribute
user_options. It is a list of tuples where each tuple corresponds to a single command-line option. The tuple consists of the option long name, short name (without the leading ‘–’ and ‘-‘) and description. Options are parsed using thedistutils.fancy_getoptmodule.user_options = [ ('input-dir=', 'i', 'input directory'), ('output-dir=', 'o', 'output directory'), ] -
Implement the method
initialize_options(). It is used to initialize the options to default values.def initialize_options(self): self.input_dir = None self.output_dir = None -
Implement the method
finalize_options(). It is used to check final option values. For example, you may want to check if a pathname exists, compute missing values or process dependencies between options.def finalize_options(self): if self.input_dir is None: raise Exception("Parameter --input-dir is missing") if self.output_dir is None: raise Exception("Parameter --output-dir is missing") if not os.path.isdir(self.input_dir): raise Exception("Input directory does not exist: {0}".format(self.input_dir)) if not os.path.isdir(self.output_dir): raise Exception("Output directory does not exist: {0}".format(self.output_dir)) -
Implement the method
run(). Therun()method does the “hard work” of the command:def run(self): def _gen_images(arg, dirname, fnames): for fname in fnames: if self.verbose: print 'processing "{0}"'.format(os.path.join(dirname, fname)) # FIXME: Really process the files here os.path.walk(self.input_dir, _gen_images, None) -
Optionally you can set the value of
descriptionclass attribute. It is used to describe what the command does when you runpython setup.py --help-commands.
Here is the complete GenImagesCommand class:
import os
from setuptools import Command
class MyCommand(Command):
""" Run my command.
"""
description = 'generate images'
user_options = [
('input-dir=', 'i', 'input directory'),
('output-dir=', 'o', 'output directory'),
]
def initialize_options(self):
self.input_dir = None
self.output_dir = None
def finalize_options(self):
if self.input_dir is None:
raise Exception("Parameter --input-dir is missing")
if self.output_dir is None:
raise Exception("Parameter --output-dir is missing")
if not os.path.isdir(self.input_dir):
raise Exception("Input directory does not exist: {0}".format(self.input_dir))
if not os.path.isdir(self.output_dir):
raise Exception("Output directory does not exist: {0}".format(self.output_dir))
def run(self):
def _gen_images(arg, dirname, fnames):
for fname in fnames:
if self.verbose: # verbose is provided "automagically"
print 'processing "{0}"'.format(os.path.join(dirname, fname))
# FIXME: Really process the file here
os.path.walk(self.input_dir, _gen_images, None)
Register the command in Setuptools
Currently there are two ways to register the command class in the Setuptools framework.
The first one is to add a cmdclass key to the setup() call in setup.py:
setup(
name="my-project",
cmdclass={
'gen_images': GenImagesCommand,
},
# other stuff ...
)
The key is the command name, the value is the command class.
Another way to register the command is to use the entry_points key.
setup(
name="my-project",
entry_points={
'distutils.commands': [
'gen_images = package.module:GenImagesCommand',
],
},
# other stuff ...
)
This states that the command gen_images is implemented by the class
GenImagesCommand from the module package.module. The command class will be
automatically imported by Setuptools when it’s needed. However, in this case the
command class must be stored in a separate module, not directly in setup.py.
Running the command
Now is possible to run the command:
$ python setup.py gen_images --input-dir assets/ --output-dir images/
running gen_images
processing "assets/a.svg"
processing "assets/c.svg"
processing "assets/b.svg"