Commit 32c90ba5 authored by Stephen Finucane's avatar Stephen Finucane

Remove support for command hooks

distutils2 is long dead and many of its best ideas have been
incorporated into setuptools. One of the ideas that *has not* been
incorporated is the idea of command hooks, of either the pre or post
kind. pbr is still carrying the code for this, and there are several
issues with this:

- No one is using this functionality in OpenStack and, given the
  complete lack of documentation on the matter, it's very doubtful that
  it's being used anywhere else [1]
- It's causing issues for projects attempting to hook into the
  'distutils.commands' entry point on Python 2.7, which it seems no else
  must have attempted yet [2].
- distutils2 is dead and advanced features like this that we don't
  explicitly need should not be retained

We could attempt to fix this but why bother? Good riddance, I say.


Change-Id: I01f657034cffbf55ce830b7e8dbb6b3d94c1fd18
parent 87637bdf
......@@ -141,16 +141,5 @@ def pbr(dist, attr, value):
if isinstance(dist.metadata.version, integer_types + (float,)):
# Some people apparently take "version number" too literally :)
dist.metadata.version = str(dist.metadata.version)
# This bit of hackery is necessary so that the Distribution will ignore
# normally unsupport command options (namely pre-hooks and post-hooks).
# dist.command_options is normally a dict mapping command names to
# dicts of their options. Now it will be a defaultdict that returns
# IgnoreDicts for the each command's options so we can pass through the
# unsupported options
ignore = ['pre_hook.*', 'post_hook.*']
dist.command_options = util.DefaultGetDict(
lambda: util.IgnoreDict(ignore)
......@@ -39,9 +39,7 @@
import os
import textwrap
from testtools import content
from testtools import matchers
from pbr.tests import base
......@@ -56,10 +54,6 @@ class TestHooks(base.BaseTestCase):
cfg.set('global', 'setup-hooks',
cfg.set('build_ext', 'pre-hook.test_pre_hook',
cfg.set('build_ext', 'post-hook.test_post_hook',
def test_global_setup_hooks(self):
"""Test setup_hooks.
......@@ -72,28 +66,6 @@ class TestHooks(base.BaseTestCase):
assert 'test_hook_1\ntest_hook_2' in stdout
assert return_code == 0
def test_command_hooks(self):
"""Test command hooks.
Simple test that the appropriate command hooks run at the
beginning/end of the appropriate command.
stdout, _, return_code = self.run_setup('egg_info')
assert 'build_ext pre-hook' not in stdout
assert 'build_ext post-hook' not in stdout
assert return_code == 0
stdout, stderr, return_code = self.run_setup('build_ext')
self.addDetailUniqueName('stderr', content.text_content(stderr))
assert textwrap.dedent("""
running build_ext
running pre_hook pbr_testpackage._setup_hooks.test_pre_hook for command build_ext
build_ext pre-hook
""") in stdout # flake8: noqa
self.expectThat(stdout, matchers.EndsWith('build_ext post-hook'))
assert return_code == 0
def test_custom_commands_known(self):
stdout, _, return_code = self.run_setup('--help-commands')
......@@ -51,7 +51,3 @@ optional = True
# pbr_testpackage._setup_hooks.test_hook_1
# pbr_testpackage._setup_hooks.test_hook_2
commands = pbr_testpackage._setup_hooks.test_command
#pre-hook.test_pre_hook = pbr_testpackage._setup_hooks.test_pre_hook
#post-hook.test_post_hook = pbr_testpackage._setup_hooks.test_post_hook
......@@ -264,8 +264,6 @@ def cfg_to_args(path='setup.cfg', script_args=()):
if entry_points:
kwargs['entry_points'] = entry_points
# Handle the [files]/extra_files option
files_extra_files = has_get_option(config, 'files', 'extra_files')
if files_extra_files:
......@@ -552,103 +550,6 @@ def get_entry_points(config):
for option, value in config['entry_points'].items())
def wrap_commands(kwargs):
dist = st_dist.Distribution()
# This should suffice to get the same config values and command classes
# that the actual Distribution will see (not counting cmdclass, which is
# handled below)
# Setuptools doesn't patch get_command_list, and as such we do not get
# extra commands from entry_points. As we need to be compatable we deal
# with this here.
for ep in pkg_resources.iter_entry_points('distutils.commands'):
if not in dist.cmdclass:
if hasattr(ep, 'resolve'):
cmdclass = ep.resolve()
# Old setuptools does not have ep.resolve, and load with
# arguments is depricated in 11+. Use resolve, 12+, if we
# can, otherwise fall back to load.
# Setuptools 11 will throw a deprication warning, as it
# uses _load instead of resolve.
cmdclass = ep.load(False)
dist.cmdclass[] = cmdclass
for cmd, _ in dist.get_command_list():
hooks = {}
for opt, val in dist.get_option_dict(cmd).items():
val = val[1]
if opt.startswith('pre_hook.') or opt.startswith('post_hook.'):
hook_type, alias = opt.split('.', 1)
hook_dict = hooks.setdefault(hook_type, {})
hook_dict[alias] = val
if not hooks:
if 'cmdclass' in kwargs and cmd in kwargs['cmdclass']:
cmdclass = kwargs['cmdclass'][cmd]
cmdclass = dist.get_command_class(cmd)
new_cmdclass = wrap_command(cmd, cmdclass, hooks)
kwargs.setdefault('cmdclass', {})[cmd] = new_cmdclass
def wrap_command(cmd, cmdclass, hooks):
def run(self, cmdclass=cmdclass):
return type(cmd, (cmdclass, object),
{'run': run, 'run_command_hooks': run_command_hooks,
'pre_hook': hooks.get('pre_hook'),
'post_hook': hooks.get('post_hook')})
def run_command_hooks(cmd_obj, hook_kind):
"""Run hooks registered for that command and phase.
*cmd_obj* is a finalized command object; *hook_kind* is either
'pre_hook' or 'post_hook'.
if hook_kind not in ('pre_hook', 'post_hook'):
raise ValueError('invalid hook kind: %r' % hook_kind)
hooks = getattr(cmd_obj, hook_kind, None)
if hooks is None:
for hook in hooks.values():
if isinstance(hook, str):
hook_obj = resolve_name(hook)
except ImportError:
err = sys.exc_info()[1] # For py3k
raise errors.DistutilsModuleError('cannot find hook %s: %s' %
hook_obj = hook
if not hasattr(hook_obj, '__call__'):
raise errors.DistutilsOptionError('hook %r is not callable' % hook)'running %s %s for command %s',
hook_kind, hook, cmd_obj.get_command_name())
try :
e = sys.exc_info()[1]
log.error('hook %s raised exception: %s\n' % (hook, e))
def has_get_option(config, section, option):
if section in config and option in config[section]:
return config[section][option]
......@@ -684,20 +585,3 @@ class DefaultGetDict(defaultdict):
if default is None:
default = self.default_factory()
return super(DefaultGetDict, self).setdefault(key, default)
class IgnoreDict(dict):
"""A dictionary that ignores any insertions in which the key is a string
matching any string in `ignore`. The ignore list can also contain wildcard
patterns using '*'.
def __init__(self, ignore):
self.__ignore = re.compile(r'(%s)' % ('|'.join(
[pat.replace('*', '.*')
for pat in ignore])))
def __setitem__(self, key, val):
if self.__ignore.match(key):
super(IgnoreDict, self).__setitem__(key, val)
- |
Support for entry point command hooks has been removed. This feature was
poorly tested, poorly documented, and broken in some environments.
Support for global hooks is not affected.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment