diff --git a/Lib/distutils/core.py b/Lib/distutils/core.py new file mode 100644 index 000000000000..3a7443c78e97 --- /dev/null +++ b/Lib/distutils/core.py @@ -0,0 +1,597 @@ +"""distutils.core + +The only module that needs to be imported to use the Distutils; provides +the 'setup' function (which must be called); the 'Distribution' class +(which may be subclassed if additional functionality is desired), and +the 'Command' class (which is used both internally by Distutils, and +may be subclassed by clients for still more flexibility).""" + +# created 1999/03/01, Greg Ward + +__rcsid__ = "$Id$" + +import sys +import string, re +from distutils.errors import * +from distutils.fancy_getopt import fancy_getopt + +# 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... +command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$') + +# Defining this as a global is probably inadequate -- what about +# listing the available options (or even commands, which can vary +# quite late as well) +usage = '%s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]' % sys.argv[0] + + + +def setup (**attrs): + """The gateway to the Distutils: do everything your setup script + needs to do, in a highly flexible and user-driven way. Briefly: + create a Distribution instance; parse the command-line, creating + and customizing instances of the command class for each command + found on the command-line; run each of those commands. + + The Distribution instance might be an instance of a class + supplied via the 'distclass' keyword argument to 'setup'; if no + such class is supplied, then the 'Distribution' class (also in + this module) is instantiated. All other arguments to 'setup' + (except for 'cmdclass') are used to set attributes of the + Distribution instance. + + The 'cmdclass' argument, if supplied, is a dictionary mapping + command names to command classes. Each command encountered on the + command line will be turned into a command class, which is in turn + instantiated; any class found in 'cmdclass' is used in place of the + default, which is (for command 'foo_bar') class 'FooBar' in module + 'distutils.command.foo_bar'. The command object must provide an + 'options' attribute which is a list of option specifiers for + 'distutils.fancy_getopt'. Any command-line options between the + current and the next command are used to set attributes in the + current command object. + + When the entire command-line has been successfully parsed, calls the + 'run' method on each command object in turn. This method will be + driven entirely by the Distribution object (which each command + object has a reference to, thanks to its constructor), and the + command-specific options that became attributes of each command + object.""" + + # Determine the distribution class -- either caller-supplied or + # our Distribution (see below). + klass = attrs.get ('distclass') + if klass: + del attrs['distclass'] + else: + klass = Distribution + + # Create the Distribution instance, using the remaining arguments + # (ie. everything except distclass) to initialize it + dist = klass (attrs) + + # Get it to parse the command line; any command-line errors are + # the end-users fault, so turn them into SystemExit to suppress + # tracebacks. + try: + dist.parse_command_line (sys.argv[1:]) + except DistutilsArgError, msg: + raise SystemExit, msg + + # And finally, run all the commands found on the command line. + dist.run_commands () + +# setup () + + +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. + global_options = [('verbose', 'v', "run verbosely"), + ('dry-run', 'n', "don't actually do anything"), + ] + + + # -- 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 = 0 + self.dry_run = 0 + + # And for all other attributes (stuff that might be passed in + # from setup.py, rather than from the end-user) + self.name = None + self.version = None + self.author = None + self.licence = None + self.description = None + + self.cmdclass = {} + + # The rest of these are really the business of various commands, + # rather than of the Distribution itself. However, they have + # to be here as a conduit to the relevant command class. + self.py_modules = None + self.ext_modules = None + self.package = None + + # Now we'll use the attrs dictionary to possibly override + # any or all of these distribution options + if attrs: + for k in attrs.keys(): + setattr (self, k, attrs[k]) + + # And now initialize bookkeeping stuff that can't be supplied by + # the caller at all + self.command_obj = {} + + # __init__ () + + + def parse_command_line (self, args): + """Parse the client'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.""" + + # 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 = [] + args = fancy_getopt (self.global_options, self, sys.argv[1:]) + + 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) + + # Have to instantiate the command class now, so we have a + # way to get its valid options and somewhere to put the + # results of parsing its share of the command-line + cmd_obj = self.create_command_obj (command) + + # 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__ + + # XXX this assumes that cmd_obj provides an 'options' + # attribute, but we're not enforcing that anywhere! + args = fancy_getopt (cmd_obj.options, cmd_obj, args[1:]) + self.command_obj[command] = cmd_obj + + # while args + + # Oops, no commands found -- an end-user error + if not self.commands: + sys.stderr.write (usage + "\n") + raise DistutilsArgError, "no commands supplied" + + # parse_command_line() + + + # -- 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 = string.join \ + (map (string.capitalize, string.split (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.""" + + 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): + """Create a command object for 'command' if necessary, and + run the command by invoking its 'run()' method.""" + + self.announce ("running " + command) + cmd_obj = self.find_command_obj (command) + cmd_obj.run () + + + def get_command_option (self, command, option): + """Create a command object for 'command' if necessary, finalize + its option values by invoking its 'set_final_options()' + method, 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.set_final_options () + 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, finalize + its option values by invoking its 'set_final_options()' + method, and return the values of all the options listed in + 'options' for that command. Raise DistutilsOptionError if + 'option' is not known for that 'command'.""" + + cmd_obj = self.find_command_obj (command) + cmd_obj.set_final_options () + 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) + +# end class Distribution + + +class Command: + """Abstract base class for defining command classes, the "worker bees" + of the Distutils. A useful analogy for command classes is to + think of them as subroutines with local variables called + "options". The options are "declared" in 'set_initial_options()' + and "initialized" (given their real values) in + 'set_final_options()', both of which must be defined by every + command class. The distinction between the two is necessary + because option values might come from the outside world (command + line, option file, ...), and any options dependent on other + options must be computed *after* these outside influences have + been processed -- hence 'set_final_values()'. The "body" of the + subroutine, where it does all its work based on the values of its + options, is the 'run()' method, which must also be implemented by + every command class.""" + + # -- Creation/initialization methods ------------------------------- + + def __init__ (self, dist): + """Create and initialize a new Command object. Most importantly, + invokes the 'set_default_options()' method, which is the + real initializer and depends on the actual command being + instantiated.""" + + if not isinstance (dist, Distribution): + raise TypeError, "dist must be a Distribution instance" + if self.__class__ is Command: + raise RuntimeError, "Command is an abstract class" + + self.distribution = dist + self.set_default_options () + + # end __init__ () + + # Subclasses must define: + # set_default_options() + # provide default values for all options; may be overridden + # by Distutils client, by command-line options, or by options + # from option file + # set_final_options() + # decide on the final values for all options; this is called + # after all possible intervention from the outside world + # (command-line, option file, etc.) has been processed + # run() + # run the command: do whatever it is we're here to do, + # controlled by the command's various option values + + def set_default_options (self): + """Set default values for all the options that this command + supports. Note that these defaults may be overridden + by the command-line supplied by the user; thus, this is + not the place to code dependencies between options; generally, + 'set_default_options()' implementations are just a bunch + of "self.foo = None" assignments. + + This method must be implemented by all command classes.""" + + raise RuntimeError, \ + "abstract method -- subclass %s must override" % self.__class__ + + def set_final_options (self): + """Set final values for all the options that this command + supports. This is always called as late as possible, ie. + after any option assignments from the command-line or from + other commands have been done. Thus, this is the place to to + code option dependencies: if 'foo' depends on 'bar', then it + is safe to set 'foo' from 'bar' as long as 'foo' still has + the same value it was assigned in 'set_default_options()'. + + This method must be implemented by all command classes.""" + + raise RuntimeError, \ + "abstract method -- subclass %s must override" % self.__class__ + + def run (self): + """A command's raison d'etre: carry out the action it exists + to perform, controlled by the options initialized in + 'set_initial_options()', customized by the user and other + commands, and finalized in 'set_final_options()'. All + terminal output and filesystem interaction should be done by + 'run()'. + + This method must be implemented by all command classes.""" + + raise RuntimeError, \ + "abstract method -- subclass %s must override" % self.__class__ + + def announce (self, msg, level=1): + """If the Distribution instance to which this command belongs + has a verbosity level of greater than or equal to 'level' + print 'msg' to stdout.""" + if self.distribution.verbose >= level: + print msg + + + # -- Option query/set methods -------------------------------------- + + def get_option (self, option): + """Return the value of a single option for this command. Raise + DistutilsOptionError if 'option' is not known.""" + try: + return getattr (self, option) + except AttributeError: + raise DistutilsOptionError, \ + "command %s: no such option %s" % \ + (self.command_name(), option) + + + def get_options (self, *options): + """Return (as a tuple) the values of several options for this + command. Raise DistutilsOptionError if any of the options in + 'options' are not known.""" + + values = [] + try: + for opt in options: + values.append (getattr (self, opt)) + except AttributeError, name: + raise DistutilsOptionError, \ + "command %s: no such option %s" % \ + (self.command_name(), name) + + return tuple (values) + + + def set_option (self, option, value): + """Set the value of a single option for this command. Raise + DistutilsOptionError if 'option' is not known.""" + + if not hasattr (self, option): + raise DistutilsOptionError, \ + "command %s: no such option %s" % \ + (self.command_name(), option) + if value is not None: + setattr (self, option, value) + + def set_options (self, **optval): + """Set the values of several options for this command. Raise + DistutilsOptionError if any of the options specified as + keyword arguments are not known.""" + + for k in optval.keys(): + if optval[k] is not None: + self.set_option (k, optval[k]) + + + # -- Convenience methods for commands ------------------------------ + + def set_undefined_options (self, src_cmd, *option_pairs): + """Set the values of any "undefined" options from corresponding + option values in some other command object. "Undefined" here + means "is None", which is the convention used to indicate + that an option has not been changed between + 'set_initial_values()' and 'set_final_values()'. Usually + called from 'set_final_values()' for options that depend on + some other command rather than another option of the same + command. 'src_cmd' is the other command from which option + values will be taken (a command object will be created for it + if necessary); the remaining arguments are + '(src_option,dst_option)' tuples which mean "take the value + of 'src_option' in the 'src_cmd' command object, and copy it + to 'dst_option' in the current command object".""" + + # Option_pairs: list of (src_option, dst_option) tuples + + src_cmd_obj = self.distribution.find_command_obj (src_cmd) + src_cmd_obj.set_final_options () + try: + for (src_option, dst_option) in option_pairs: + if getattr (self, dst_option) is None: + self.set_option (dst_option, + src_cmd_obj.get_option (src_option)) + except AttributeError, name: + # duh, which command? + raise DistutilsOptionError, "unknown option %s" % name + + + def set_peer_option (self, command, option, value): + """Attempt to simulate a command-line override of some option + value in another command. Creates a command object for + 'command' if necessary, sets 'option' to 'value', and invokes + 'set_final_options()' on that command object. This will only + have the desired effect if the command object for 'command' + has not previously been created. Generally this is used to + ensure that the options in 'command' dependent on 'option' + are computed, hopefully (but not necessarily) deriving from + 'value'. It might be more accurate to call this method + 'influence_dependent_peer_options()'.""" + + cmd_obj = self.distribution.find_command_obj (command) + cmd_obj.set_option (option, value) + cmd_obj.set_final_options () + + + def run_peer (self, command): + """Run some other command: uses the 'run_command()' method of + Distribution, which creates the command object if necessary + and then invokes its 'run()' method.""" + + self.distribution.run_command (command) + +# end class Command diff --git a/Lib/distutils/errors.py b/Lib/distutils/errors.py new file mode 100644 index 000000000000..6605ad2cb161 --- /dev/null +++ b/Lib/distutils/errors.py @@ -0,0 +1,63 @@ +"""distutils.errors + +Provides exceptions used by the Distutils modules. Note that Distutils +modules may raise standard exceptions; in particular, SystemExit is +usually raised for errors that are obviously the end-user's fault +(eg. bad command-line arguments). + +This module safe to use in "from ... import *" mode; it only exports +symbols whose names start with "Distutils" and end with "Error".""" + +# created 1999/03/03, Greg Ward + +__rcsid__ = "$Id$" + +from types import * + +if type (RuntimeError) is ClassType: + + # DistutilsError is the root of all Distutils evil. + class DistutilsError (Exception): + pass + + # DistutilsModuleError is raised if we are unable to load an expected + # module, or find an expected class within some module + class DistutilsModuleError (DistutilsError): + pass + + # DistutilsClassError is raised if we encounter a distribution or command + # class that's not holding up its end of the bargain. + class DistutilsClassError (DistutilsError): + pass + + # DistutilsGetoptError (help me -- I have JavaProgrammersDisease!) is + # raised if the option table provided to fancy_getopt is bogus. + class DistutilsGetoptError (DistutilsError): + pass + + # DistutilsArgError is raised by fancy_getopt in response to getopt.error; + # distutils.core then turns around and raises SystemExit from that. (Thus + # client code should never see DistutilsArgError.) + class DistutilsArgError (DistutilsError): + pass + + # DistutilsFileError is raised for any problems in the filesystem: + # expected file not found, etc. + class DistutilsFileError (DistutilsError): + pass + + # DistutilsOptionError is raised anytime an attempt is made to access + # (get or set) an option that does not exist for a particular command + # (or for the distribution itself). + class DistutilsOptionError (DistutilsError): + pass + +# String-based exceptions +else: + DistutilsError = 'DistutilsError' + DistutilsModuleError = 'DistutilsModuleError' + DistutilsClassError = 'DistutilsClassError' + DistutilsGetoptError = 'DistutilsGetoptError' + DistutilsArgError = 'DistutilsArgError' + DistutilsFileError = 'DistutilsFileError' + DistutilsOptionError = 'DistutilsOptionError' diff --git a/Lib/distutils/fancy_getopt.py b/Lib/distutils/fancy_getopt.py new file mode 100644 index 000000000000..c63ce61b8ed4 --- /dev/null +++ b/Lib/distutils/fancy_getopt.py @@ -0,0 +1,115 @@ +"""distutils.fancy_getopt + +Wrapper around the standard getopt module that provides the following +additional features: + * short and long options are tied together + * options have help strings, so fancy_getopt could potentially + create a complete usage summary + * options set attributes of a passed-in object +""" + +# created 1999/03/03, Greg Ward + +__rcsid__ = "$Id$" + +import string, re +from types import * +import getopt +from distutils.errors import * + +# Much like command_re in distutils.core, this is close to but not quite +# the same as a Python NAME -- except, in the spirit of most GNU +# utilities, we use '-' in place of '_'. (The spirit of LISP lives on!) +# The similarities to NAME are again not a coincidence... +longopt_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9-]*)$') + +# This is used to translate long options to legitimate Python identifiers +# (for use as attributes of some object). +longopt_xlate = string.maketrans ('-', '_') + + +def fancy_getopt (options, object, args): + + # The 'options' table is a list of 3-tuples: + # (long_option, short_option, help_string) + # if an option takes an argument, its long_option should have '=' + # appended; short_option should just be a single character, no ':' in + # any case. If a long_option doesn't have a corresponding + # short_option, short_option should be None. All option tuples must + # have long options. + + # Build the short_opts string and long_opts list, remembering how + # the two are tied together + + short_opts = [] # we'll join 'em when done + long_opts = [] + short2long = {} + attr_name = {} + takes_arg = {} + + for (long, short, help) in options: + # Type-check the option names + if type (long) is not StringType or len (long) < 2: + raise DistutilsGetoptError, \ + "long option must be a string of length >= 2" + + if (not ((short is None) or + (type (short) is StringType and len (short) == 1))): + raise DistutilsGetoptError, \ + "short option must be None or string of length 1" + + long_opts.append (long) + + if long[-1] == '=': # option takes an argument? + if short: short = short + ':' + long = long[0:-1] + takes_arg[long] = 1 + else: + takes_arg[long] = 0 + + # Now enforce some bondage on the long option name, so we can later + # translate it to an attribute name in 'object'. Have to do this a + # bit late to make sure we've removed any trailing '='. + if not longopt_re.match (long): + raise DistutilsGetoptError, \ + ("invalid long option name '%s' " + + "(must be letters, numbers, hyphens only") % long + + attr_name[long] = string.translate (long, longopt_xlate) + if short: + short_opts.append (short) + short2long[short[0]] = long + + # end loop over 'options' + + short_opts = string.join (short_opts) + try: + (opts, args) = getopt.getopt (args, short_opts, long_opts) + except getopt.error, msg: + raise DistutilsArgError, msg + + for (opt, val) in opts: + if len (opt) == 2 and opt[0] == '-': # it's a short option + opt = short2long[opt[1]] + + elif len (opt) > 2 and opt[0:2] == '--': + opt = opt[2:] + + else: + raise RuntimeError, "getopt lies! (bad option string '%s')" % \ + opt + + attr = attr_name[opt] + if takes_arg[opt]: + setattr (object, attr, val) + else: + if val == '': + setattr (object, attr, 1) + else: + raise RuntimeError, "getopt lies! (bad value '%s')" % value + + # end loop over options found in 'args' + + return args + +# end fancy_getopt() diff --git a/Lib/distutils/options.py b/Lib/distutils/options.py new file mode 100644 index 000000000000..f6cae82dc50a --- /dev/null +++ b/Lib/distutils/options.py @@ -0,0 +1,111 @@ +# XXX this is ridiculous! if commands need to pass options around, +# they can just pass them via the 'run' method... what we REALLY need +# is a way for commands to get at each other, via the Distribution! + +class Options: + """Used by Distribution and Command to encapsulate distribution + and command options -- parsing them from command-line arguments, + passing them between the distribution and command objects, etc.""" + + # -- Creation/initialization methods ------------------------------- + + def __init__ (self, owner): + + # 'owner' is the object (presumably either a Distribution + # or Command instance) to which this set of options applies. + self.owner = owner + + # The option table: maps option names to dictionaries, which + # look something like: + # { 'longopt': long command-line option string (optional) + # 'shortopt': short option (1 char) (optional) + # 'type': 'string', 'boolean', or 'list' + # 'description': text description (eg. for help strings) + # 'default': default value for the option + # 'send': list of (cmd,option) tuples: send option down the line + # 'receive': (cmd,option) tuple: pull option from upstream + # } + self.table = {} + + + def set_basic_options (self, *options): + """Add very basic options: no separate longopt, no fancy typing, no + send targets or receive destination. The arguments should just + be {1..4}-tuples of + (name [, shortopt [, description [, default]]]) + If name ends with '=', the option takes a string argument; + otherwise it's boolean.""" + + for opt in options: + if not (type (opt) is TupleType and 1 <= len (opt) <= 4): + raise ValueError, \ + ("invalid basic option record '%s': " + \ + "must be tuple of length 1 .. 4") % opt + + elements = ('name', 'shortopt', 'description', 'default') + name = opt[0] + self.table[name] = {} + for i in range (1,4): + if len (opt) >= i: + self.table[name][elements[i]] = opt[i] + else: + break + + # set_basic_options () + + + def add_option (self, name, **args): + + # XXX should probably sanity-check the keys of args + self.table[name] = args + + + # ------------------------------------------------------------------ + + # These are in the order that they will execute in to ensure proper + # prioritizing of option sources -- the default value is the most + # basic; it can be overridden by "client options" (the keyword args + # passed from setup.py to the 'setup' function); they in turn lose to + # options passed in "from above" (ie. from the Distribution, or from + # higher-level Commands); these in turn may be overridden by + # command-line arguments (which come from the end-user, the runner of + # setup.py). Only when all this is done can we pass options down to + # other Commands. + + # Hmmm, it also matters in which order Commands are processed: should a + # command-line option to 'make_blib' take precedence over the + # corresponding value passed down from its boss, 'build'? + + def set_defaults (self): + pass + + def set_client_options (self, options): + # 'self' should be a Distribution instance for this one -- + # this is to process the kw args passed to 'setup' + pass + + def receive_option (self, option, value): + # do we need to know the identity of the sender? don't + # think we should -- too much B&D + + # oh, 'self' should be anything *but* a Distribution (ie. + # a Command instance) -- only Commands take orders from above! + # (ironically enough) + pass + + def parse_command_line (self, args): + # here, 'self' can usefully be either a Distribution (for parsing + # "global" command-line options) or a Command (for "command-specific" + # options) + pass + + + def send_option (self, option, dest): + # perhaps this should not take a dest, but send the option + # to all possible receivers? + pass + + + # ------------------------------------------------------------------ + +# class Options diff --git a/Lib/distutils/util.py b/Lib/distutils/util.py new file mode 100644 index 000000000000..7c13abe09d58 --- /dev/null +++ b/Lib/distutils/util.py @@ -0,0 +1,245 @@ +"""distutils.util + +General-purpose utility functions used throughout the Distutils +(especially in command classes). Mostly filesystem manipulation, but +not limited to that. The functions in this module generally raise +DistutilsFileError when they have problems with the filesystem, because +os.error in pre-1.5.2 Python only gives the error message and not the +file causing it.""" + +# created 1999/03/08, Greg Ward + +__rcsid__ = "$Id$" + +import os +from distutils.errors import * + + +# I don't use os.makedirs because a) it's new to Python 1.5.2, and +# b) it blows up if the directory already exists (I want to silently +# succeed in that case). +def mkpath (name, mode=0777, verbose=0): + """Create a directory and any missing ancestor directories. If the + directory already exists, return silently. Raise + DistutilsFileError if unable to create some directory along the + way (eg. some sub-path exists, but is a file rather than a + directory). If 'verbose' is true, print a one-line summary of + each mkdir to stdout.""" + + # XXX what's the better way to handle verbosity? print as we create + # each directory in the path (the current behaviour), or only announce + # the creation of the whole path, and force verbose=0 on all sub-calls? + + if os.path.isdir (name): + return + + (head, tail) = os.path.split (name) + tails = [tail] # stack of lone dirs to create + + while head and tail and not os.path.isdir (head): + #print "splitting '%s': " % head, + (head, tail) = os.path.split (head) + #print "to ('%s','%s')" % (head, tail) + tails.insert (0, tail) # push next higher dir onto stack + + #print "stack of tails:", tails + + # now 'head' contains the highest directory that already exists + for d in tails: + #print "head = %s, d = %s: " % (head, d), + head = os.path.join (head, d) + if verbose: + print "creating", head + try: + os.mkdir (head) + except os.error, (errno, errstr): + raise DistutilsFileError, "%s: %s" % (head, errstr) + +# mkpath () + + +def newer (file1, file2): + """Return true if file1 exists and is more recently modified than + file2, or if file1 exists and file2 doesn't. Return false if both + exist and file2 is the same age or younger than file1. Raises + DistutilsFileError if file1 does not exist.""" + + if not os.path.exists (file1): + raise DistutilsFileError, "file '%s' does not exist" % file1 + if not os.path.exists (file2): + return 1 + + from stat import * + mtime1 = os.stat(file1)[ST_MTIME] + mtime2 = os.stat(file2)[ST_MTIME] + + return mtime1 > mtime2 + +# newer () + + +def make_file (src, dst, func, args, + verbose=0, update_message=None, noupdate_message=None): + """Makes 'dst' from 'src' (both filenames) by calling 'func' with + 'args', but only if it needs to: i.e. if 'dst' does not exist or + 'src' is newer than 'dst'.""" + + if newer (src, dst): + if verbose and update_message: + print update_message + apply (func, args) + else: + if verbose and noupdate_message: + print noupdate_message + +# make_file () + + +def _copy_file_contents (src, dst, buffer_size=16*1024): + """Copy the file 'src' to 'dst'; both must be filenames. Any error + opening either file, reading from 'src', or writing to 'dst', + raises DistutilsFileError. Data is read/written in chunks of + 'buffer_size' bytes (default 16k). No attempt is made to handle + anything apart from regular files.""" + + # Stolen from shutil module in the standard library, but with + # custom error-handling added. + + fsrc = None + fdst = None + try: + try: + fsrc = open(src, 'rb') + except os.error, (errno, errstr): + raise DistutilsFileError, "could not open %s: %s" % (src, errstr) + + try: + fdst = open(dst, 'wb') + except os.error, (errno, errstr): + raise DistutilsFileError, "could not create %s: %s" % (dst, errstr) + + while 1: + try: + buf = fsrc.read (buffer_size) + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "could not read from %s: %s" % (src, errstr) + + if not buf: + break + + try: + fdst.write(buf) + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "could not write to %s: %s" % (dst, errstr) + + finally: + if fdst: + fdst.close() + if fsrc: + fsrc.close() + +# _copy_file_contents() + + +def copy_file (src, dst, + preserve_mode=1, + preserve_times=1, + update=0, + verbose=0): + + """Copy a file 'src' to 'dst'. If 'dst' is a directory, then 'src' + is copied there with the same name; otherwise, it must be a + filename. (If the file exists, it will be ruthlessly clobbered.) + If 'preserve_mode' is true (the default), the file's mode (type + and permission bits, or whatever is analogous on the current + platform) is copied. If 'preserve_times' is true (the default), + the last-modified and last-access times are copied as well. If + 'update' is true, 'src' will only be copied if 'dst' does not + exist, or if 'dst' does exist but is older than 'src'. If + 'verbose' is true, then a one-line summary of the copy will be + printed to stdout.""" + + # XXX doesn't copy Mac-specific metadata + + from shutil import copyfile + from stat import * + + if not os.path.isfile (src): + raise DistutilsFileError, \ + "can't copy %s:not a regular file" % src + + if os.path.isdir (dst): + dir = dst + dst = os.path.join (dst, os.path.basename (src)) + else: + dir = os.path.dirname (dst) + + if update and not newer (src, dst): + return + + if verbose: + print "copying %s -> %s" % (src, dir) + + copyfile (src, dst) + if preserve_mode or preserve_times: + st = os.stat (src) + if preserve_mode: + os.chmod (dst, S_IMODE (st[ST_MODE])) + if preserve_times: + os.utime (dst, (st[ST_ATIME], st[ST_MTIME])) + +# copy_file () + + +def copy_tree (src, dst, + preserve_mode=1, + preserve_times=1, + preserve_symlinks=0, + update=0, + verbose=0): + + """Copy an entire directory tree 'src' to a new location 'dst'. Both + 'src' and 'dst' must be directory names. If 'src' is not a + directory, raise DistutilsFileError. If 'dst' does not exist, it + is created with 'mkpath'. The endresult of the copy is that + every file in 'src' is copied to 'dst', and directories under + 'src' are recursively copied to 'dst'. + + 'preserve_mode' and 'preserve_times' are the same as for + 'copy_file'; note that they only apply to regular files, not to + directories. If 'preserve_symlinks' is true, symlinks will be + copied as symlinks (on platforms that support them!); otherwise + (the default), the destination of the symlink will be copied. + 'update' and 'verbose' are the same as for 'copy_file'.""" + + if not os.path.isdir (src): + raise DistutilsFileError, \ + "cannot copy tree %s: not a directory" % src + try: + names = os.listdir (src) + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "error listing files in %s: %s" % (src, errstr) + + + mkpath (dst, verbose=verbose) + + for n in names: + src_name = os.path.join (src, n) + dst_name = os.path.join (dst, n) + + if preserve_symlinks and os.path.islink (src_name): + link_dest = os.readlink (src_name) + os.symlink (link_dest, dst_name) + elif os.path.isdir (src_name): + copy_tree (src_name, dst_name, + preserve_mode, preserve_times, preserve_symlinks, + update, verbose) + else: + copy_file (src_name, dst_name, + preserve_mode, preserve_times, + update, verbose) + +# copy_tree ()