Commit 1e8e1918 authored by Fred Drake's avatar Fred Drake

This allows additional commands to be provided for existing setup.py

scripts without modifying either the distutils installation or the
setup.py scripts of packages with which the new commands will be used.

Specifically, an option is added to distutils that allows additional
packages to be searched for command implementations in addition to
distutils.command.  The additional packages can be specified on the
command line or via the installation or personal configuration files
already loaded by distutils.

For discussion, see the thread starting with:

http://mail.python.org/pipermail/distutils-sig/2004-August/004112.html

This closes SF patch #102241.
parent ab4c0545
...@@ -141,6 +141,14 @@ class Distribution: ...@@ -141,6 +141,14 @@ class Distribution:
# for the setup script to override command classes # for the setup script to override command classes
self.cmdclass = {} 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] # '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 # and sys.argv[1:], but they can be overridden when the caller is
# not necessarily a setup script run from the command-line. # not necessarily a setup script run from the command-line.
...@@ -406,6 +414,8 @@ class Distribution: ...@@ -406,6 +414,8 @@ class Distribution:
setattr(self, alias, not strtobool(val)) setattr(self, alias, not strtobool(val))
elif opt in ('verbose', 'dry_run'): # ugh! elif opt in ('verbose', 'dry_run'): # ugh!
setattr(self, opt, strtobool(val)) setattr(self, opt, strtobool(val))
else:
setattr(self, opt, val)
except ValueError, msg: except ValueError, msg:
raise DistutilsOptionError, msg raise DistutilsOptionError, msg
...@@ -437,11 +447,12 @@ class Distribution: ...@@ -437,11 +447,12 @@ class Distribution:
# We now have enough information to show the Macintosh dialog # We now have enough information to show the Macintosh dialog
# that allows the user to interactively specify the "command line". # that allows the user to interactively specify the "command line".
# #
toplevel_options = self._get_toplevel_options()
if sys.platform == 'mac': if sys.platform == 'mac':
import EasyDialogs import EasyDialogs
cmdlist = self.get_command_list() cmdlist = self.get_command_list()
self.script_args = EasyDialogs.GetArgv( 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 # We have to parse the command line a bit at a time -- global
# options, then the first command, then its options, and so on -- # options, then the first command, then its options, and so on --
...@@ -451,7 +462,7 @@ class Distribution: ...@@ -451,7 +462,7 @@ class Distribution:
# until we know what the command is. # until we know what the command is.
self.commands = [] 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_negative_aliases(self.negative_opt)
parser.set_aliases({'licence': 'license'}) parser.set_aliases({'licence': 'license'})
args = parser.getopt(args=self.script_args, object=self) args = parser.getopt(args=self.script_args, object=self)
...@@ -488,6 +499,17 @@ class Distribution: ...@@ -488,6 +499,17 @@ class Distribution:
# parse_command_line() # 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): def _parse_command_opts (self, parser, args):
"""Parse the command-line options for a single command. """Parse the command-line options for a single command.
'parser' must be a FancyGetopt instance; 'args' must be the list 'parser' must be a FancyGetopt instance; 'args' must be the list
...@@ -586,7 +608,6 @@ class Distribution: ...@@ -586,7 +608,6 @@ class Distribution:
# _parse_command_opts () # _parse_command_opts ()
def finalize_options (self): def finalize_options (self):
"""Set final values for all the options on the Distribution """Set final values for all the options on the Distribution
instance, analogous to the .finalize_options() method of Command instance, analogous to the .finalize_options() method of Command
...@@ -627,7 +648,11 @@ class Distribution: ...@@ -627,7 +648,11 @@ class Distribution:
from distutils.cmd import Command from distutils.cmd import Command
if global_options: 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:") parser.print_help("Global options:")
print print
...@@ -791,6 +816,19 @@ class Distribution: ...@@ -791,6 +816,19 @@ class Distribution:
# -- Command class/object methods ---------------------------------- # -- 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): def get_command_class (self, command):
"""Return the class that implements the Distutils command named by """Return the class that implements the Distutils command named by
'command'. First we check the 'cmdclass' dictionary; if the 'command'. First we check the 'cmdclass' dictionary; if the
...@@ -807,26 +845,28 @@ class Distribution: ...@@ -807,26 +845,28 @@ class Distribution:
if klass: if klass:
return klass return klass
module_name = 'distutils.command.' + command for pkgname in self.get_command_packages():
klass_name = command module_name = "%s.%s" % (pkgname, command)
klass_name = command
try: try:
__import__ (module_name) __import__ (module_name)
module = sys.modules[module_name] module = sys.modules[module_name]
except ImportError: except ImportError:
raise DistutilsModuleError, \ continue
"invalid command '%s' (no module named '%s')" % \
(command, 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)
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 () # get_command_class ()
......
"""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)
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