1
0
mirror of https://github.com/openSUSE/osc.git synced 2025-01-13 17:16:23 +01:00

- update cmdln.py to planned 1.0 version. The main changes are

related to points that were raised during employment in osc:

  # v0.8.3
  - Fix a bug where errors with passing an incorrect number of args to
    functions in do_foo() implementations would be masked.
  
  # v1.0.0
  - [backward incompat] `Cmdln.main()` no longer takes an `optparser`
    argument. Top-level option parsing has been changed so that top-level
    options for a `Cmdln` subclass can more naturally be defined and
    handled on the class definition. Changes:
    - `Cmdln.main()` calls `self.get_optparser` to get an option handler.
      Subclasses should overload this method for custom top-level options.
    - After option parsing, but before sub-command handling, the
      `self.postoptparse()` hook is called.
  - Add a `version` attribute on `Cmdln` subclasses. If set, the default
    top-level option parser will have a `--version` attribute.
  - [backward incompat] Simplify the StopProcessing/opts.stop handling for
    option handling in subcommands. The "opts" argument to "do_*"
    sub-command functions will no longer have a "stop" value.
    StopProcessing is now called StopOptionProcessing. This shouldn't
    affect simple usage of cmdln.py.
  
- adjust osc.commandline for these changes.
This commit is contained in:
Dr. Peter Poeml 2007-05-16 10:55:05 +00:00
parent 34a2310db3
commit fc6eaf68da
2 changed files with 81 additions and 118 deletions

View File

@ -35,8 +35,8 @@ See the README.txt or <http://trentm.com/projects/cmdln/> for more
details.
"""
__revision__ = "$Id: cmdln.py 1087 2006-05-11 00:04:28Z trentm $"
__version_info__ = (0, 8, 2)
__revision__ = "$Id: cmdln.py 1666 2007-05-09 03:13:03Z trentm $"
__version_info__ = (1, 0, 0)
__version__ = '.'.join(map(str, __version_info__))
import os
@ -122,6 +122,7 @@ class RawCmdln(cmd.Cmd):
"""
name = None # if unset, defaults basename(sys.argv[0])
prompt = None # if unset, defaults to self.name+"> "
version = None # if set, default top-level options include --version
# Default messages for some 'help' command error cases.
# They are interpolated with one arg: the command.
@ -172,7 +173,31 @@ class RawCmdln(cmd.Cmd):
self.completekey = completekey
self.cmdlooping = False
def main(self, argv=None, optparser=_NOT_SPECIFIED, loop=LOOP_NEVER):
def get_optparser(self):
"""Hook for subclasses to set the option parser for the
top-level command/shell.
This option parser is used retrieved and used by `.main()' to
handle top-level options.
The default implements a single '-h|--help' option. Sub-classes
can return None to have no options at the top-level. Typically
an instance of CmdlnOptionParser should be returned.
"""
version = (self.version is not None
and "%s %s" % (self._name_str, self.version)
or None)
return CmdlnOptionParser(self, version=version)
def postoptparse(self):
"""Hook method executed just after `.main()' parses top-level
options.
When called `self.values' holds the results of the option parse.
"""
pass
def main(self, argv=None, loop=LOOP_NEVER):
"""A possible mainline handler for a script, like so:
import cmdln
@ -192,13 +217,6 @@ class RawCmdln(cmd.Cmd):
It must be a sequence, where the first element is the
command name and subsequent elements the args for that
command.
"optparser" (optional) is a cmdln.CmdlnOptionParser instance
to process top-level options. If left unspecified,
option parsing is done with a default option list: -h,
--help. None can be given to disable top-level option
processing. If option processing is done, the 'options'
attribute is set to the resulting `optparse.Values`
instance.
"loop" (optional, default LOOP_NEVER) is a constant
indicating if a command loop should be started (i.e. an
interactive shell). Valid values (constants on this module):
@ -212,11 +230,8 @@ class RawCmdln(cmd.Cmd):
argv = sys.argv
else:
argv = argv[:] # don't modify caller's list
optparser = self.optparser
if optparser is _NOT_SPECIFIED:
optparser = CmdlnOptionParser(self)
self.optparser = optparser
self.optparser = self.get_optparser()
if self.optparser: # i.e. optparser=None means don't process for opts
try:
self.options, args = self.optparser.parse_args(argv[1:])
@ -226,10 +241,11 @@ class RawCmdln(cmd.Cmd):
self.stderr.write(self._str(msg))
self.stderr.flush()
return 1
if self.options.stop:
except StopOptionProcessing, ex:
return 0
else:
self.options, args = None, argv[1:]
self.postoptparse()
if loop == LOOP_ALWAYS:
if args:
@ -828,6 +844,8 @@ class RawCmdln(cmd.Cmd):
def _do_EOF(self, argv):
# Default EOF handler
# Note: an actual EOF is redirected to this command.
#TODO: separate name for this. Currently it is available from
# command-line. Is that okay?
self.stdout.write('\n')
self.stdout.flush()
self.stop = True
@ -846,37 +864,26 @@ class RawCmdln(cmd.Cmd):
# See the class _OptionParserEx docstring for details.
#
class StopProcessing(Exception):
class StopOptionProcessing(Exception):
"""Indicate that option *and argument* processing should stop
cleanly. This is not an error condition. It is similar in spirit to
StopIteration. This is raised by the default "help" and "version"
option actions and can be raised by custom option callbacks too.
cleanly. This is not an error condition. It is similar in spirit to
StopIteration. This is raised by _OptionParserEx's default "help"
and "version" option actions and can be raised by custom option
callbacks too.
A new boolean "stop" attribute on the optparse.Values instance
returned by parser.parse_args() indicates if StopProcessing was
encountered. Hence the typical CmdlnOptionParser usage is:
Hence the typical CmdlnOptionParser (a subclass of _OptionParserEx)
usage is:
parser = CmdlnOptionParser(mycmd)
parser.add_option("-f", "--force", dest="force")
...
parser.parse_args()
#XXX This is wrong: doesn't match current impl.
if parser.stop: # normal termination, "--help" was probably given
try:
opts, args = parser.parse_args()
except StopOptionProcessing:
# normal termination, "--help" was probably given
sys.exit(0)
"""
class _OptionEx(optparse.Option):
def take_action(self, action, dest, opt, value, values, parser):
if action == "help":
parser.print_help()
raise StopProcessing
elif action == "version":
parser.print_version()
raise StopProcessing
else:
return optparse.Option.take_action(self, action, dest, opt,
value, values, parser)
class _OptionParserEx(optparse.OptionParser):
"""An optparse.OptionParser that uses exceptions instead of sys.exit.
@ -884,10 +891,9 @@ class _OptionParserEx(optparse.OptionParser):
as follows:
- Correct (IMO) the default OptionParser error handling to never
sys.exit(). Instead OptParseError exceptions are passed through.
- Add the StopProcessing exception (a la StopIteration) to
indicate normal termination of option processing. Add the "stop"
boolean attribute to Values returned by .parse_args(). See
StopProcessing's docstring for details.
- Add the StopOptionProcessing exception (a la StopIteration) to
indicate normal termination of option processing.
See StopOptionProcessing's docstring for details.
I'd also like to see the following in the core optparse.py, perhaps
as a RawOptionParser which would serve as a base class for the more
@ -899,62 +905,17 @@ class _OptionParserEx(optparse.OptionParser):
optparser.add_option("--version", action="version")
These are good practices, just not valid defaults if they can
get in the way.
- Something like the clear separation of user-error vs.
programmer-error in CmdlnOptionParser would be good: a new
OptParseUserError or something.
"""
#TODO: deal with older Python 2.3 optparse differences
def __init__(self, **kwargs):
if "option_class" in kwargs:
raise optparse.OptParseError(
"cannot specify 'option_class' on _OptionParserEx")
kwargs["option_class"] = _OptionEx
optparse.OptionParser.__init__(self, **kwargs)
def error(self, msg):
raise optparse.OptParseError(msg)
def exit(self, status=0, msg=None):
# Proposed transition code would use deprecation warning.
#import warnings
#warnings.warn("OptionParser.exit() is deprecated")
raise optparse.OptParseError("_OptionParserEx.exit() is obsolete")
def parse_args(self, args=None, values=None):
"""Override parse_args to properly handle StopProcessing.
Also changed: don't catch OptParseError's raised by
._process_args() because we would just change them back to
exceptions in .error() anyway.
"""
rargs = self._get_args(args)
if values is None:
values = self.get_default_values()
# for convenience of callbacks:
self.rargs = rargs
self.largs = largs = []
self.values = values
#TODO:
# - Should 'stop' be changed to a third return arg instead of
# attr on Values? I think so, yes.
try:
self._process_args(largs, rargs, values)
except (optparse.BadOptionError, optparse.OptionValueError), ex:
self.error(ex.msg)
except StopProcessing:
values.stop = True
if status == 0:
raise StopOptionProcessing(msg)
else:
values.stop = False
args = largs + rargs
return self.check_values(values, args)
#TODO: don't lose status info here
raise optparse.OptParseError(msg)
def add_option(self, *args, **kwargs):
opt = optparse.OptionParser.add_option(self, *args, **kwargs)
if opt.dest == "stop":
raise optparse.OptionConflictError(
"option dest value of 'stop' is reserved")
#---- optparse.py-based option processing support
@ -989,7 +950,7 @@ class CmdlnOptionParser(_OptionParserEx):
class SubCmdOptionParser(_OptionParserEx):
def set_cmdln_info(self, cmdln, subcmd):
"""Called by CmdlnOpt to pass relevant info about itself needed
"""Called by Cmdln to pass relevant info about itself needed
for print_help().
"""
self.cmdln = cmdln
@ -1022,11 +983,6 @@ def option(*args, **kwargs):
return decorate
#TODO: Could name these RawCmdln and Cmdln, the latter with optparse
# integration.
# Then *always* have an optparser (even empty)?
# TODO: check/test this *always* have an optparser case, even if
# no decorator is provided.
class Cmdln(RawCmdln):
"""An improved (on cmd.Cmd) framework for building multi-subcommand
scripts (think "svn" & "cvs") and simple shells (think "pdb" and
@ -1079,6 +1035,8 @@ class Cmdln(RawCmdln):
for arg in args:
bar(arg)
TODO: explain that "*args" can be other signatures as well.
The `cmdln.option` decorator corresponds to an `add_option()`
method call on an `optparse.OptionParser` instance.
@ -1101,8 +1059,11 @@ class Cmdln(RawCmdln):
optparser = handler.im_func.optparser = SubCmdOptionParser()
assert isinstance(optparser, SubCmdOptionParser)
optparser.set_cmdln_info(self, argv[0])
opts, args = optparser.parse_args(argv[1:])
if opts.stop:
try:
opts, args = optparser.parse_args(argv[1:])
except StopOptionProcessing:
#TODO: this doesn't really fly for a replacement of
# optparse.py behaviour, does it?
return 0 # Normal command termination
try:
@ -1114,6 +1075,14 @@ class Cmdln(RawCmdln):
# do_foo() takes exactly 5 arguments (6 given)
# Raise CmdlnUserError for these with a suitably
# massaged error message.
import sys
tb = sys.exc_info()[2] # the traceback object
if tb.tb_next is not None:
# If the traceback is more than one level deep, then the
# TypeError do *not* happen on the "handler(...)" call
# above. In that we don't want to handle it specially
# here: it would falsely mask deeper code errors.
raise
msg = ex.args[0]
match = _INCORRECT_NUM_ARGS_RE.search(msg)
if match:
@ -1123,13 +1092,6 @@ class Cmdln(RawCmdln):
msg[2] = msg[2].replace("arguments", "argument")
msg[3] = int(msg[3]) - 3
msg = ''.join(map(str, msg))
# To debug errors which involve calling functions with
# wrong number of arguments, uncomment the following line.
# Otherwise, all errors of this kind are presented as
# "incorrect usage" to the user:
#raise
raise CmdlnUserError(msg)
else:
raise

View File

@ -12,7 +12,6 @@ import conf
class Osc(cmdln.Cmdln):
"""usage:
osc [GLOBALOPTS] SUBCOMMAND [OPTS] [ARGS...]
osc help SUBCOMMAND
@ -28,32 +27,34 @@ class Osc(cmdln.Cmdln):
"""
name = 'osc'
def __init__(self, *args, **kwargs):
cmdln.Cmdln.__init__(self, *args, **kwargs)
cmdln.Cmdln.do_help.aliases.append('h')
conf.get_config()
# set up and parse options before subcommand
self.optparser = cmdln.CmdlnOptionParser(self,
version=get_osc_version())
self.optparser.add_option('-H', '--http-debug', action='store_true',
def get_optparser(self):
"""this is the parser for "global" options (not specific to subcommand)"""
optparser = cmdln.CmdlnOptionParser(self, version=get_osc_version())
optparser.add_option('-H', '--http-debug', action='store_true',
default=conf.config['http_debug'],
help='debug HTTP traffic')
self.optparser.add_option('-A', '--apisrv', dest='apisrv',
optparser.add_option('-A', '--apisrv', dest='apisrv',
metavar='URL',
help='specify URL to access API server at')
return optparser
(self.global_opts, self.myargs) = self.optparser.parse_args()
# XXX version is printed twice otherwise...
self.optparser.version = ''
def postoptparse(self):
"""merge commandline options into the config"""
# merge commandline options into the config
conf.config['http_debug'] = self.global_opts.http_debug
if self.global_opts.apisrv:
conf.config['http_debug'] = self.options.http_debug
if self.options.apisrv:
conf.config['scheme'], conf.config['apisrv'] = \
conf.parse_apisrv_url(conf.config['scheme'], self.global_opts.apisrv)
conf.parse_apisrv_url(conf.config['scheme'], self.options.apisrv)
conf.config['apiurl'] = conf.config['scheme'] + '://' + conf.config['apisrv']
# XXX unless config['user'] goes away (and is replaced with a handy function, or