134

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?

6
  • Many setup.py have a setup.py test command that you use after setup.py install. Commented Nov 29, 2013 at 16:08
  • 5
    I am hoping to automate the script run rather than requiring the user to enter a second command. Any thoughts? Commented Nov 29, 2013 at 16:13
  • 1
    This might be what you're looking for: stackoverflow.com/questions/17806485/… Commented Jan 16, 2014 at 17:59
  • 1
    If you do need this, this blog post that I found by a quick google looks like it would be useful. (Also see Extending and Reusing Setuptools in the docs.) Commented Aug 27, 2014 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. Commented Apr 18, 2018 at 18:55

7 Answers 7

118

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.

Sign up to request clarification or add additional context in comments.

19 Comments

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.
using this solution it seems that install_requires dependencies are ignored
This didn't work for me with pip3. The install script ran when publishing the package, but not when installing it.
Just seem to work with setup.py install and NOT when pip installing the package.
@Raf is there a solution that works for both setup.py install and pip install?
|
21

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},

10 Comments

@kynan Because setuptools is quite under-documented. Others have already amended their answers on this Q&A with the correct solutions.
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).
I was also having problems with dependencies and atexit works for me
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.
Thank you for providing this answer. It works for me (as does the answer provided by @mertyildiran ). One thing i found is that the output of the print statement is only shown if i run pip install in verbose mode, for example: pip3 install -v -e .
|
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.

4 Comments

In my case, overriding run() causes the package dependencies not to be installed.
@Apalala that was because the wrong cmdclass was replaced, I've fixed this.
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.
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?
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.

4 Comments

What is my_name here?
my_name is the name of the installed package, and is the same name you parse to the setup function's name keyword parameter.
What if my_name lives inside pyproject.toml and not inside setup.py?
@IntrastellarExplorer Then you could bootstrap the setup.py file itself from the pyproject.toml by making a script that extracts the package name from the toml file and then creates a setup.py file (possibly from a setup.py.in template file) with the package name embedded in it.
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().

1 Comment

Did you try this? I am trying with Python 3.4 and install works as normal but the post_actions are not executed...
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.

1 Comment

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.
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()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.