2

In an interactively running python session I kick off modules/programs I wrote using argparse (because these modules are meant to be used as commands from the shell prompt as well).

If I call the module with the wrong args parameters, argparse correctly spits out an error as expected, but unfortunately arparse then calls sys.exit() which in turn terminates the outer python session.

That's not what I want. How can I protect the outer interactive python session from being terminated without changing the sys.exit() code in the inner module and without wrapping my module in code. - I'm looking for something like a switch I can set or something I can do to the interactive python session before I start it, so that sys.exit() does not terminate it.

Update from the comments:

The resaon why I ask this question is emacs's python-mode (python.el): It effectively "pastes" the complete code of the module file in the python session. If I want to wrap this in a try..except I need to indent the whole module before I wrap it and hand over to the python-session buffer in emacs.

Here a sample module I use (most of my command line utilities use a similar template):

#!/usr/bin/env python
"""\
tool to do stuff
"""
__author__ = """halloleo"""
__version__ = """0.1"""

import logging
import sys
import os

import clitools # my own helpers

#
# Options
#    
def argParser(locArgs = None):
    parser = clitools.HelpLogArgParser(description=__doc__)
    parser.add_argument('files', metavar='FILE', nargs='+',
                        help='file to work on')
    parser.add_loglevel_group()
    return parser.parse_args(locArgs)

def doStuff(file)
    # do stuff
    print file

#
# main
#
if __name__ == '__main__':

    args = argParser()     
    clitools.infoLoggerConfig(args.loglevel)
    logging.debug("args = %s" % args)          

    for f in args.files
        dostuff(f)

# Local Variables:
# leo-python-args-to-send: "--debug c:/tmp/testfile"
# End:

BTW, I know now, this approach works. I have implemented it, but it is kind of scary to indent the whole module code... Here is what at the end I handover to the python session:

import sys; sys.argv = '''scraper.py --debug c:/tmp/testfile'''.split()
try:
    #!/usr/bin/env python
    """\
    tool to do stuff
    """

    .
    .
    .

    # Local Variables:
    # leo-python-args-to-send: "--debug c:/tmp/testfile"
    # End:
except SystemExit, e:
    print "Terminated  with exit code", e
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
halloleo
  • 9,216
  • 13
  • 64
  • 122
  • 2
    Catch the `SystemExit` exception. – Tim Peters Nov 06 '13 at 04:43
  • Basically all python-modes offer executes through a dedicated process, which seems the way to go. Need a clean example relying on open code to tell more. – Andreas Röhler Nov 07 '13 at 10:35
  • Dedicated process is an ide, indeed, but then I have to make sure in the case the modyle does *not* exit via ys.exit() that the deciacted buffer gets cleaned up. - BTW, Example module I want to exexcute (without exiting) is provided above. Not "clean" enough? That's how the modules I have all look like. – halloleo Nov 07 '13 at 14:36
  • @halloleo Meant an example which I may run and check. "Clitools" don't exist here. – Andreas Röhler Nov 07 '13 at 16:32

3 Answers3

4

Use a try/except block catching SystemExit. For example:

import sys

try:
    sys.exit()
except SystemExit:
    print "Tried to sys.exit()"
David Robinson
  • 77,383
  • 16
  • 167
  • 187
  • Mmmmh, but the outer python session is already running (*interactively*) - and I don't want run the module always with typing all the `try... except` stuff around it! Might edit the question to make this clearer. – halloleo Nov 06 '13 at 06:52
  • @halloleo: Could you show an example of the way you're calling argparse interactively that is leading to this error? That would help make it clear how to prevent it from throwing the exception. – David Robinson Nov 06 '13 at 07:48
  • Effectively I paste the complete code of the module file in the python session. Well that is, not *I* do this, but emacs' *python mode* ([python.el](https://github.com/fgallina/python.el/blob/master/python.el) does it for me. - So what I'm trying now, is to enhance the `python.el`'s elisp function to wrap the python code with the `try except`; - kind of scary to indent *the whole python code* of file for execution in the python mode... – halloleo Nov 07 '13 at 01:10
  • Update: the indenting amazingly works, but `"""` strings spanning multiple lines are obviously altered... :-( So it would be better if I could somehow leave the whole code unindented and than just *call* the main stuff of the module in a `try except`... – halloleo Nov 07 '13 at 01:31
  • 1
    @halloleo: You can wrap the function you're calling in a decorator that takes care of the try/except and would make it easy. But if I saw an example of an actual line of code that broke it would make it easier – David Robinson Nov 07 '13 at 01:41
  • I have added a sample module. Curious about the decorator approach! – halloleo Nov 07 '13 at 05:20
2

As documented in python's sys module, sys.exit does the following:

Exit from Python. This is implemented by raising the SystemExit exception, so cleanup actions specified by finally clauses of try statements are honored, and it is possible to intercept the exit attempt at an outer level.

So you can handle the exception in a try except block as any other exception.


argparse actually calls sys.exit from the exit method of argparse.ArgumentParser.

In some circumstances, it might be more worthwhile to subclass argparse.ArgumentParser to raise a more meaningful error:

import argparse
class BadCLIArgs(Exception):
    pass

class NonExitingArgumentParser(argparse.ArgumentParser):
    def exit(self, status=1, message=None):
        raise BadCLIArgs((status, message))

Or alternatively override error (as exit is also likely to be called for the --help flag.)

I've never really liked the approach of argparse to exit rather than raising a more meaningful exception.

mgilson
  • 300,191
  • 65
  • 633
  • 696
  • The Exception approach doesn't give nice error messages when I use the module from a shell, but a stack trace. So for the shell case sys.exit *is* appropriate I think. - I just want to catch this in the interactive python session - with a general switch or so (see reworded question). – halloleo Nov 06 '13 at 07:00
  • There is no general switch. You could add a switch to `NonExitingArgumentParser` so that `exit` would delegate to a `argparse.ArgumentParser.exit` if turned on (off). You could even have that switch toggle depending on whether you're [in the interactive prompt or not.](http://stackoverflow.com/a/2356420/748858) – mgilson Nov 06 '13 at 07:06
0

I just came across this problem. It seems that the SystemExit error is raised when I was using

args = parser.parse_args(argv)

so my solution was simply to use a try-except with only this line:

def main(argv=None):
    ...
    try:
        args = parser.parse_args(argv)
    except SystemExit:
        return 0
    ...
    return 0

In my script code I use:

if __name__ == '__main__':
    sys.exit(main())

This still places a 0 in the interactive shell after displaying the error message, but it does successfully print the error message and stop the interactive shell from terminating.