How do I write a setup.py for a twistd/twisted plugin that works with setuptools, distribute, etc?
Asked Answered
L

4

26

The Twisted Plugin System is the preferred way to write extensible twisted applications.

However, due to the way the plugin system is structured (plugins go into a twisted/plugins directory which should not be a Python package), writing a proper setup.py for installing those plugins appears to be non-trivial.

I've seen some attempts that add 'twisted.plugins' to the 'packages' key of the distutils setup command, but since it is not really a package, bad things happen (for example, an __init__.py is helpfully added by some tools).

Other attempts seem to use 'package_data' instead (eg, http://bazaar.launchpad.net/~glyph/divmod.org/trunk/view/head:/Epsilon/epsilon/setuphelper.py), but that can also fail in weird ways.

The question is: has anyone successfully written a setup.py for installing twisted plugins which works in all cases?

Lower answered 1/9, 2011 at 19:14 Comment(2)
A description of the package_data failure would be helpful.Nupercaine
This doesn't answer the explicit part of this question, i.e. how to specify the files to be installed, but it does address a somewhat cleaner way of (re-) generating the plugin cache (which is implied as part of it) #1321770Nupercaine
H
17

I document a setup.py below that is needed only if you have users with pip < 1.2 (e.g. on Ubuntu 12.04). If everyone has pip 1.2 or newer, the only thing you need is packages=[..., 'twisted.plugins'].

By preventing pip from writing the line "twisted" to .egg-info/top_level.txt, you can keep using packages=[..., 'twisted.plugins'] and have a working pip uninstall that doesn't remove all of twisted/. This involves monkeypatching setuptools/distribute near the top of your setup.py. Here is a sample setup.py:

from distutils.core import setup

# When pip installs anything from packages, py_modules, or ext_modules that
# includes a twistd plugin (which are installed to twisted/plugins/),
# setuptools/distribute writes a Package.egg-info/top_level.txt that includes
# "twisted".  If you later uninstall Package with `pip uninstall Package`,
# pip <1.2 removes all of twisted/ instead of just Package's twistd plugins.
# See https://github.com/pypa/pip/issues/355 (now fixed)
#
# To work around this problem, we monkeypatch
# setuptools.command.egg_info.write_toplevel_names to not write the line
# "twisted".  This fixes the behavior of `pip uninstall Package`.  Note that
# even with this workaround, `pip uninstall Package` still correctly uninstalls
# Package's twistd plugins from twisted/plugins/, since pip also uses
# Package.egg-info/installed-files.txt to determine what to uninstall,
# and the paths to the plugin files are indeed listed in installed-files.txt.
try:
    from setuptools.command import egg_info
    egg_info.write_toplevel_names
except (ImportError, AttributeError):
    pass
else:
    def _top_level_package(name):
        return name.split('.', 1)[0]

    def _hacked_write_toplevel_names(cmd, basename, filename):
        pkgs = dict.fromkeys(
            [_top_level_package(k)
                for k in cmd.distribution.iter_distribution_names()
                if _top_level_package(k) != "twisted"
            ]
        )
        cmd.write_file("top-level names", filename, '\n'.join(pkgs) + '\n')

    egg_info.write_toplevel_names = _hacked_write_toplevel_names

setup(
    name='MyPackage',
    version='1.0',
    description="You can do anything with MyPackage, anything at all.",
    url="http://example.com/",
    author="John Doe",
    author_email="[email protected]",
    packages=['mypackage', 'twisted.plugins'],
    # You may want more options here, including install_requires=,
    # package_data=, and classifiers=
)

# Make Twisted regenerate the dropin.cache, if possible.  This is necessary
# because in a site-wide install, dropin.cache cannot be rewritten by
# normal users.
try:
    from twisted.plugin import IPlugin, getPlugins
except ImportError:
    pass
else:
    list(getPlugins(IPlugin))

I've tested this with pip install, pip install --user, and easy_install. With any install method, the above monkeypatch and pip uninstall work fine.

You might be wondering: do I need to clear the monkeypatch to avoid messing up the next install? (e.g. pip install --no-deps MyPackage Twisted; you wouldn't want to affect Twisted's top_level.txt.) The answer is no; the monkeypatch does not affect another install because pip spawns a new python for each install.

Related: keep in mind that in your project, you must not have a file twisted/plugins/__init__.py. If you see this warning during installation:

package init file 'twisted/plugins/__init__.py' not found (or not a regular file)

it is completely normal and you should not try to fix it by adding an __init__.py.

Hood answered 23/9, 2011 at 6:34 Comment(15)
Hacktastic! The uninstall of the plugin file will only occur in this case if pip was used to install the plugin-containing project (as opposed to easy_install or "python setup.py install"), because only pip writes installed-files.txt at install time. I think that's a drawback that any solution to this problem can't avoid, though, since having a full list of installed files is the only way to uninstall correctly if projects are allowed to go dropping files into other projects' packages.Leeward
This could use a little more explaining to be a complete answer: what does the setup.py look like? How does this work with a raw setup.py? What about with easy_install? How about with a --user install, etc. But, as Carl put it: hacktastic! (Hopefully this proves that it is possible to fix github.com/pypa/pip/issues/355 - at least in the case where pip was doing the installing ...)Nupercaine
Updated answer per Glyph's commentHood
Now: how many of these systems support a proper post-install hook that properly generates dropin.cache as per twistedmatrix.com/documents/11.0.0/core/howto/plugin.html#auto3 :)Nupercaine
Updated with dropin.cache regenerationHood
I should note that this dropin.cache regeneration is wrong for debian (you need a postinst/postrm) and wrong for bdist_wininst (which is more subtle and I don't quite understand it). I think it will probably work with pip, and ... in some cases, with easy_install, although I don't recall which cases caused a problem.Nupercaine
Reference for bdist_wininst is here - docs.python.org/distutils/… - I don't think this works with bdist_msi though.Nupercaine
@Nupercaine - Oh, there's never been any doubt that it's possible to fix #355 by ignoring top_level.txt if we have installed-files.txt (which is still not quite the same as this hack, since we're not going to special-case the "twisted" package in pip, as this does). The question was always whether this was a good idea or not :-) As you can see on that bug, I've reconsidered my take on that.Leeward
I just wanted to note that the dropin.cache regeneration is indeed "wrong" for Debian (as it needs to be done at package installation / removal time), but harmless as it won't cause any problems during building of the source package.Brabant
Is there somewhere that we can put the cache regeneration where some standard debian tool will find it? Perhaps stdeb has a way to find post-install post-rm hooks that is an extension to distutils?Nupercaine
The relevant stdeb issue appears to be here: github.com/astraw/stdeb/issues/46Nupercaine
Wokkel has a problem which is tangentially related, although I'm not sure if its setup.py is really following this advice: wokkel.ik.nu/ticket/76Nupercaine
Note that Pip bug #355 has now been fixed, although I'm not sure if it has been included in a release yet.Brabant
By now I'm pretty sure that it's been in a release. (Wouldn't it be nice if Github could aggregate this information for you? Oh well.)Nupercaine
This is fixed as of Pip 1.2Brabant
H
3

Here is a blog entry which describes doing it with 'package_data':

http://chrismiles.livejournal.com/23399.html

In what weird ways can that fail? It could fail if the installation of the package doesn't put the package data into a directory which is on the sys.path. In that case the Twisted plugin loader wouldn't find it. However, all installations of Python packages that I know of will put it into the same directory where they are installing the Python modules or packages themselves, so that won't be a problem.

Hypnotherapy answered 1/9, 2011 at 21:15 Comment(9)
One of the problems with using package_data like that is that you still need to list 'twisted.plugins' in the the list of packages; this results in pip uninstall blowing away your entire Twisted installation.Brabant
Is there any way to inform pip not to do this? Is there a bug report open in the Pip bugtracker?Nupercaine
I haven't opened a bug report, but it does seem like a bug in pip; it has a record of exactly what files it installed, so I don't see why it needs to remove other files/directories that it didn't install (with the possible caveat of .py[co] files)Brabant
Aside from this bug in pip, are there any other issues? It sounds like this may be the right option.Nupercaine
I've now filed a bug report here: github.com/pypa/pip/issues/355 -- the fact that twisted ends up in top_level.txt when installing a plugin module like this does suggest that the this approach may not be the right thing to do.Brabant
The linked post ends by saying "Unfortunately Twisted and setuptools don't play nicely together, so I'm not able to ... install it using easy_install", which sounds like a weird failure (but he doesn't describe what the failure is). Have things improved since then? I know we have some setuptools compatibility cruft now but I don't know how well it works.Nupercaine
Additionally, the comment that Ivan had me put on Eric's answer may also apply here.Nupercaine
Note that Pip bug #355 has now been fixed, although I'm not sure if it has been included in a release yet.Brabant
With the fix to Pip bug #355, Chris Miles's approach seems to work. I used it to package oauth-proxy just now and it appeared to work: github.com/mojodna/oauth-proxy/pull/5Hypnotherapy
R
2

Maybe you could adapt the package_data idea to use data_files instead: it wouldn’t require you to list twisted.plugins as package, as it uses absolute paths. It would still be a kludge, though.

My tests with pure distutils have told me that its is possible to overwrite files from another distribution. I wanted to test poor man’s namespace packages using pkgutil.extend_path and distutils, and it turns out that I can install spam/ham/__init__.py with spam.ham/setup.py and spam/eggs/__init__.py with spam.eggs/setup.py. Directories are not a problem, but files will be happily overwritten. I think this is actually undefined behavior in distutils which trickles up to setuptools and pip, so pip could IMO close as wontfix.

What is the usual way to install Twisted plugins? Drop-it-here by hand?

Rheotaxis answered 21/9, 2011 at 15:43 Comment(7)
There is no "usual" way to install Twisted plugins - there are a couple of different random ideas floating around, each with their own drawbacks. This question is part of an effort to nail down one "right" way to do it, and at least have some concept of what the drawbacks are and how to cope with them.Nupercaine
Pip has closed as wontfix :-) Does the data_files approach work? If so it seems like the most promising approach listed here. It would have the same drawback as the "patch write_toplevel_names" approach: the plugin would only be uninstalled correctly if pip had been used to install it.Leeward
Ivan tells me (but cannot comment due to SO's weird reputation limits) - One problem with this approach is that pip uninstall will not remove the corresponding .pyc for your .py data_file. You can't add the .pyc to your data_files= either, because PYTHON_DONT_WRITE_BYTECODE might be set.Nupercaine
The .pyc/.pyo issue is a big one. If pip supports uninstall hooks, it could be done here (distutils2 will have such hooks). If not, it only shows that you must use py_modules or packages to include Python files.Rheotaxis
[cont.] So it seems hard to use distutils with Twisted plugins, as they’re not regular public modules. The best that could probably be done would be to add a custom setup function in Twisted for use in plugins’ setup scripts. The function would get the path to the plugins directory and coerce or monkey-patch distutils into installing there. For uninstall, I don’t know; distutils only supports the --record option, but apparently pip does more.Rheotaxis
In the future, I hope PEP 402 is accepted, then distutils2 will add support for it and Twisted will be able to change its custom idea of plugins for a namespace package (think mercurial and hgext).Rheotaxis
For now, what's the final word here?Concinnous
D
1

I use this approach:

  1. Put '.py' and '.pyc' versions of your file to "twisted/plugins/" folder inside your package. Note that '.pyc' file can be empty, it just should exist.
  2. In setup.py specify copying both files to a library folder (make sure that you will not overwrite existing plugins!). For example:

    # setup.py
    
    from distutils import sysconfig
    
    LIB_PATH = sysconfig.get_python_lib()
    
    # ...
    
    plugin_name = '<your_package>/twisted/plugins/<plugin_name>'
    # '.pyc' extension is necessary for correct plugins removing
    data_files = [
      (os.path.join(LIB_PATH, 'twisted', 'plugins'),
       [''.join((plugin_name, extension)) for extension in ('.py', '.pyc')])
    ]
    
    setup(
          # ...
          data_files=data_files
    )
    
Denounce answered 28/11, 2013 at 13:6 Comment(2)
What platforms, configurations, bdist_* plugins, etc, have you tested this with?Nupercaine
Windows 7 and Red Hat 6; Python 2.7; bdist_dumb (gztar format); installing with pip. I believe this approach works for all Python versions less than 3.2 because of this.Denounce

© 2022 - 2024 — McMap. All rights reserved.