110

Is it possible to specify a post-install Python script file as part of the setuptools setup.py file so that a user can run the command:

python setup.py install

on a local project file archive, or

pip install <name>

for a PyPI project and the script will be run at the completion of the standard setuptools install? I am looking to perform post-install tasks that can be coded in a single Python script file (e.g. deliver a custom post-install message to the user, pull additional data files from a different remote source repository).

I came across this SO answer from several years ago that addresses the topic and it sounds as though the consensus at that time was that you need to create an install subcommand. If that is still the case, would it be possible for someone to provide an example of how to do this so that it is not necessary for the user to enter a second command to run the script?

Community
  • 1
  • 1
Chris Simpkins
  • 1,314
  • 2
  • 10
  • 13
  • Many setup.py have a `setup.py test` command that you use after `setup.py install`. – User Nov 29 '13 at 16:08
  • 4
    I am hoping to automate the script run rather than requiring the user to enter a second command. Any thoughts? – Chris Simpkins Nov 29 '13 at 16:13
  • 1
    This might be what you're looking for: http://stackoverflow.com/questions/17806485/execute-a-python-script-post-install-using-distutils-setuptools – limp_chimp Jan 16 '14 at 17:59
  • 1
    Thank you! I will check it out – Chris Simpkins Jan 17 '14 at 03:36
  • Are you sure you need a post-install script? I'm pretty sure delivering a custom message can be done without one, and pulling additional data files might be better done by making a setuptools distribution for those data files and listing it as a dependency. – abarnert Aug 27 '14 at 11:45
  • 1
    If you do need this, [this blog post](http://www.niteoweb.com/blog/setuptools-run-custom-code-during-install) that I found by a quick google looks like it would be useful. (Also see [Extending and Reusing Setuptools](http://pythonhosted.org/setuptools/setuptools.html#extending-and-reusing-setuptools) in the docs.) – abarnert Aug 27 '14 at 11:46
  • 1
    @Simon Well, you're looking at a comment from 4 years ago about something that probably isn't what someone with this problem wants, so you can't really expect it to be monitored and kept up to date. If this were an answer, it would be worth the effort to find new resources to replace them, but it's not. If you need the outdated information, you can always use the Wayback Machine, or you can search for the equivalent section in the current docs. – abarnert Apr 18 '18 at 18:55

7 Answers7

100

Note: The solution below only works when installing a source distribution zip or tarball, or installing in editable mode from a source tree. It will not work when installing from a binary wheel (.whl)


This solution is more transparent:

You will make a few additions to setup.py and there is no need for an extra file.

Also you need to consider two different post-installations; one for development/editable mode and the other one for install mode.

Add these two classes that includes your post-install script to setup.py:

from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install


class PostDevelopCommand(develop):
    """Post-installation for development mode."""
    def run(self):
        develop.run(self)
        # PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION

class PostInstallCommand(install):
    """Post-installation for installation mode."""
    def run(self):
        install.run(self)
        # PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION

and insert cmdclass argument to setup() function in setup.py:

setup(
    ...

    cmdclass={
        'develop': PostDevelopCommand,
        'install': PostInstallCommand,
    },

    ...
)

You can even call shell commands during installation, like in this example which does pre-installation preparation:

from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
from subprocess import check_call


class PreDevelopCommand(develop):
    """Pre-installation for development mode."""
    def run(self):
        check_call("apt-get install this-package".split())
        develop.run(self)

class PreInstallCommand(install):
    """Pre-installation for installation mode."""
    def run(self):
        check_call("apt-get install this-package".split())
        install.run(self)


setup(
    ...

P.S. there are no any pre-install entry points available on setuptools. Read this discussion if you are wondering why there is none.

akaihola
  • 24,161
  • 5
  • 52
  • 64
mertyildiran
  • 5,915
  • 4
  • 30
  • 48
  • Looks cleaner than the others, but doesn't this execute the custom code *before* the actual `install` command? – raphinesse Oct 04 '16 at 10:03
  • @raphinesse I guess so, I don't remember entirely. But there was a workaround. I will make a complete test and update my answer. Thanks for the notification. – mertyildiran Oct 04 '16 at 14:11
  • 7
    It's up to you: if you call `run` on the parent *first* then your command is a post-install, otherwise it's a pre-install. I've updated the answer to reflect this. – kynan May 22 '17 at 14:23
  • @kynan Absolutely. I voted for your answer. The other answers do not work for me. – personal_cloud Sep 18 '17 at 16:15
  • The question title is about post-install not pre-install. So it is confusing to have the answer be about pre-install. I tried this answer assuming it was actually an answer to the post-install question, and then gave up when it didn't work. Only 3 days later did I stumble on the detailed comments (thank you kynan for the clearest explanation) and realize that there was actually an answer for post-install. So I have submitted an edit request. – personal_cloud Sep 18 '17 at 16:28
  • @personal_cloud "*there is no any pre-install entry points available on setuptools. Read [this discussion](https://bugs.python.org/setuptools/issue41) if you are wondering why there is none.*" `install.run(self)` is just finishing the installation and not doing much. – mertyildiran Sep 18 '17 at 17:39
  • @mertyildiran Actually, I did a simple test, and `install.run(self)` is doing all of the `data_files` movement. – personal_cloud Sep 18 '17 at 20:22
  • 1
    using this solution it seems that `install_requires` dependencies are ignored – ealfonso Jul 31 '18 at 16:56
  • @ealfonso I'm sure it does not have any effect on `install_requires`. How did you reach that conclusion? In this arrangement actually, it executes post-install script before installing Python package dependencies. Maybe you are confusing this to that? – mertyildiran Aug 01 '18 at 09:49
  • I can confirm that this approach will ignore the `install_requires`. Tested on python 2.7.x. The `atexit` approach below will work – Joshua H Oct 23 '18 at 14:45
  • This didn't work for me with `pip install -e .` for `Python 3.6`. The `atexit` method below works though. – Seanny123 Oct 25 '18 at 20:21
  • 7
    This didn't work for me with `pip3`. The install script ran when publishing the package, but not when installing it. – Eric Wiener Apr 13 '19 at 21:40
  • link to discussion it's broken – Juan Antonio Orozco Apr 17 '19 at 19:53
  • 1
    @JuanAntonioOrozco I have updated the broken link using Wayback Machine. I don't know why it's broken at this very moment. Maybe something is wrong with https://bugs.python.org/ right now. – mertyildiran Apr 18 '19 at 08:32
  • To @JoshuaH, this can be resolved here: https://stackoverflow.com/questions/21915469/python-setuptools-install-requires-is-ignored-when-overriding-cmdclass – onesiumus Jul 24 '20 at 19:15
  • 1
    Just seem to work with `setup.py install` and NOT when `pip install`ing the package. – Raf Aug 28 '20 at 09:54
  • @Raf is there a solution that works for both `setup.py install` and `pip install`? – banskt Apr 21 '21 at 15:19
16

Note: The solution below only works when installing a source distribution zip or tarball, or installing in editable mode from a source tree. It will not work when installing from a binary wheel (.whl)


This is the only strategy that has worked for me when the post-install script requires that the package dependencies have already been installed:

import atexit
from setuptools.command.install import install


def _post_install():
    print('POST INSTALL')


class new_install(install):
    def __init__(self, *args, **kwargs):
        super(new_install, self).__init__(*args, **kwargs)
        atexit.register(_post_install)


setuptools.setup(
    cmdclass={'install': new_install},
akaihola
  • 24,161
  • 5
  • 52
  • 64
Apalala
  • 8,159
  • 3
  • 26
  • 47
  • Why do you register an `atexit` handler rather than simply calling the post install function after the installation step? – kynan May 22 '17 at 14:30
  • @kynan It's what worked for me. `setuptools` is very undocumented. See other (amended) answers in this Q&A in which people have finally figured out who to do it with only `setuptools`. – Apalala May 22 '17 at 20:07
  • Why do you use `atexit` instead of just calling `_post_install()` in your `cmdclass`? – kynan May 22 '17 at 21:54
  • 1
    @kynan Because `setuptools` is quite under-documented. Others have already amended their answers on this Q&A with the correct solutions. – Apalala May 22 '17 at 23:27
  • 3
    Well the other answers do not work for me: either the post install script is not executed, or the dependencies are not handled anymore. So far, I'll stick to `atexit` and **not** redefining `install.run()` (this is the reason why the dependencies are not handled anymore). In addition, in order to know the install directory, I've put `_post_install()` as a method of `new_install`, what lets me access to `self.install_purelib` and `self.install_platlib` (don't know which one to use, but `self.install_lib` is wrong, weirdly). – zezollo Oct 12 '17 at 15:12
  • 2
    I was also having problems with dependencies and atexit works for me – ealfonso Jul 31 '18 at 20:48
  • 7
    None of the methods here seem to work with wheels. Wheels do not run setup.py so, the messages are only displayed when building, not when installing the package. – JCGB Mar 04 '19 at 15:12
8

Note: The solution below only works when installing a source distribution zip or tarball, or installing in editable mode from a source tree. It will not work when installing from a binary wheel (.whl)


A solution could be to include a post_setup.py in setup.py's directory. post_setup.py will contain a function which does the post-install and setup.py will only import and launch it at the appropriate time.

In setup.py:

from distutils.core import setup
from distutils.command.install_data import install_data

try:
    from post_setup import main as post_install
except ImportError:
    post_install = lambda: None

class my_install(install_data):
    def run(self):
        install_data.run(self)
        post_install()

if __name__ == '__main__':
    setup(
        ...
        cmdclass={'install_data': my_install},
        ...
    )

In post_setup.py:

def main():
    """Do here your post-install"""
    pass

if __name__ == '__main__':
    main()

With the common idea of launching setup.py from its directory, you will be able to import post_setup.py else it will launch an empty function.

In post_setup.py, the if __name__ == '__main__': statement allows you to manually launch post-install from command line.

akaihola
  • 24,161
  • 5
  • 52
  • 64
Zulu
  • 6,802
  • 9
  • 42
  • 51
  • 4
    In my case, overriding ``run()`` causes the package dependencies not to be installed. – Apalala Jul 17 '16 at 14:34
  • 1
    @Apalala that was because the wrong `cmdclass` was replaced, I've fixed this. – kynan May 22 '17 at 14:33
  • 1
    Ah, finally, we find the right answer. How come wrong answers get so many votes on StackOverflow? Indeed, you have to run your `post_install()` *after* the `install_data.run(self)` otherwise you'll be missing some stuff. Like `data_files` at least. Thank you kynan. – personal_cloud Sep 18 '17 at 16:19
  • 1
    Does not work for me. I guess, for any reason, the command `install_data` is not executed in my case. So, hasn't `atexit` the advantage of ensuring the post install script will be executed in the end, in any situation? – zezollo Oct 12 '17 at 15:02
3

Combining the answers from @Apalala, @Zulu and @mertyildiran; this worked for me in a Python 3.5 environment:

import atexit
import os
import sys
from setuptools import setup
from setuptools.command.install import install

class CustomInstall(install):
    def run(self):
        def _post_install():
            def find_module_path():
                for p in sys.path:
                    if os.path.isdir(p) and my_name in os.listdir(p):
                        return os.path.join(p, my_name)
            install_path = find_module_path()

            # Add your post install code here

        atexit.register(_post_install)
        install.run(self)

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

This also gives you access the to the installation path of the package in install_path, to do some shell work on.

Ezbob
  • 123
  • 6
2

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
  • 49
  • 2
  • 1
    Did you try this? I am trying with Python 3.4 and install works as normal but the post_actions are not executed... – dojuba Jul 26 '18 at 07:42
1

If using atexit, there is no need to create a new cmdclass. You can simply create your atexit register right before the setup() call. It does the same thing.

Also, if you need dependencies to be installed first, this does not work with pip install since your atexit handler will be called before pip moves the packages into place.

myjay610
  • 83
  • 5
  • Like a few suggestions posted here, this one doesn't account for whether or not you are running in "install" mode or not. That's the point of why custom "command" classes are employed. – BuvinJ Apr 03 '20 at 14:43
1

I wasn't able to solve a problem with any presented recommendations, so here is what helped me.

You can call function, that you want to run after installation just after setup() in setup.py, like that:

from setuptools import setup

def _post_install():
    <your code>

setup(...)

_post_install()
sdrenn00
  • 41
  • 6