summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/dist/dist.tex31
-rw-r--r--Lib/distutils/dist.py84
-rw-r--r--Lib/distutils/tests/test_dist.py100
3 files changed, 193 insertions, 22 deletions
diff --git a/Doc/dist/dist.tex b/Doc/dist/dist.tex
index 5a95d78..784c908 100644
--- a/Doc/dist/dist.tex
+++ b/Doc/dist/dist.tex
@@ -1946,6 +1946,37 @@ This approach is most valuable if the new implementations must be used
to use a particular package, as everyone interested in the package
will need to have the new command implementation.
+Beginning with Python 2.4, a third option is available, intended to
+allow new commands to be added which can support existing
+\file{setup.py} scripts without requiring modifications to the Python
+installation. This is expected to allow third-party extensions to
+provide support for additional packaging systems, but the commands can
+be used for anything distutils commands can be used for. A new
+configuration option, \option{command\_packages} (command-line option
+\longprogramopt{command-packages}), can be used to specify additional
+packages to be searched for modules implementing commands. Like all
+distutils options, this can be specified on the command line or in a
+configuration file. This option can only be set in the
+\code{[global]} section of a configuration file, or before any
+commands on the command line. If set in a configuration file, it can
+be overridden from the command line; setting it to an empty string on
+the command line causes the default to be used. This should never be
+set in a configuration file provided with a package.
+
+This new option can be used to add any number of packages to the list
+of packages searched for command implementations; multiple package
+names should be separated by commas. When not specified, the search
+is only performed in the \module{distutils.command} package. When
+\file{setup.py} is run with the option
+\longprogramopt{command-packages} \programopt{distcmds,buildcmds},
+however, the packages \module{distutils.command}, \module{distcmds},
+and \module{buildcmds} will be searched in that order. New commands
+are expected to be implemented in modules of the same name as the
+command by classes sharing the same name. Given the example command
+line option above, the command \command{bdist\_openpkg} could be
+implemented by the class \class{distcmds.bdist_openpkg.bdist_openpkg}
+or \class{buildcmds.bdist_openpkg.bdist_openpkg}.
+
\chapter{Command Reference}
\label{reference}
diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py
index b4dd0b9..53846e9 100644
--- a/Lib/distutils/dist.py
+++ b/Lib/distutils/dist.py
@@ -141,6 +141,14 @@ class Distribution:
# 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 @@ class Distribution:
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 @@ class Distribution:
# 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 @@ class Distribution:
# 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 @@ class Distribution:
# 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 @@ class Distribution:
# _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 @@ class Distribution:
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 @@ class Distribution:
# -- 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 @@ class Distribution:
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)
+
+ self.cmdclass[command] = klass
+ return klass
+
+ raise DistutilsModuleError("invalid command '%s'" % command)
- 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
# get_command_class ()
diff --git a/Lib/distutils/tests/test_dist.py b/Lib/distutils/tests/test_dist.py
new file mode 100644
index 0000000..695f6d8
--- /dev/null
+++ b/Lib/distutils/tests/test_dist.py
@@ -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)