Index: Lib/distutils/dist.py =================================================================== RCS file: /cvsroot/python/python/dist/src/Lib/distutils/dist.py,v retrieving revision 1.68 diff -u -d -r1.68 dist.py --- Lib/distutils/dist.py 18 Jul 2004 06:14:42 -0000 1.68 +++ Lib/distutils/dist.py 3 Aug 2004 15:16:29 -0000 @@ -141,6 +141,14 @@ # for the setup script to override command classes self.cmdclass = {} + # 'command_packages' is a list of packages in which commands + # are searched for. The factory for command 'foo' is expected + # to be named 'foo' in the module 'foo' in one of the packages + # named here. This list is searched from the left; an error + # is raised if no named package provides the command being + # searched for. (Always access using get_command_packages().) + self.command_packages = None + # 'script_name' and 'script_args' are usually set to sys.argv[0] # and sys.argv[1:], but they can be overridden when the caller is # not necessarily a setup script run from the command-line. @@ -406,6 +414,8 @@ setattr(self, alias, not strtobool(val)) elif opt in ('verbose', 'dry_run'): # ugh! setattr(self, opt, strtobool(val)) + else: + setattr(self, opt, val) except ValueError, msg: raise DistutilsOptionError, msg @@ -437,11 +447,12 @@ # We now have enough information to show the Macintosh dialog # that allows the user to interactively specify the "command line". # + toplevel_options = self._get_toplevel_options() if sys.platform == 'mac': import EasyDialogs cmdlist = self.get_command_list() self.script_args = EasyDialogs.GetArgv( - self.global_options + self.display_options, cmdlist) + toplevel_options + self.display_options, cmdlist) # We have to parse the command line a bit at a time -- global # options, then the first command, then its options, and so on -- @@ -451,7 +462,7 @@ # until we know what the command is. self.commands = [] - parser = FancyGetopt(self.global_options + self.display_options) + parser = FancyGetopt(toplevel_options + self.display_options) parser.set_negative_aliases(self.negative_opt) parser.set_aliases({'licence': 'license'}) args = parser.getopt(args=self.script_args, object=self) @@ -488,6 +499,17 @@ # parse_command_line() + def _get_toplevel_options (self): + """Return the non-display options recognized at the top level. + + This includes options that are recognized *only* at the top + level as well as options recognized for commands. + """ + return self.global_options + [ + ("command-packages=", None, + "list of packages that provide distutils commands"), + ] + def _parse_command_opts (self, parser, args): """Parse the command-line options for a single command. 'parser' must be a FancyGetopt instance; 'args' must be the list @@ -586,7 +608,6 @@ # _parse_command_opts () - def finalize_options (self): """Set final values for all the options on the Distribution instance, analogous to the .finalize_options() method of Command @@ -627,7 +648,11 @@ from distutils.cmd import Command if global_options: - parser.set_option_table(self.global_options) + if display_options: + options = self._get_toplevel_options() + else: + options = self.global_options + parser.set_option_table(options) parser.print_help("Global options:") print @@ -791,6 +816,19 @@ # -- Command class/object methods ---------------------------------- + def get_command_packages (self): + """Return a list of packages from which commands are loaded.""" + pkgs = self.command_packages + if not isinstance(pkgs, type([])): + pkgs = string.split(pkgs or "", ",") + for i in range(len(pkgs)): + pkgs[i] = string.strip(pkgs[i]) + pkgs = filter(None, pkgs) + if "distutils.command" not in pkgs: + pkgs.insert(0, "distutils.command") + self.command_packages = pkgs + return pkgs + def get_command_class (self, command): """Return the class that implements the Distutils command named by 'command'. First we check the 'cmdclass' dictionary; if the @@ -807,26 +845,28 @@ if klass: return klass - module_name = 'distutils.command.' + command - klass_name = command + for pkgname in self.get_command_packages(): + module_name = "%s.%s" % (pkgname, command) + klass_name = command - try: - __import__ (module_name) - module = sys.modules[module_name] - except ImportError: - raise DistutilsModuleError, \ - "invalid command '%s' (no module named '%s')" % \ - (command, module_name) + try: + __import__ (module_name) + module = sys.modules[module_name] + except ImportError: + continue - try: - klass = getattr(module, klass_name) - except AttributeError: - raise DistutilsModuleError, \ - "invalid command '%s' (no class '%s' in module '%s')" \ - % (command, klass_name, module_name) + try: + klass = getattr(module, klass_name) + except AttributeError: + raise DistutilsModuleError, \ + "invalid command '%s' (no class '%s' in module '%s')" \ + % (command, klass_name, module_name) + + self.cmdclass[command] = klass + return klass + + raise DistutilsModuleError("invalid command '%s'" % command) - self.cmdclass[command] = klass - return klass # get_command_class () Index: Lib/distutils/tests/test_dist.py =================================================================== RCS file: Lib/distutils/tests/test_dist.py diff -N Lib/distutils/tests/test_dist.py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ Lib/distutils/tests/test_dist.py 3 Aug 2004 15:16:29 -0000 @@ -0,0 +1,100 @@ +"""Tests for distutils.dist.""" + +import distutils.cmd +import distutils.dist +import os +import shutil +import sys +import tempfile +import unittest + +from test.test_support import TESTFN + + +class test_dist(distutils.cmd.Command): + """Sample distutils extension command.""" + + user_options = [ + ("sample-option=", "S", "help text"), + ] + + def initialize_options(self): + self.sample_option = None + + +class TestDistribution(distutils.dist.Distribution): + """Distribution subclasses that avoids the default search for + configuration files. + + The ._config_files attribute must be set before + .parse_config_files() is called. + """ + + def find_config_files(self): + return self._config_files + + +class DistributionTestCase(unittest.TestCase): + + def setUp(self): + self.argv = sys.argv[:] + del sys.argv[1:] + + def tearDown(self): + sys.argv[:] = self.argv + + def create_distribution(self, configfiles=()): + d = TestDistribution() + d._config_files = configfiles + d.parse_config_files() + d.parse_command_line() + return d + + def test_command_packages_unspecified(self): + sys.argv.append("build") + d = self.create_distribution() + self.assertEqual(d.get_command_packages(), ["distutils.command"]) + + def test_command_packages_cmdline(self): + sys.argv.extend(["--command-packages", + "foo.bar,distutils.tests", + "test_dist", + "-Ssometext", + ]) + d = self.create_distribution() + # let's actually try to load our test command: + self.assertEqual(d.get_command_packages(), + ["distutils.command", "foo.bar", "distutils.tests"]) + cmd = d.get_command_obj("test_dist") + self.assert_(isinstance(cmd, test_dist)) + self.assertEqual(cmd.sample_option, "sometext") + + def test_command_packages_configfile(self): + sys.argv.append("build") + f = open(TESTFN, "w") + try: + print >>f, "[global]" + print >>f, "command_packages = foo.bar, splat" + f.close() + d = self.create_distribution([TESTFN]) + self.assertEqual(d.get_command_packages(), + ["distutils.command", "foo.bar", "splat"]) + + # ensure command line overrides config: + sys.argv[1:] = ["--command-packages", "spork", "build"] + d = self.create_distribution([TESTFN]) + self.assertEqual(d.get_command_packages(), + ["distutils.command", "spork"]) + + # Setting --command-packages to '' should cause the default to + # be used even if a config file specified something else: + sys.argv[1:] = ["--command-packages", "", "build"] + d = self.create_distribution([TESTFN]) + self.assertEqual(d.get_command_packages(), ["distutils.command"]) + + finally: + os.unlink(TESTFN) + + +def test_suite(): + return unittest.makeSuite(DistributionTestCase)