From 42926ddc7e413f7f14ad68b75f7a6fffe9d96733 Mon Sep 17 00:00:00 2001 From: Greg Ward Date: Wed, 8 Sep 1999 02:41:09 +0000 Subject: [PATCH] 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()'. --- Lib/distutils/core.py | 134 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/Lib/distutils/core.py b/Lib/distutils/core.py index 2d5d0661458..8d2572afca3 100644 --- a/Lib/distutils/core.py +++ b/Lib/distutils/core.py @@ -72,9 +72,12 @@ def setup (**attrs): # (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 @@ class Distribution: ('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 @@ class Distribution: 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 @@ class Distribution: # 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 @@ class Distribution: # '.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 @@ class Distribution: 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 @@ class Distribution: "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 @@ class Distribution: 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: # -- 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