diff options
author | Greg Ward <gward@python.net> | 2000-04-04 01:40:52 (GMT) |
---|---|---|
committer | Greg Ward <gward@python.net> | 2000-04-04 01:40:52 (GMT) |
commit | fe6462c1f3206dd69659d20b5babab515fc6d4c3 (patch) | |
tree | 500b1215d19bb611a3c754789434a8757bc1c8d6 /Lib/distutils/dist.py | |
parent | 6c981ad25e38a2f02a9347310bdc680755208450 (diff) | |
download | cpython-fe6462c1f3206dd69659d20b5babab515fc6d4c3.zip cpython-fe6462c1f3206dd69659d20b5babab515fc6d4c3.tar.gz cpython-fe6462c1f3206dd69659d20b5babab515fc6d4c3.tar.bz2 |
Reorganization: moved the Distribution class from core.py to dist.py, and
the Command class from core.py to cmd.py. No other code needs changing
though; distutils.core still provides the Command and Distribution classes,
although indirectly now.
Diffstat (limited to 'Lib/distutils/dist.py')
-rw-r--r-- | Lib/distutils/dist.py | 567 |
1 files changed, 567 insertions, 0 deletions
diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py new file mode 100644 index 0000000..50e7556 --- /dev/null +++ b/Lib/distutils/dist.py @@ -0,0 +1,567 @@ +"""distutils.dist + +Provides the Distribution class, which represents the module distribution +being built/installed/distributed.""" + +# created 2000/04/03, Greg Ward +# (extricated from core.py; actually dates back to the beginning) + +__revision__ = "$Id$" + +import sys, string, re +from types import * +from copy import copy +from distutils.errors import * +from distutils.fancy_getopt import fancy_getopt, print_help + + +# Regex to define acceptable Distutils command names. This is not *quite* +# the same as a Python NAME -- I don't allow leading underscores. The fact +# that they're very similar is no coincidence; the default naming scheme is +# to look for a Python module named after the command. +command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$') + + +class Distribution: + """The core of the Distutils. Most of the work hiding behind + 'setup' is really done within a Distribution instance, which + farms the work out to the Distutils commands specified on the + command line. + + Clients will almost never instantiate Distribution directly, + unless the 'setup' function is totally inadequate to their needs. + However, it is conceivable that a client might wish to subclass + Distribution for some specialized purpose, and then pass the + subclass to 'setup' as the 'distclass' keyword argument. If so, + it is necessary to respect the expectations that 'setup' has of + Distribution: it must have a constructor and methods + 'parse_command_line()' and 'run_commands()' with signatures like + those described below.""" + + + # 'global_options' describes the command-line options that may be + # supplied to the client (setup.py) prior to any actual commands. + # Eg. "./setup.py -nv" or "./setup.py --verbose" both take advantage of + # these global options. This list should be kept to a bare minimum, + # since every global option is also valid as a command option -- and we + # don't want to pollute the commands with too many options that they + # have minimal control over. + global_options = [('verbose', 'v', + "run verbosely (default)"), + ('quiet', 'q', + "run quietly (turns verbosity off)"), + ('dry-run', 'n', + "don't actually do anything"), + ('force', 'f', + "skip dependency checking between files"), + ('help', 'h', + "show this help message"), + ] + negative_opt = {'quiet': 'verbose'} + + + # -- Creation/initialization methods ------------------------------- + + def __init__ (self, attrs=None): + """Construct a new Distribution instance: initialize all the + attributes of a Distribution, and then uses 'attrs' (a + dictionary mapping attribute names to values) to assign + some of those attributes their "real" values. (Any attributes + not mentioned in 'attrs' will be assigned to some null + value: 0, None, an empty list or dictionary, etc.) Most + importantly, initialize the 'command_obj' attribute + to the empty dictionary; this will be filled in with real + command objects by 'parse_command_line()'.""" + + # Default values for our command-line options + self.verbose = 1 + self.dry_run = 0 + self.force = 0 + self.help = 0 + self.help_commands = 0 + + # And the "distribution meta-data" options -- these can only + # come from setup.py (the caller), not the command line + # (or a hypothetical config file). + self.name = None + self.version = None + self.author = None + self.author_email = None + self.maintainer = None + self.maintainer_email = None + self.url = None + self.licence = None + self.description = None + + # 'cmdclass' maps command names to class objects, so we + # can 1) quickly figure out which class to instantiate when + # we need to create a new command object, and 2) have a way + # for the client to override command classes + self.cmdclass = {} + + # These options are really the business of various commands, rather + # than of the Distribution itself. We provide aliases for them in + # Distribution as a convenience to the developer. + # dictionary. + self.packages = None + self.package_dir = None + self.py_modules = None + self.libraries = None + self.ext_modules = None + self.ext_package = None + self.include_dirs = None + self.extra_path = None + + # And now initialize bookkeeping stuff that can't be supplied by + # the caller at all. 'command_obj' maps command names to + # Command instances -- that's how we enforce that every command + # class is a singleton. + self.command_obj = {} + + # 'have_run' maps command names to boolean values; it keeps track + # of whether we have actually run a particular command, to make it + # cheap to "run" a command whenever we think we might need to -- if + # it's already been done, no need for expensive filesystem + # operations, we just check the 'have_run' dictionary and carry on. + # It's only safe to query 'have_run' for a command class that has + # been instantiated -- a false value will be inserted when the + # command object is created, and replaced with a true value when + # the command is succesfully run. Thus it's probably best to use + # '.get()' rather than a straight lookup. + self.have_run = {} + + # Now we'll use the attrs dictionary (ultimately, keyword args from + # the client) to possibly override any or all of these distribution + # options. + if attrs: + + # Pull out the set of command options and work on them + # specifically. Note that this order guarantees that aliased + # command options will override any supplied redundantly + # through the general options dictionary. + options = attrs.get ('options') + if options: + del attrs['options'] + for (command, cmd_options) in options.items(): + cmd_obj = self.find_command_obj (command) + for (key, val) in cmd_options.items(): + cmd_obj.set_option (key, val) + # loop over commands + # if any command options + + # Now work on the rest of the attributes. Any attribute that's + # not already defined is invalid! + for (key,val) in attrs.items(): + if hasattr (self, key): + setattr (self, key, val) + else: + raise DistutilsOptionError, \ + "invalid distribution option '%s'" % key + + # __init__ () + + + def parse_command_line (self, args): + """Parse the setup script's command line: set any Distribution + attributes tied to command-line options, create all command + objects, and set their options from the command-line. 'args' + must be a list of command-line arguments, most likely + 'sys.argv[1:]' (see the 'setup()' function). This list is first + processed for "global options" -- options that set attributes of + the Distribution instance. Then, it is alternately scanned for + Distutils command and options for that command. Each new + command terminates the options for the previous command. The + allowed options for a command are determined by the 'options' + attribute of the command object -- thus, we instantiate (and + cache) every command object here, in order to access its + 'options' attribute. Any error in that 'options' attribute + raises DistutilsGetoptError; any error on the command-line + raises DistutilsArgError. If no Distutils commands were found + on the command line, raises DistutilsArgError. Return true if + command-line successfully parsed and we should carry on with + executing commands; false if no errors but we shouldn't execute + commands (currently, this only happens if user asks for + help).""" + + # late import because of mutual dependence between these classes + from distutils.cmd import Command + + + # We have to parse the command line a bit at a time -- global + # options, then the first command, then its options, and so on -- + # because each command will be handled by a different class, and + # the options that are valid for a particular class aren't + # known until we instantiate the command class, which doesn't + # happen until we know what the command is. + + self.commands = [] + options = self.global_options + \ + [('help-commands', None, + "list all available commands")] + args = fancy_getopt (options, self.negative_opt, + self, sys.argv[1:]) + + # User just wants a list of commands -- we'll print it out and stop + # processing now (ie. if they ran "setup --help-commands foo bar", + # we ignore "foo bar"). + if self.help_commands: + self.print_commands () + print + print usage + return + + while args: + # Pull the current command from the head of the command line + command = args[0] + if not command_re.match (command): + raise SystemExit, "invalid command name '%s'" % command + self.commands.append (command) + + # Make sure we have a command object to put the options into + # (this either pulls it out of a cache of command objects, + # or finds and instantiates the command class). + try: + cmd_obj = self.find_command_obj (command) + except DistutilsModuleError, msg: + raise DistutilsArgError, msg + + # Require that the command class be derived from Command -- + # that way, we can be sure that we at least have the 'run' + # and 'get_option' methods. + if not isinstance (cmd_obj, Command): + raise DistutilsClassError, \ + "command class %s must subclass Command" % \ + cmd_obj.__class__ + + # Also make sure that the command object provides a list of its + # known options + if not (hasattr (cmd_obj, 'user_options') and + type (cmd_obj.user_options) is ListType): + raise DistutilsClassError, \ + ("command class %s must provide " + + "'user_options' attribute (a list of tuples)") % \ + cmd_obj.__class__ + + # Poof! like magic, all commands support the global + # options too, just by adding in 'global_options'. + negative_opt = self.negative_opt + if hasattr (cmd_obj, 'negative_opt'): + negative_opt = copy (negative_opt) + negative_opt.update (cmd_obj.negative_opt) + + options = self.global_options + cmd_obj.user_options + args = fancy_getopt (options, negative_opt, + cmd_obj, args[1:]) + if cmd_obj.help: + print_help (self.global_options, + header="Global options:") + print + print_help (cmd_obj.user_options, + header="Options for '%s' command:" % command) + print + print usage + return + + self.command_obj[command] = cmd_obj + self.have_run[command] = 0 + + # while args + + # If the user wants help -- ie. they gave the "--help" option -- + # give it to 'em. We do this *after* processing the commands in + # case they want help on any particular command, eg. + # "setup.py --help foo". (This isn't the documented way to + # get help on a command, but I support it because that's how + # CVS does it -- might as well be consistent.) + if self.help: + print_help (self.global_options, header="Global options:") + print + + for command in self.commands: + klass = self.find_command_class (command) + print_help (klass.user_options, + header="Options for '%s' command:" % command) + print + + print usage + return + + # Oops, no commands found -- an end-user error + if not self.commands: + raise DistutilsArgError, "no commands supplied" + + # All is well: return true + return 1 + + # parse_command_line() + + + def print_command_list (self, commands, header, max_length): + """Print a subset of the list of all commands -- used by + 'print_commands()'.""" + + print header + ":" + + for cmd in commands: + klass = self.cmdclass.get (cmd) + if not klass: + klass = self.find_command_class (cmd) + try: + description = klass.description + except AttributeError: + description = "(no description available)" + + print " %-*s %s" % (max_length, cmd, description) + + # print_command_list () + + + def print_commands (self): + """Print out a help message listing all available commands with + a description of each. The list is divided into "standard + commands" (listed in distutils.command.__all__) and "extra + commands" (mentioned in self.cmdclass, but not a standard + command). The descriptions come from the command class + attribute 'description'.""" + + import distutils.command + std_commands = distutils.command.__all__ + is_std = {} + for cmd in std_commands: + is_std[cmd] = 1 + + extra_commands = [] + for cmd in self.cmdclass.keys(): + if not is_std.get(cmd): + extra_commands.append (cmd) + + max_length = 0 + for cmd in (std_commands + extra_commands): + if len (cmd) > max_length: + max_length = len (cmd) + + self.print_command_list (std_commands, + "Standard commands", + max_length) + if extra_commands: + print + self.print_command_list (extra_commands, + "Extra commands", + max_length) + + # print_commands () + + + + # -- Command class/object methods ---------------------------------- + + # This is a method just so it can be overridden if desired; it doesn't + # actually use or change any attributes of the Distribution instance. + def find_command_class (self, command): + """Given a command, derives the names of the module and class + expected to implement the command: eg. 'foo_bar' becomes + 'distutils.command.foo_bar' (the module) and 'FooBar' (the + class within that module). Loads the module, extracts the + class from it, and returns the class object. + + Raises DistutilsModuleError with a semi-user-targeted error + message if the expected module could not be loaded, or the + expected class was not found in it.""" + + module_name = 'distutils.command.' + 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: + klass = vars(module)[klass_name] + except KeyError: + raise DistutilsModuleError, \ + "invalid command '%s' (no class '%s' in module '%s')" \ + % (command, klass_name, module_name) + + return klass + + # find_command_class () + + + def create_command_obj (self, command): + """Figure out the class that should implement a command, + instantiate it, cache and return the new "command object". + The "command class" is determined either by looking it up in + the 'cmdclass' attribute (this is the mechanism whereby + clients may override default Distutils commands or add their + own), or by calling the 'find_command_class()' method (if the + command name is not in 'cmdclass'.""" + + # Determine the command class -- either it's in the command_class + # dictionary, or we have to divine the module and class name + klass = self.cmdclass.get(command) + if not klass: + klass = self.find_command_class (command) + self.cmdclass[command] = klass + + # Found the class OK -- instantiate it + cmd_obj = klass (self) + return cmd_obj + + + def find_command_obj (self, command, create=1): + """Look up and return a command object in the cache maintained by + 'create_command_obj()'. If none found, the action taken + depends on 'create': if true (the default), create a new + command object by calling 'create_command_obj()' and return + it; otherwise, return None. If 'command' is an invalid + command name, then DistutilsModuleError will be raised.""" + + cmd_obj = self.command_obj.get (command) + if not cmd_obj and create: + cmd_obj = self.create_command_obj (command) + self.command_obj[command] = cmd_obj + + return cmd_obj + + + # -- Methods that operate on the Distribution ---------------------- + + def announce (self, msg, level=1): + """Print 'msg' if 'level' is greater than or equal to the verbosity + level recorded in the 'verbose' attribute (which, currently, + can be only 0 or 1).""" + + if self.verbose >= level: + print msg + + + def run_commands (self): + """Run each command that was seen on the client command line. + Uses the list of commands found and cache of command objects + created by 'create_command_obj()'.""" + + for cmd in self.commands: + self.run_command (cmd) + + + def get_option (self, option): + """Return the value of a distribution option. Raise + DistutilsOptionError if 'option' is not known.""" + + try: + return getattr (self, opt) + except AttributeError: + raise DistutilsOptionError, \ + "unknown distribution option %s" % option + + + def get_options (self, *options): + """Return (as a tuple) the values of several distribution + options. Raise DistutilsOptionError if any element of + 'options' is not known.""" + + values = [] + try: + for opt in options: + values.append (getattr (self, opt)) + except AttributeError, name: + raise DistutilsOptionError, \ + "unknown distribution option %s" % name + + return tuple (values) + + + # -- Methods that operate on its Commands -------------------------- + + def run_command (self, command): + + """Do whatever it takes to run a command (including nothing at all, + if the command has already been run). Specifically: if we have + already created and run the command named by 'command', return + silently without doing anything. If the command named by + 'command' doesn't even have a command object yet, create one. + Then invoke 'run()' on that command object (or an existing + one).""" + + # Already been here, done that? then return silently. + if self.have_run.get (command): + return + + self.announce ("running " + command) + cmd_obj = self.find_command_obj (command) + cmd_obj.ensure_ready () + cmd_obj.run () + self.have_run[command] = 1 + + + def get_command_option (self, command, option): + """Create a command object for 'command' if necessary, ensure that + its option values are all set to their final values, and return + the value of its 'option' option. Raise DistutilsOptionError if + 'option' is not known for that 'command'.""" + + cmd_obj = self.find_command_obj (command) + cmd_obj.ensure_ready () + return cmd_obj.get_option (option) + try: + return getattr (cmd_obj, option) + except AttributeError: + raise DistutilsOptionError, \ + "command %s: no such option %s" % (command, option) + + + def get_command_options (self, command, *options): + """Create a command object for 'command' if necessary, ensure that + its option values are all set to their final values, and return + a tuple containing the values of all the options listed in + 'options' for that command. Raise DistutilsOptionError if any + invalid option is supplied in 'options'.""" + + cmd_obj = self.find_command_obj (command) + cmd_obj.ensure_ready () + values = [] + try: + for opt in options: + values.append (getattr (cmd_obj, option)) + except AttributeError, name: + raise DistutilsOptionError, \ + "command %s: no such option %s" % (command, name) + + return tuple (values) + + + # -- Distribution query methods ------------------------------------ + + def has_pure_modules (self): + return len (self.packages or self.py_modules or []) > 0 + + def has_ext_modules (self): + return self.ext_modules and len (self.ext_modules) > 0 + + def has_c_libraries (self): + return self.libraries and len (self.libraries) > 0 + + def has_modules (self): + return self.has_pure_modules() or self.has_ext_modules() + + def is_pure (self): + return (self.has_pure_modules() and + not self.has_ext_modules() and + not self.has_c_libraries()) + + def get_name (self): + return self.name or "UNKNOWN" + + def get_full_name (self): + return "%s-%s" % ((self.name or "UNKNOWN"), (self.version or "???")) + +# class Distribution + + +if __name__ == "__main__": + dist = Distribution () + print "ok" |