36

Note: distutils is deprecated and the accepted answer has been updated to use setuptools

I'm trying to add a post-install task to Python distutils as described in How to extend distutils with a simple post install script?. The task is supposed to execute a Python script in the installed lib directory. This script generates additional Python modules the installed package requires.

My first attempt is as follows:

from distutils.core import setup
from distutils.command.install import install

class post_install(install):
    def run(self):
        install.run(self)
        from subprocess import call
        call(['python', 'scriptname.py'],
             cwd=self.install_lib + 'packagename')

setup(
 ...
 cmdclass={'install': post_install},
)

This approach works, but as far as I can tell has two deficiencies:

  1. If the user has used a Python interpreter other than the one picked up from PATH, the post install script will be executed with a different interpreter which might cause a problem.
  2. It's not safe against dry-run etc. which I might be able to remedy by wrapping it in a function and calling it with distutils.cmd.Command.execute.

How could I improve my solution? Is there a recommended way / best practice for doing this? I'd like to avoid pulling in another dependency if possible.

kynan
  • 13,235
  • 6
  • 79
  • 81
  • For those who want to be able to use also `python setup.py install`, as well as `pip install`, see: http://stackoverflow.com/questions/21915469/python-setuptools-install-requires-is-ignored-when-overriding-cmdclass – 0 _ Mar 27 '15 at 23:28

2 Answers2

39

The way to address these deficiences is:

  1. Get the full path to the Python interpreter executing setup.py from sys.executable.

  2. Classes inheriting from setuptools.Command (such as setuptools.command.install.install which we use here) implement the execute method, which executes a given function in a "safe way" i.e. respecting the dry-run flag.

    Note however that the --dry-run option is currently broken and does not work as intended anyway.

I ended up with the following solution:

import os, sys
from setuptools import setup
from setuptools.command.install import install as _install


def _post_install(dir):
    from subprocess import call
    call([sys.executable, 'scriptname.py'],
         cwd=os.path.join(dir, 'packagename'))


class install(_install):
    def run(self):
        _install.run(self)
        self.execute(_post_install, (self.install_lib,),
                     msg="Running post install task")


setup(
    ...
    cmdclass={'install': install},
)

Note that I use the class name install for my derived class because that is what python setup.py --help-commands will use.

kynan
  • 13,235
  • 6
  • 79
  • 81
  • thanks, this really helped out, I also needed to follow (http://stackoverflow.com/questions/15853058/run-custom-task-when-call-pip-install) to avoid an error in my pip install. I put it all together in a blog post (http://diffbrent.ghost.io/correctly-adding-nltk-to-your-python-package-using-setup-py-post-install-commands/). Let me know if I missed something. – brent.payne Aug 03 '14 at 21:29
  • @brent.payne Glad to hear it helped! Note my comment on why I used `install` as class name. – kynan Aug 05 '14 at 19:56
  • 1
    it works, but I wasn't been able to have the custom install executed with `pip install -e name`. ps, just found [this link](http://www.niteoweb.com/blog/setuptools-run-custom-code-during-install), see the BONUS section. – Paolo Aug 12 '14 at 20:53
  • Looks very good, the post install part still runs fine with `setuptools` instead of `distutils`, but the ability of `setuptools` to handle dependencies seems to be lost (they are just ignored). – zezollo Oct 11 '17 at 16:29
  • 1
    @Paolo 's link has moved to: https://blog.niteo.co/setuptools-run-custom-code-in-setup-py/ – ipetrik Apr 09 '18 at 22:40
  • Since [distutils is deprecated](https://stackoverflow.com/a/14753678/913098), how would this solution change for setuptools? – Gulzar Jan 17 '23 at 10:34
  • `setuptools` is a compatible replacement for `distutils`. I've updated my answer. – kynan Jan 21 '23 at 17:20
-1

I think the easiest way to perform the post-install, and keep the requirements, is to decorate the call to setup(...):

from setup tools import setup


def _post_install(setup):
    def _post_actions():
        do_things()
    _post_actions()
    return setup

setup = _post_install(
    setup(
        name='NAME',
        install_requires=['...
    )
)

This will run setup() when declaring setup. Once done with the requirements installation, it will run the _post_install() function, which will run the inner function _post_actions().

Mbm
  • 43
  • 2