[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