[Python-checkins] distutils2: Add command post-hooks
tarek.ziade
python-checkins at python.org
Sun Aug 8 11:50:46 CEST 2010
tarek.ziade pushed 593b7db7a5b4 to distutils2:
http://hg.python.org/distutils2/rev/593b7db7a5b4
changeset: 441:593b7db7a5b4
user: Konrad Delong <konryd at gmail.com>
date: Thu Aug 05 15:48:12 2010 +0200
summary: Add command post-hooks
files: docs/source/command_hooks.rst, docs/source/index.rst, src/distutils2/command/cmd.py, src/distutils2/dist.py, src/distutils2/tests/test_dist.py, src/distutils2/tests/test_util.py, src/distutils2/util.py
diff --git a/docs/source/command_hooks.rst b/docs/source/command_hooks.rst
new file mode 100644
--- /dev/null
+++ b/docs/source/command_hooks.rst
@@ -0,0 +1,31 @@
+=============
+Command hooks
+=============
+
+Distutils2 provides a way of extending its commands by the use of pre- and
+post- command hooks. The hooks are simple Python functions (or any callable
+objects) and are specified in the config file using their full qualified names.
+The pre-hooks are run after the command is finalized (its options are
+processed), but before it is run. The post-hooks are run after the command
+itself. Both types of hooks receive an instance of the command object.
+
+Sample usage of hooks
+=====================
+
+Firstly, you need to make sure your hook is present in the path. This is usually
+done by dropping them to the same folder where `setup.py` file lives ::
+
+ # file: myhooks.py
+ def my_install_hook(install_cmd):
+ print "Oh la la! Someone is installing my project!"
+
+Then, you need to point to it in your `setup.cfg` file, under the appropriate
+command section ::
+
+ [install]
+ pre-hook.project = myhooks.my_install_hook
+
+The hooks defined in different config files (system-wide, user-wide and
+package-wide) do not override each other as long as they are specified with
+different aliases (additional names after the dot). The alias in the example
+above is ``project``.
diff --git a/docs/source/index.rst b/docs/source/index.rst
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -15,6 +15,7 @@
pkgutil
depgraph
commands
+ command_hooks
test_framework
pypi
version
diff --git a/src/distutils2/command/cmd.py b/src/distutils2/command/cmd.py
--- a/src/distutils2/command/cmd.py
+++ b/src/distutils2/command/cmd.py
@@ -51,6 +51,12 @@
# defined. The canonical example is the "install" command.
sub_commands = []
+ # Pre and post command hooks are run just before or just after the command
+ # itself. They are simple functions that receive the command instance. They
+ # should be specified as dotted strings.
+ pre_hook = None
+ post_hook = None
+
# -- Creation/initialization methods -------------------------------
diff --git a/src/distutils2/dist.py b/src/distutils2/dist.py
--- a/src/distutils2/dist.py
+++ b/src/distutils2/dist.py
@@ -18,7 +18,7 @@
from distutils2.errors import (DistutilsOptionError, DistutilsArgError,
DistutilsModuleError, DistutilsClassError)
from distutils2.fancy_getopt import FancyGetopt, translate_longopt
-from distutils2.util import check_environ, strtobool
+from distutils2.util import check_environ, strtobool, resolve_dotted_name
from distutils2 import log
from distutils2.metadata import DistributionMetadata
@@ -28,7 +28,6 @@
# to look for a Python module named after the command.
command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
-
class Distribution(object):
"""The core of the Distutils. Most of the work hiding behind 'setup'
is really done within a Distribution instance, which farms the work out
@@ -116,7 +115,7 @@
('use-2to3', None,
"use 2to3 to make source python 3.x compatible"),
('convert-2to3-doctests', None,
- "use 2to3 to convert doctests in seperate text files"),
+ "use 2to3 to convert doctests in seperate text files"),
]
display_option_names = map(lambda x: translate_longopt(x[0]),
display_options)
@@ -382,7 +381,19 @@
if opt != '__name__':
val = parser.get(section,opt)
opt = opt.replace('-', '_')
- opt_dict[opt] = (filename, val)
+
+ # ... although practicality beats purity :(
+ if opt.startswith("pre_hook.") or opt.startswith("post_hook."):
+ hook_type, alias = opt.split(".")
+ # Hooks are somewhat exceptional, as they are
+ # gathered from many config files (not overriden as
+ # other options).
+ # The option_dict expects {"command": ("filename", # "value")}
+ # so for hooks, we store only the last config file processed
+ hook_dict = opt_dict.setdefault(hook_type, (filename, {}))[1]
+ hook_dict[alias] = val
+ else:
+ opt_dict[opt] = (filename, val)
# Make the RawConfigParser forget everything (so we retain
# the original filenames that options come from)
@@ -583,7 +594,7 @@
objects.
"""
if getattr(self, 'convert_2to3_doctests', None):
- self.convert_2to3_doctests = [os.path.join(p)
+ self.convert_2to3_doctests = [os.path.join(p)
for p in self.convert_2to3_doctests]
else:
self.convert_2to3_doctests = []
@@ -948,13 +959,23 @@
if self.have_run.get(command):
return
- log.info("running %s", command)
cmd_obj = self.get_command_obj(command)
cmd_obj.ensure_finalized()
+ self.run_command_hooks(cmd_obj, 'pre_hook')
+ log.info("running %s", command)
cmd_obj.run()
+ self.run_command_hooks(cmd_obj, 'post_hook')
self.have_run[command] = 1
+ def run_command_hooks(self, cmd_obj, hook_kind):
+ hooks = getattr(cmd_obj, hook_kind)
+ if hooks is None:
+ return
+ for hook in hooks.values():
+ hook_func = resolve_dotted_name(hook)
+ hook_func(cmd_obj)
+
# -- Distribution query methods ------------------------------------
def has_pure_modules(self):
diff --git a/src/distutils2/tests/test_dist.py b/src/distutils2/tests/test_dist.py
--- a/src/distutils2/tests/test_dist.py
+++ b/src/distutils2/tests/test_dist.py
@@ -240,6 +240,49 @@
# make sure --no-user-cfg disables the user cfg file
self.assertEqual(len(all_files)-1, len(files))
+ def test_special_hooks_parsing(self):
+ temp_home = self.mkdtemp()
+ config_files = [os.path.join(temp_home, "config1.cfg"),
+ os.path.join(temp_home, "config2.cfg")]
+
+ # Store two aliased hooks in config files
+ self.write_file((temp_home, "config1.cfg"), '[test_dist]\npre-hook.a = type')
+ self.write_file((temp_home, "config2.cfg"), '[test_dist]\npre-hook.b = type')
+
+ sys.argv.extend(["--command-packages",
+ "distutils2.tests",
+ "test_dist"])
+ cmd = self.create_distribution(config_files).get_command_obj("test_dist")
+ self.assertEqual(cmd.pre_hook, {"a": 'type', "b": 'type'})
+
+
+ def test_hooks_get_run(self):
+ temp_home = self.mkdtemp()
+ config_file = os.path.join(temp_home, "config1.cfg")
+
+ self.write_file((temp_home, "config1.cfg"), textwrap.dedent('''
+ [test_dist]
+ pre-hook.test = distutils2.tests.test_dist.DistributionTestCase.log_pre_call
+ post-hook.test = distutils2.tests.test_dist.DistributionTestCase.log_post_call'''))
+
+ sys.argv.extend(["--command-packages",
+ "distutils2.tests",
+ "test_dist"])
+ d = self.create_distribution([config_file])
+ cmd = d.get_command_obj("test_dist")
+
+ # prepare the call recorders
+ record = []
+ DistributionTestCase.log_pre_call = staticmethod(lambda _cmd: record.append(('pre', _cmd)))
+ DistributionTestCase.log_post_call = staticmethod(lambda _cmd: record.append(('post', _cmd)))
+ test_dist.run = lambda _cmd: record.append(('run', _cmd))
+ test_dist.finalize_options = lambda _cmd: record.append(('finalize_options', _cmd))
+
+ d.run_command('test_dist')
+ self.assertEqual(record, [('finalize_options', cmd),
+ ('pre', cmd),
+ ('run', cmd),
+ ('post', cmd)])
class MetadataTestCase(support.TempdirManager, support.EnvironGuard,
unittest.TestCase):
diff --git a/src/distutils2/tests/test_util.py b/src/distutils2/tests/test_util.py
--- a/src/distutils2/tests/test_util.py
+++ b/src/distutils2/tests/test_util.py
@@ -18,7 +18,7 @@
_find_exe_version, _MAC_OS_X_LD_VERSION,
byte_compile, find_packages, spawn, find_executable,
_nt_quote_args, get_pypirc_path, generate_pypirc,
- read_pypirc)
+ read_pypirc, resolve_dotted_name)
from distutils2 import util
from distutils2.tests import support
@@ -342,6 +342,16 @@
res = find_packages([root], ['pkg1.pkg2'])
self.assertEqual(set(res), set(['pkg1', 'pkg5', 'pkg1.pkg3', 'pkg1.pkg3.pkg6']))
+ def test_resolve_dotted_name(self):
+ self.assertEqual(UtilTestCase, resolve_dotted_name("distutils2.tests.test_util.UtilTestCase"))
+ self.assertEqual(UtilTestCase.test_resolve_dotted_name,
+ resolve_dotted_name("distutils2.tests.test_util.UtilTestCase.test_resolve_dotted_name"))
+
+ self.assertRaises(ImportError, resolve_dotted_name,
+ "distutils2.tests.test_util.UtilTestCaseNot")
+ self.assertRaises(ImportError, resolve_dotted_name,
+ "distutils2.tests.test_util.UtilTestCase.nonexistent_attribute")
+
@unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
def test_run_2to3_on_code(self):
content = "print 'test'"
diff --git a/src/distutils2/util.py b/src/distutils2/util.py
--- a/src/distutils2/util.py
+++ b/src/distutils2/util.py
@@ -633,6 +633,24 @@
return packages
+def resolve_dotted_name(dotted_name):
+ module_name, rest = dotted_name.split('.')[0], dotted_name.split('.')[1:]
+ while len(rest) > 0:
+ try:
+ ret = __import__(module_name)
+ break
+ except ImportError:
+ if rest == []:
+ raise
+ module_name += ('.' + rest[0])
+ rest = rest[1:]
+ while len(rest) > 0:
+ try:
+ ret = getattr(ret, rest.pop(0))
+ except AttributeError:
+ raise ImportError
+ return ret
+
# utility functions for 2to3 support
def run_2to3(files, doctests_only=False, fixer_names=None, options=None,
--
Repository URL: http://hg.python.org/distutils2
More information about the Python-checkins
mailing list