Careful rethink of command options, distribution options, distribution
  attributes, etc.  Biggest change was to the Distribution constructor
  -- it now looks for an 'options' attribute, which contains values
  (options) that are explicitly farmed out to the commands.  Also,
  certain options supplied to Distribution (ie. in the 'setup()' call in
  setup.py) are now "command option aliases", meaning they are dropped
  right into a certain command rather than being distribution options.
  This is handled by a new Distribution class attribute,
  'alias_options'.
Various comment changes to reflect the new way-of-thinking.
Added 'get_command_name()' method to Command -- was assuming its
  existence all along as 'command_name()', so changed the code that
  needs it to call 'get_command_name()'.
diff --git a/Lib/distutils/core.py b/Lib/distutils/core.py
index 2d5d066..8d2572a 100644
--- a/Lib/distutils/core.py
+++ b/Lib/distutils/core.py
@@ -72,9 +72,12 @@
     # (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.
+    # If we had a config file, this is where we would parse it: override
+    # the client-supplied command options, but be overridden by the
+    # command line.
+
+    # 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:
@@ -111,6 +114,18 @@
                       ('dry-run', 'n', "don't actually do anything"),
                      ]
 
+    # 'alias_options' map distribution options to command options -- the
+    # idea is that the most common, essential options can be directly
+    # specified as Distribution attributes, and the rest can go in the
+    # 'options' dictionary.  These aliases are for those common, essential
+    # options.
+    alias_options = { 'py_modules': ('build_py', 'modules'),
+                      'ext_modules': ('build_ext', 'modules'),
+                      'package': [('build_py', 'package',),
+                                  ('build_ext', 'package')],
+
+                    }
+
 
     # -- Creation/initialization methods -------------------------------
     
@@ -129,11 +144,13 @@
         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)
+        # 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.url = None
         self.licence = None
         self.description = None
 
@@ -143,18 +160,14 @@
         # for the client to override command classes
         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])
+        # 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.        
+        # XXX not needed anymore! (I think...)
+        #self.py_modules = None
+        #self.ext_modules = None
+        #self.package = None
 
         # And now initialize bookkeeping stuff that can't be supplied by
         # the caller at all.  'command_obj' maps command names to
@@ -174,6 +187,49 @@
         # '.get()' rather than a straight lookup.
         self.have_run = {}
 
+        # Now we'll use the attrs dictionary (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.  Note that some of
+            # these may be aliases for command options, so we might go
+            # through some of the above again.
+            for (key,val) in attrs.items():
+                alias = self.alias_options.get (key)
+                if alias:
+                    if type (alias) is ListType:
+                        for (command, cmd_option) in alias:
+                            cmd_obj = self.find_command_obj (command)
+                            cmd_obj.set_option (cmd_option, val)
+                    elif type (alias) is TupleType:
+                        (command, cmd_option) = alias
+                        cmd_obj = self.find_command_obj (command)
+                        cmd_obj.set_option (cmd_option, val)
+                    else:
+                        raise RuntimeError, \
+                              ("oops! bad alias option for '%s': " +
+                               "must be tuple or list of tuples") % key
+                    
+                elif hasattr (self, key):
+                    setattr (self, key, val)
+                else:
+                    raise DistutilsOptionError, \
+                          "invalid distribution option '%s'" % key
+
     # __init__ ()
 
 
@@ -213,10 +269,10 @@
                 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)
+            # 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).
+            cmd_obj = self.find_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'
@@ -226,8 +282,15 @@
                       "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!
+            # Also make sure that the command object provides a list of its
+            # known options
+            if not (hasattr (cmd_obj, 'options') and
+                    type (cmd_obj.options) is ListType):
+                raise DistutilsClasserror, \
+                      ("command class %s must provide an 'options' attribute "+
+                       "(a list of tuples)") % \
+                      cmd_obj.__class__
+
             args = fancy_getopt (cmd_obj.options, cmd_obj, args[1:])
             self.command_obj[command] = cmd_obj
             self.have_run[command] = 0
@@ -376,6 +439,11 @@
            Then invoke 'run()' on that command object (or an existing
            one)."""
 
+        # XXX currently, this is the only place where we invoke a
+        # command object's 'run()' method -- so it might make sense to
+        # put the 'set_final_options()' call here, too, instead of
+        # requiring every command's 'run()' to call it first.        
+
         # Already been here, done that? then return silently.
         if self.have_run.get (command):
             return
@@ -530,7 +598,7 @@
         except AttributeError:
             raise DistutilsOptionError, \
                   "command %s: no such option %s" % \
-                  (self.command_name(), option)
+                  (self.get_command_name(), option)
 
 
     def get_options (self, *options):
@@ -545,7 +613,7 @@
         except AttributeError, name:
             raise DistutilsOptionError, \
                   "command %s: no such option %s" % \
-                  (self.command_name(), name)
+                  (self.get_command_name(), name)
             
         return tuple (values)
     
@@ -557,7 +625,7 @@
         if not hasattr (self, option):
             raise DistutilsOptionError, \
                   "command %s: no such option %s" % \
-                  (self.command_name(), option)
+                  (self.get_command_name(), option)
         if value is not None:
             setattr (self, option, value)
 
@@ -573,6 +641,20 @@
 
     # -- Convenience methods for commands ------------------------------
 
+    def get_command_name (self):
+        if hasattr (self, 'command_name'):
+            return self.command_name
+        else:
+            class_name = self.__class__.__name__
+
+            # The re.split here returs empty strings delimited by the
+            # words we're actually interested in -- e.g.  "FooBarBaz"
+            # splits to ['', 'Foo', '', 'Bar', '', 'Baz', ''].  Hence
+            # the 'filter' to strip out the empties.            
+            words = filter (None, re.split (r'([A-Z][a-z]+)', class_name))
+            return string.join (map (string.lower, words), "_")
+
+
     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