88

How can I pass a user-defined parameter both from the command line and setup.cfg configuration file to distutils' setup.py script?

I want to write a setup.py script, which accepts my package specific parameters. For example:

python setup.py install -foo myfoo
mkrieger1
  • 19,194
  • 5
  • 54
  • 65

8 Answers8

81

As Setuptools/Distuils are horribly documented, I had problems finding the answer to this myself. But eventually I stumbled across this example. Also, this similar question was helpful. Basically, a custom command with an option would look like:

from distutils.core import setup, Command

class InstallCommand(Command):
    description = "Installs the foo."
    user_options = [
        ('foo=', None, 'Specify the foo to bar.'),
    ]
    def initialize_options(self):
        self.foo = None
    def finalize_options(self):
        assert self.foo in (None, 'myFoo', 'myFoo2'), 'Invalid foo!'
    def run(self):
        install_all_the_things()

setup(
    ...,
    cmdclass={
        'install': InstallCommand,
    }
)
Community
  • 1
  • 1
Cerin
  • 60,957
  • 96
  • 316
  • 522
  • 9
    Hi, what is the command line you run to pass in foo=something ? – Stuart Axon Jan 19 '16 at 17:00
  • 2
    The problem with this is it seems to change the `install` behaviour to the new command. It looks to me like the OP wants to modify the install command so that it accepts a new argument rather than completely replace it. – danio Dec 01 '16 at 11:00
  • 3
    There's something I don't understand here. How does the program know that the string `foo=` is related to the variable `self.foo`? By looking at even more complicated examples, I see that people exchange hyphens with underscores; so something like `input-dir=` becomes `self.input_dir`. How does this all work altogether? I would appreciate explaining this. – The Quantum Physicist Jan 13 '17 at 20:33
  • 2
    This works great for me, but how do you specify multiple `user_options`? How do you extend it? – Ela782 Mar 01 '17 at 18:12
  • 1
    Similar to what @Quantum Physicist asked, how does setuptools treat leading dashes? What if you just want the argument to behave as a flag; does passing it initialize the value to True, or do all arguments need an `=` in order to be properly finalized? Can this work with argument aliases, e.g. `-t` for `--token`? What is the second value in a `user_options` tuple? – Jake Stevens-Haas Mar 25 '20 at 22:05
  • 3
    A lot of these questions are dated, but I found it very instructive to read the source code for `sdist`. https://github.com/python/cpython/blob/master/Lib/distutils/command/sdist.py It answers many of the questions above like multiple arguments etc. @JakeStevens-Haas, second argument is for alternate argument form, so if you wanted to use `-t` and `--token` then you would specify your additional arguments as `('token=', 't', 'Token documentation.')` – dant Apr 30 '20 at 23:14
  • The related documentation can be found [here](https://docs.python.org/3/distutils/apiref.html#creating-a-new-distutils-command) – artu-hnrq Jun 16 '20 at 19:43
37

Here is a very simple solution, all you have to do is filter out sys.argv and handle it yourself before you call to distutils setup(..). Something like this:

if "--foo" in sys.argv:
    do_foo_stuff()
    sys.argv.remove("--foo")
...
setup(..)

The documentation on how to do this with distutils is terrible, eventually I came across this one: the hitchhikers guide to packaging, which uses sdist and its user_options. I find the extending distutils reference not particularly helpful.

Although this looks like the "proper" way of doing it with distutils (at least the only one that I could find that is vaguely documented). I could not find anything on --with and --without switches mentioned in the other answer.

The problem with this distutils solution is that it is just way too involved for what I am looking for (which may also be the case for you). Adding dozens of lines and subclassing sdist is just wrong for me.

totaam
  • 1,306
  • 14
  • 25
  • 7
    This solution is not correct, as --foo could be intended for another command: Using “setup.py build_ext --inplace --foo install”, install should not think it got --foo. – merwok Oct 10 '11 at 15:54
  • 1
    I’m afraid subclassing a command is the only way to add options to a command. However, it is not as hard as commonly thought. – merwok Oct 10 '11 at 15:55
  • 2
    I have no idea why you downvote me for giving an example of what I would like to be able to do. I never claimed this was a solution, so why say this is not correct? I provided pointers to the only documentation I could find on the subject, saying that it is "not as hard as commonly thought" does not help us in finding a better answer. – totaam Oct 13 '11 at 10:34
  • 1
    Sorry, I misread your message and thought you were proposing to look into sys.argv, but you were indeed asking for an equivalent to that. I tried to revert my downvote but SO is not cooperating, as usual :( – merwok Oct 18 '11 at 15:06
  • This is great if you have dynamic `install_requires` that you want to control – Joshua H Jan 23 '19 at 06:21
23

Yes, it's 2015 and the documentation for adding commands and options in both setuptools and distutils is still largely missing.

After a few frustrating hours I figured out the following code for adding a custom option to the install command of setup.py:

from setuptools.command.install import install


class InstallCommand(install):
    user_options = install.user_options + [
        ('custom_option=', None, 'Path to something')
    ]

    def initialize_options(self):
        install.initialize_options(self)
        self.custom_option = None

    def finalize_options(self):
        #print('The custom option for install is ', self.custom_option)
        install.finalize_options(self)

    def run(self):
        global my_custom_option
        my_custom_option = self.custom_option
        install.run(self)  # OR: install.do_egg_install(self)

It's worth to mention that install.run() checks if it's called "natively" or had been patched:

if not self._called_from_setup(inspect.currentframe()):
    orig.install.run(self)
else:
    self.do_egg_install()

At this point you register your command with setup:

setup(
    cmdclass={
        'install': InstallCommand,
    },
    :
Some One
  • 48
  • 5
Ronen Botzer
  • 6,951
  • 22
  • 41
  • 1
    What's the command line you used to pass in the argument? I followed your example and tried: python install --custom_option=xxx but it did not work. Error message is something like "no option custom_option" – Rainfield Mar 11 '16 at 06:16
  • 1
    It is not allowed to have underscore in commands. When changing `custom_option=` to `custom-option` it is possible to use `--custom-option=bar` as parameter. The exact error is `distutils.errors.DistutilsGetoptError: invalid long option name 'custom_option' (must be letters, numbers, hyphens only` – kap Jun 07 '16 at 07:06
  • 3
    It should be noticed, that this InstallCommnad is run after the setup() is called. that means, you can't control anything before the setup() command like building extensions depending on the user-parameters. Anyone knowing how to do so? Like stating if a cython extension should use openmp or not? – MuellerSeb May 03 '19 at 14:14
14

You can't really pass custom parameters to the script. However the following things are possible and could solve your problem:

  • optional features can be enabled using --with-featurename, standard features can be disabled using --without-featurename. [AFAIR this requires setuptools]
  • you can use environment variables, these however require to be set on windows whereas prefixing them works on linux/ OS X (FOO=bar python setup.py).
  • you can extend distutils with your own cmd_classes which can implement new features. They are also chainable, so you can use that to change variables in your script. (python setup.py foo install) will execute the foo command before it executes install.

Hope that helps somehow. Generally speaking I would suggest providing a bit more information what exactly your extra parameter should do, maybe there is a better solution available.

Armin Ronacher
  • 31,998
  • 13
  • 65
  • 69
  • 4
    I haven't met any `--with-featurename` flag in *setuptools*. Just in case someone else stumble on this... – ankostis Jun 04 '18 at 23:13
11

I successfully used a workaround to use a solution similar to totaam's suggestion. I ended up popping my extra arguments from the sys.argv list:

import sys
from distutils.core import setup
foo = 0
if '--foo' in sys.argv:
    index = sys.argv.index('--foo')
    sys.argv.pop(index)  # Removes the '--foo'
    foo = sys.argv.pop(index)  # Returns the element after the '--foo'
# The foo is now ready to use for the setup
setup(...)

Some extra validation could be added to ensure the inputs are good, but this is how I did it

Jodin
  • 164
  • 1
  • 5
  • 2
    This is a bit of a hack, but it works and is relatively easy to understand. One could do a similar thing by leveraging argparse and replacing sys.argv with the positional arguments from argparse (and using the keyword arguments for whatever you wanted). That'd be an even bigger hack, but would allow one to leverage argparse. – GeorgeLewis Feb 24 '16 at 16:26
  • 2
    Even better, the remaining unconsumed arguments can be obtained as the second value returned from [`ArgumentParser.parse_known_arguments`](https://docs.python.org/2/library/argparse.html#partial-parsing). This is preferred, because it will handle also unconsumed args that aren't positional (thus avoid assumptions about `setuptools`). Using `argparse` is much better. Also, replacing `sys.argv` with the unconsumed args isn't more of a hack than popping positional args. They both simply remove consumed args, for "passing the remaining arguments on to another script or program". – 0 _ Jan 26 '17 at 01:12
7

A quick and easy way similar to that given by totaam would be to use argparse to grab the -foo argument and leave the remaining arguments for the call to distutils.setup(). Using argparse for this would be better than iterating through sys.argv manually imho. For instance, add this at the beginning of your setup.py:

argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('--foo', help='required foo argument', required=True)
args, unknown = argparser.parse_known_args()
sys.argv = [sys.argv[0]] + unknown

The add_help=False argument means that you can still get the regular setup.py help using -h (provided --foo is given).

andrew
  • 1,843
  • 20
  • 19
6

Perhaps you are an unseasoned programmer like me that still struggled after reading all the answers above. Thus, you might find another example potentially helpful (and to address the comments in previous answers about entering the command line arguments):

class RunClientCommand(Command):
    """
    A command class to runs the client GUI.
    """

    description = "runs client gui"

    # The format is (long option, short option, description).
    user_options = [
        ('socket=', None, 'The socket of the server to connect (e.g. '127.0.0.1:8000')',
    ]

    def initialize_options(self):
        """
        Sets the default value for the server socket.

        The method is responsible for setting default values for
        all the options that the command supports.

        Option dependencies should not be set here.
        """
        self.socket = '127.0.0.1:8000'

    def finalize_options(self):
        """
        Overriding a required abstract method.

        The method is responsible for setting and checking the 
        final values and option dependencies for all the options 
        just before the method run is executed.

        In practice, this is where the values are assigned and verified.
        """
        pass

    def run(self):
        """
        Semantically, runs 'python src/client/view.py SERVER_SOCKET' on the
        command line.
        """
        print(self.socket)
        errno = subprocess.call([sys.executable, 'src/client/view.py ' + self.socket])
        if errno != 0:
            raise SystemExit("Unable to run client GUI!")

setup(
    # Some other omitted details
    cmdclass={
        'runClient': RunClientCommand,
    },

The above is tested and from some code I wrote. I have also included slightly more detailed docstrings to make things easier to understand.

As for the command line: python setup.py runClient --socket=127.0.0.1:7777. A quick double check using print statements shows that indeed the correct argument is picked up by the run method.

Other resources I found useful (more and more examples):

Custom distutils commands

https://seasonofcode.com/posts/how-to-add-custom-build-steps-and-commands-to-setuppy.html

Community
  • 1
  • 1
OthmanEmpire
  • 103
  • 2
  • 7
5

To be fully compatible with both python setup.py install and pip install . you need to use environment variables because pip option --install-option= is bugged:

  1. pip --install-option leaks across lines
  2. Determine what should be done about --(install|global)-option with Wheels
  3. pip not naming abi3 wheels correctly

This is a full example not using the --install-option:

import os
environment_variable_name = 'MY_ENVIRONMENT_VARIABLE'
environment_variable_value = os.environ.get( environment_variable_name, None )

if environment_variable_value is not None:
    sys.stderr.write( "Using '%s=%s' environment variable!\n" % (
            environment_variable_name, environment_variable_value ) )

setup(
        name = 'packagename',
        version = '1.0.0',
        ...
)

Then, you can run it like this on Linux:

MY_ENVIRONMENT_VARIABLE=1 pip install .
MY_ENVIRONMENT_VARIABLE=1 pip install -e .
MY_ENVIRONMENT_VARIABLE=1 python setup.py install
MY_ENVIRONMENT_VARIABLE=1 python setup.py develop

But, if you are on Windows, run it like this:

set "MY_ENVIRONMENT_VARIABLE=1" && pip install .
set "MY_ENVIRONMENT_VARIABLE=1" && pip install -e .
set "MY_ENVIRONMENT_VARIABLE=1" && python setup.py install
set "MY_ENVIRONMENT_VARIABLE=1" && python setup.py develop

References:

  1. How to obtain arguments passed to setup.py from pip with '--install-option'?
  2. Passing command line arguments to pip install
  3. Passing the library path as a command line argument to setup.py
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144
  • 2
    "Messing up" without giving a version of `pip` that does mess up isn't helpful for anyone seeing this answer in the future. – 0xC0000022L May 27 '19 at 07:25
  • It is bugged and I now make it explicitly on the answer with proof. Also, this answer has a clean code anyone can easily understand and try it out by themselves. This answer is extremely useful for someone who is struggling with shameless `pip` and `setuptools` bugs. – Evandro Coan May 27 '19 at 07:38
  • 2
    My point here is not so much whether there is an issue with `pip`, but the fact that once that issue gets fixed in the future - say - this answer will become incredibly confusing ... that's why you should qualify what version(s) of `pip` is/are affected. That's all I am saying ... – 0xC0000022L May 27 '19 at 07:44
  • I understand it. I just do not have any hope they will ever fix anything. Anyways, using envinroment variables is a valid and solution whether pip has bugs or not. I did not post pip versions, but the answer has links to the issues, Once they are closed, you will know they are fixed. – Evandro Coan May 27 '19 at 07:54
  • This answer does not deserve to be downgraded as it points to a compatibility issue between pip and setuptools. Note that in `pip 20.2` `--install-option` will be removed. https://github.com/pypa/pip/issues/7309 – Samir Sadek Mar 04 '20 at 09:12
  • This is a reasonable solution for passing information from a makefile through to setup.py, e.g., use_cython option for building extensions ([docs](https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html)) – Eney Sep 26 '22 at 17:53