]> granicus.if.org Git - python/commitdiff
First checkin of real Distutils code.
authorGreg Ward <gward@python.net>
Mon, 22 Mar 1999 14:52:19 +0000 (14:52 +0000)
committerGreg Ward <gward@python.net>
Mon, 22 Mar 1999 14:52:19 +0000 (14:52 +0000)
Lib/distutils/core.py [new file with mode: 0644]
Lib/distutils/errors.py [new file with mode: 0644]
Lib/distutils/fancy_getopt.py [new file with mode: 0644]
Lib/distutils/options.py [new file with mode: 0644]
Lib/distutils/util.py [new file with mode: 0644]

diff --git a/Lib/distutils/core.py b/Lib/distutils/core.py
new file mode 100644 (file)
index 0000000..3a7443c
--- /dev/null
@@ -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 (file)
index 0000000..6605ad2
--- /dev/null
@@ -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 (file)
index 0000000..c63ce61
--- /dev/null
@@ -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 (file)
index 0000000..f6cae82
--- /dev/null
@@ -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 (file)
index 0000000..7c13abe
--- /dev/null
@@ -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 ()