Re: [Python-Dev] anyone tried to use optparse with something else than sys.argv?

Erik Heneryd <[email protected]> Tue, 10 Dec 2002 21:52:36 +0100 (CET)
Newsgroups gmane.comp.python.optik.user
Message-ID <Pine.LNX.4.44.0212102026300.4771-200000@brain.secret.pythonware.com>
On Tue, 10 Dec 2002, Greg Ward wrote:

> [cc'ing [email protected], since this concerns
>  Optik's reincarnation as optparse in the Python standard library]
> 
> > well, to me, it's pretty obvious that the design effort on optparse has
> > pretty much been focused strictly on initial sys.argv parsing and while
> > it's extendable, other use cases (embedded consoles etc) seem to have been
> > given little or no real thought.
> 
> Guilty as charged.  My intention was always to support that kind of
> stuff, but I've never had occasion to test it myself.  Sounds like I
> carefully hid lots of little bugs in there -- congrats for finding them.
> ;-)

:)

well, i guess it's not so much about bugs, as it's about keeping it 
simple.  optparse seems quite complex in it's design and i don't want to 
subclass, override etc if i could have done the same thing just by 
catching an exception.  i don't know, maybe i'm just stuck thinking in 
getopt ways... :)

well, i've tried to apply all those things in a patch (attached):
can't say i fully understand optparse, it's quite complex, so i thought 
i'd post it here for comments before anything else...

the main thing i've done is that i've centralized the error handling.  if 
something goes wrong, error() is not called, but an exception raised.  all 
exceptions are then caught in parse_args().  there one can decide what to 
do with them - call error() or really throw an exception?

the help and version actions no longer call methods, they too raise 
exceptions, to be caught in parse_args().

the standard help and version Options are created on the fly and therefore 
of the right option_class type.

concrete api change:

* opobj.parse_args(list, values, throw=0)
- list is supposed to be the whole sys.argv (ie including the command).
- opobj.prog = list[0].
- if throw is true, BadOptionError and OptionValueError is raised instead 
  of calling error().
- if throw is true, OptionMessage is raised with the message as contents, 
  instead of printing and exiting.

i really don't know about all this, but is seemed like the right thing to 
do.  i think those, small, simple customizations will be much easier, 
while still able to do that heavy duty class-stuff...

what do you think?

        erik
optparse.diff (text/plain, 11.9 KB)
Index: optparse.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/optparse.py,v
retrieving revision 1.1
diff -c -r1.1 optparse.py
*** optparse.py	14 Nov 2002 22:00:19 -0000	1.1
--- optparse.py	10 Dec 2002 19:25:20 -0000
***************
*** 82,87 ****
--- 82,93 ----
      """
      Raised if an invalid or ambiguous option is seen on the command-line.
      """
+ 
+ class OptionMessage (Exception):
+     """
+     Used when a valid, message triggering, option is seen on the command
+     line. For example, show help or version."""
+ 
  class HelpFormatter:
  
      """
***************
*** 621,650 ****
              kwargs = self.callback_kwargs or {}
              self.callback(self, opt, value, parser, *args, **kwargs)
          elif action == "help":
!             parser.print_help()
!             sys.exit(0)
          elif action == "version":
!             parser.print_version()
!             sys.exit(0)
          else:
              raise RuntimeError, "unknown action %r" % self.action
  
          return 1
  
  # class Option
- def get_prog_name ():
-     return os.path.basename(sys.argv[0])
  
  
  SUPPRESS_HELP = "SUPPRESS"+"HELP"
  SUPPRESS_USAGE = "SUPPRESS"+"USAGE"
  
! STD_HELP_OPTION = Option("-h", "--help",
!                          action="help",
!                          help="show this help message and exit")
! STD_VERSION_OPTION = Option("--version",
!                             action="version",
!                             help="show program's version number and exit")
  
  
  class Values:
--- 627,653 ----
              kwargs = self.callback_kwargs or {}
              self.callback(self, opt, value, parser, *args, **kwargs)
          elif action == "help":
!             raise OptionMessage, parser.format_help()
          elif action == "version":
!             raise OptionMessage, parser.get_version()
          else:
              raise RuntimeError, "unknown action %r" % self.action
  
          return 1
  
  # class Option
  
  
  SUPPRESS_HELP = "SUPPRESS"+"HELP"
  SUPPRESS_USAGE = "SUPPRESS"+"USAGE"
  
! STD_HELP_OPTS = ("-h", "--help")
! STD_HELP_ATTRS = {"action": "help",
!                   "help": "show this help message and exit"}
! 
! STD_VERSION_OPTS = ("--version",)
! STD_VERSION_ATTRS = {"action": "version",
!                      "help": "show program's version number and exit"}
  
  
  class Values:
***************
*** 979,986 ****
  
          # Populate the option list; initial sources are the
          # standard_option_list class attribute, the 'option_list'
!         # argument, and the STD_VERSION_OPTION (if 'version' supplied)
!         # and STD_HELP_OPTION globals.
          self._populate_option_list(option_list,
                                     add_help=add_help_option)
  
--- 982,990 ----
  
          # Populate the option list; initial sources are the
          # standard_option_list class attribute, the 'option_list'
!         # argument.
!         # A standard help option (and if 'version' supplied, a
!         # standard version option) will also be created and added.
          self._populate_option_list(option_list,
                                     add_help=add_help_option)
  
***************
*** 1000,1008 ****
          if option_list:
              self.add_options(option_list)
          if self.version:
!             self.add_option(STD_VERSION_OPTION)
          if add_help:
!             self.add_option(STD_HELP_OPTION)
  
      def _init_parsing_state (self):
          # These are set in parse_args() for the convenience of callbacks.
--- 1004,1012 ----
          if option_list:
              self.add_options(option_list)
          if self.version:
!             self.add_option(*STD_VERSION_OPTS, **STD_VERSION_ATTRS)
          if add_help:
!             self.add_option(*STD_HELP_OPTS, **STD_HELP_ATTRS)
  
      def _init_parsing_state (self):
          # These are set in parse_args() for the convenience of callbacks.
***************
*** 1068,1082 ****
  
      # -- Option-parsing methods ----------------------------------------
  
!     def _get_args (self, args):
!         if args is None:
!             return sys.argv[1:]
          else:
!             return args[:]              # don't modify caller's list
  
!     def parse_args (self, args=None, values=None):
          """
!         parse_args(args : [string] = sys.argv[1:],
                     values : Values = None)
          -> (values : Values, args : [string])
  
--- 1072,1087 ----
  
      # -- Option-parsing methods ----------------------------------------
  
!     def _get_prog_name (self, args):
!         if args:
!             return os.path.basename(args[0])
          else:
!             # no args, fall back on the empty string
!             return ""
  
!     def parse_args (self, args=sys.argv, values=None, throw=0):
          """
!         parse_args(args : [string] = sys.argv,
                     values : Values = None)
          -> (values : Values, args : [string])
  
***************
*** 1088,1094 ****
          your option values) and 'args' is the list of arguments left
          over after parsing options.
          """
-         rargs = self._get_args(args)
          if values is None:
              values = self.get_default_values()
  
--- 1093,1098 ----
***************
*** 1101,1114 ****
          #     the leftover arguments -- ie. what's left after removing
          #     options and their arguments (the "l" stands for "leftover"
          #     or "left-hand")
!         self.rargs = rargs
          self.largs = largs = []
          self.values = values
  
          try:
              stop = self._process_args(largs, rargs, values)
          except (BadOptionError, OptionValueError), err:
!             self.error(err.msg)
  
          args = largs + rargs
          return self.check_values(values, args)
--- 1105,1128 ----
          #     the leftover arguments -- ie. what's left after removing
          #     options and their arguments (the "l" stands for "leftover"
          #     or "left-hand")
!         self.rargs = rargs = args[1:]
          self.largs = largs = []
          self.values = values
+         self.prog = self._get_prog_name(args)
  
          try:
              stop = self._process_args(largs, rargs, values)
          except (BadOptionError, OptionValueError), err:
!             if throw:
!                 raise
!             else:
!                 self.error(err.msg)
!         except OptionMessage, err:
!             if throw:
!                 raise
!             else:
!                 print >> sys.stdout, err
!                 sys.exit(0)
  
          args = largs + rargs
          return self.check_values(values, args)
***************
*** 1205,1214 ****
              nargs = option.nargs
              if len(rargs) < nargs:
                  if nargs == 1:
!                     self.error("%s option requires a value" % opt)
                  else:
!                     self.error("%s option requires %d values"
!                                % (opt, nargs))
              elif nargs == 1:
                  value = rargs.pop(0)
              else:
--- 1219,1227 ----
              nargs = option.nargs
              if len(rargs) < nargs:
                  if nargs == 1:
!                     raise OptionValueError, "%s option requires a value" % opt
                  else:
!                     raise OptionValueError, "%s option requires %d values" % (opt, nargs)
              elif nargs == 1:
                  value = rargs.pop(0)
              else:
***************
*** 1216,1222 ****
                  del rargs[0:nargs]
  
          elif had_explicit_value:
!             self.error("%s option does not take a value" % opt)
  
          else:
              value = None
--- 1229,1235 ----
                  del rargs[0:nargs]
  
          elif had_explicit_value:
!             raise OptionValueError, "%s option does not take a value" % opt
  
          else:
              value = None
***************
*** 1233,1239 ****
              i += 1                      # we have consumed a character
  
              if not option:
!                 self.error("no such option: %s" % opt)
              if option.takes_value():
                  # Any characters left in arg?  Pretend they're the
                  # next arg, and stop consuming characters of arg.
--- 1246,1252 ----
              i += 1                      # we have consumed a character
  
              if not option:
!                 raise BadOptionError, "no such option: %s" % opt
              if option.takes_value():
                  # Any characters left in arg?  Pretend they're the
                  # next arg, and stop consuming characters of arg.
***************
*** 1244,1253 ****
                  nargs = option.nargs
                  if len(rargs) < nargs:
                      if nargs == 1:
!                         self.error("%s option requires a value" % opt)
                      else:
!                         self.error("%s option requires %s values"
!                                    % (opt, nargs))
                  elif nargs == 1:
                      value = rargs.pop(0)
                  else:
--- 1257,1265 ----
                  nargs = option.nargs
                  if len(rargs) < nargs:
                      if nargs == 1:
!                         raise OptionValueError, "%s option requires a value" % opt
                      else:
!                         raise OptionValueError, "%s option requires %s values" % (opt, nargs)
                  elif nargs == 1:
                      value = rargs.pop(0)
                  else:
***************
*** 1273,1284 ****
          should either exit or raise an exception.
          """
          self.print_usage(sys.stderr)
!         sys.exit("%s: error: %s" % (get_prog_name(), msg))
  
      def get_usage (self):
          if self.usage:
              return self.formatter.format_usage(
!                 self.usage.replace("%prog", get_prog_name()))
          else:
              return ""
  
--- 1285,1296 ----
          should either exit or raise an exception.
          """
          self.print_usage(sys.stderr)
!         sys.exit("%s: error: %s" % (self.prog, msg))
  
      def get_usage (self):
          if self.usage:
              return self.formatter.format_usage(
!                 self.usage.replace("%prog", self.prog))
          else:
              return ""
  
***************
*** 1296,1302 ****
  
      def get_version (self):
          if self.version:
!             return self.version.replace("%prog", get_prog_name())
          else:
              return ""
  
--- 1308,1314 ----
  
      def get_version (self):
          if self.version:
!             return self.version.replace("%prog", self.prog)
          else:
              return ""
  
***************
*** 1339,1352 ****
          result.append(self.format_option_help(formatter))
          return "".join(result)
  
!     def print_help (self, file=None):
          """print_help(file : file = stdout)
  
          Print an extended help message, listing all options and any
          help text provided with them, to 'file' (default stdout).
          """
-         if file is None:
-             file = sys.stdout
          file.write(self.format_help())
  
  # class OptionParser
--- 1351,1362 ----
          result.append(self.format_option_help(formatter))
          return "".join(result)
  
!     def print_help (self, file=sys.stdout):
          """print_help(file : file = stdout)
  
          Print an extended help message, listing all options and any
          help text provided with them, to 'file' (default stdout).
          """
          file.write(self.format_help())
  
  # class OptionParser