pythonmercurialmercurial-extension

How to handle errors in Mercurial extension


What is the recommended approach to handling errors in a Mercurial extension? We could not find any docs for this aspect online.

The following example will print Hello world and set errorlevel to 1 in a Windows shell.

@command('print-parents', [('','', None, '')], '')
def printparents(ui, repo, node, **opts):
  print "Hello world"
  return True 

Why is errorlevel set to 1? We expected it to be 0. How does Python or Mercurial handle True and False in this context? Changing to return False produces errorlevel 0.

We have seen some examples of raise error.Abort(..) but this output a very verbose callstack which is not neccessary. A simple text message and correct errorlevel is what is needed.

Using Windows 7 and Mercurial 3.4.1


Solution

  • In a @command function, use ui.warn() to display an error message, then return an integer value to set the exit code:

    if something_is_wrong:
        ui.warn(_('Something is wrong, please correct this'))
        return 1
    

    However, using raise mercurial.error.Abort() is fine too and doesn't result in a traceback unless you have ui.traceback set to True (it defaults to False). Raising Abort() normally results in a ui.warn('abort: ' + msg_from_exception) and return -1 combo (which results in a 255 exit code):

    $ cat demo.py
    from mercurial import cmdutil, error
    
    cmdtable = {}
    command = cmdutil.command(cmdtable)
    
    @command('demo1', [], '')
    def demo1(ui, repo, *args, **opts):
        ui.warn('Error message written directly to ui.warn')
        return 1
    
    @command('demo2', [], '')
    def demo2(ui, repo, *args, **opts):
        raise error.Abort('Error message raised with Abort')
    
    $ hg --config extensions.demo=`pwd`/demo.py demo1
    Error message written directly to ui.warn
    $ echo $?
    1
    $ hg --config extensions.demo=`pwd`/demo.py demo2
    abort: Error message raised with Abort
    $?
    255
    $ hg --config extensions.demo=`pwd`/demo.py --config ui.traceback=true demo2
    Traceback (most recent call last):
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/dispatch.py", line 204, in _runcatch
        return _dispatch(req)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/dispatch.py", line 880, in _dispatch
        cmdpats, cmdoptions)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/extensions.py", line 210, in closure
        return func(*(args + a), **kw)
      File "/opt/facebook/hg/lib/python2.7/site-packages/fastmanifest/cachemanager.py", line 318, in runcommandtrigger
        result = orig(*args, **kwargs)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/extensions.py", line 210, in closure
        return func(*(args + a), **kw)
      File "/opt/facebook/hg/lib/python2.7/site-packages/fastmanifest/__init__.py", line 174, in _logonexit
        r = orig(ui, repo, cmd, fullargs, *args)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/extensions.py", line 210, in closure
        return func(*(args + a), **kw)
      File "/opt/facebook/hg/lib/python2.7/site-packages/hgext/journal.py", line 79, in runcommand
        return orig(lui, repo, cmd, fullargs, *args)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/dispatch.py", line 637, in runcommand
        ret = _runcommand(ui, options, cmd, d)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/extensions.py", line 210, in closure
        return func(*(args + a), **kw)
      File "/opt/facebook/hg/lib/python2.7/site-packages/hgext/pager.py", line 160, in pagecmd
        return orig(ui, options, cmd, cmdfunc)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/extensions.py", line 210, in closure
        return func(*(args + a), **kw)
      File "/opt/facebook/hg/lib/python2.7/site-packages/hgext/color.py", line 503, in colorcmd
        return orig(ui_, opts, cmd, cmdfunc)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/dispatch.py", line 1010, in _runcommand
        return checkargs()
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/dispatch.py", line 971, in checkargs
        return cmdfunc()
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/dispatch.py", line 877, in <lambda>
        d = lambda: util.checksignature(func)(ui, *args, **cmdoptions)
      File "/opt/facebook/hg/lib/python2.7/site-packages/mercurial/util.py", line 1036, in check
        return func(*args, **kwargs)
      File "/tmp/demo/demo.py", line 13, in demo2
        raise error.Abort('Error message raised with Abort')
    Abort: Error message raised with Abort
    abort: Error message raised with Abort
    

    Note that I had to explicitly enable the traceback!

    In Python, the bool type is a subclass of int, and True has a value of 1 and False has an integer value of 0. Because Mercurial expects @command functions to return an integer to set an exit code (or None for exit code 0), you set the exit code to 1 by returning True.

    I generally look over the mercurial.commands module and the various hgext extensions to learn how others have solved specific problems. The hg grep command, for example, contains this example of handling an error:

    try:
        regexp = util.re.compile(pattern, reflags)
    except re.error as inst:
        ui.warn(_("grep: invalid match pattern: %s\n") % inst)
        return 1
    

    while the hg addremove code raises error.Abort():

    try:
        sim = float(opts.get('similarity') or 100)
    except ValueError:
        raise error.Abort(_('similarity must be a number'))
    

    I'd use error.Abort() for command errors, and ui.warn() plus returning an integer only if you specifically need an exit code other than 255.