diff --git a/IPython/core/iplib.py b/IPython/core/iplib.py index ceccebb..163a299 100644 --- a/IPython/core/iplib.py +++ b/IPython/core/iplib.py @@ -1243,7 +1243,8 @@ class InteractiveShell(Component, Magic): """ self.showtraceback((etype,value,tb),tb_offset=0) - def showtraceback(self,exc_tuple = None,filename=None,tb_offset=None): + def showtraceback(self,exc_tuple = None,filename=None,tb_offset=None, + exception_only=False): """Display the exception that just occurred. If nothing is known about the exception, this is the method which @@ -1254,18 +1255,24 @@ class InteractiveShell(Component, Magic): care of calling it if needed, so unless you are explicitly catching a SyntaxError exception, don't try to analyze the stack manually and simply call this method.""" - - - # Though this won't be called by syntax errors in the input line, - # there may be SyntaxError cases whith imported code. try: if exc_tuple is None: etype, value, tb = sys.exc_info() else: etype, value, tb = exc_tuple + + if etype is None: + if hasattr(sys, 'last_type'): + etype, value, tb = sys.last_type, sys.last_value, \ + sys.last_traceback + else: + self.write('No traceback available to show.\n') + return if etype is SyntaxError: + # Though this won't be called by syntax errors in the input + # line, there may be SyntaxError cases whith imported code. self.showsyntaxerror(filename) elif etype is UsageError: print "UsageError:", value @@ -1281,12 +1288,20 @@ class InteractiveShell(Component, Magic): if etype in self.custom_exceptions: self.CustomTB(etype,value,tb) else: - self.InteractiveTB(etype,value,tb,tb_offset=tb_offset) - if self.InteractiveTB.call_pdb: - # pdb mucks up readline, fix it back - self.set_completer() + if exception_only: + m = ('An exception has occurred, use %tb to see the ' + 'full traceback.') + print m + self.InteractiveTB.show_exception_only(etype, value) + else: + self.InteractiveTB(etype,value,tb,tb_offset=tb_offset) + if self.InteractiveTB.call_pdb: + # pdb mucks up readline, fix it back + self.set_completer() + except KeyboardInterrupt: - self.write("\nKeyboardInterrupt\n") + self.write("\nKeyboardInterrupt\n") + def showsyntaxerror(self, filename=None): """Display the syntax error that just occurred. @@ -1299,7 +1314,7 @@ class InteractiveShell(Component, Magic): """ etype, value, last_traceback = sys.exc_info() - # See note about these variables in showtraceback() below + # See note about these variables in showtraceback() above sys.last_type = etype sys.last_value = value sys.last_traceback = last_traceback @@ -1865,6 +1880,9 @@ class InteractiveShell(Component, Magic): # We are off again... __builtin__.__dict__['__IPYTHON__active'] -= 1 + # Turn off the exit flag, so the mainloop can be restarted if desired + self.exit_now = False + def safe_execfile(self, fname, *where, **kw): """A safe version of the builtin execfile(). @@ -1880,7 +1898,8 @@ class InteractiveShell(Component, Magic): One or two namespaces, passed to execfile() as (globals,locals). If only one is given, it is passed as both. exit_ignore : bool (False) - If True, then don't print errors for non-zero exit statuses. + If True, then silence SystemExit for non-zero status (it is always + silenced for zero status, as it is so common). """ kw.setdefault('exit_ignore', False) @@ -1905,40 +1924,21 @@ class InteractiveShell(Component, Magic): with prepended_to_syspath(dname): try: - if sys.platform == 'win32' and sys.version_info < (2,5,1): - # Work around a bug in Python for Windows. The bug was - # fixed in in Python 2.5 r54159 and 54158, but that's still - # SVN Python as of March/07. For details, see: - # http://projects.scipy.org/ipython/ipython/ticket/123 - try: - globs,locs = where[0:2] - except: - try: - globs = locs = where[0] - except: - globs = locs = globals() - exec file(fname) in globs,locs - else: - execfile(fname,*where) - except SyntaxError: - self.showsyntaxerror() - warn('Failure executing file: <%s>' % fname) + execfile(fname,*where) except SystemExit, status: - # Code that correctly sets the exit status flag to success (0) - # shouldn't be bothered with a traceback. Note that a plain - # sys.exit() does NOT set the message to 0 (it's empty) so that - # will still get a traceback. Note that the structure of the - # SystemExit exception changed between Python 2.4 and 2.5, so - # the checks must be done in a version-dependent way. - show = False - if status.args[0]==0 and not kw['exit_ignore']: - show = True - if show: - self.showtraceback() - warn('Failure executing file: <%s>' % fname) + # If the call was made with 0 or None exit status (sys.exit(0) + # or sys.exit() ), don't bother showing a traceback, as both of + # these are considered normal by the OS: + # > python -c'import sys;sys.exit(0)'; echo $? + # 0 + # > python -c'import sys;sys.exit()'; echo $? + # 0 + # For other exit status, we show the exception unless + # explicitly silenced, but only in short form. + if status.code not in (0, None) and not kw['exit_ignore']: + self.showtraceback(exception_only=True) except: self.showtraceback() - warn('Failure executing file: <%s>' % fname) def safe_execfile_ipy(self, fname): """Like safe_execfile, but for .ipy files with IPython syntax. @@ -2152,9 +2152,8 @@ class InteractiveShell(Component, Magic): sys.excepthook = old_excepthook except SystemExit: self.resetbuffer() - self.showtraceback() - warn("Type %exit or %quit to exit IPython " - "(%Exit or %Quit do so unconditionally).",level=1) + self.showtraceback(exception_only=True) + warn("To exit: use any of 'exit', 'quit', %Exit or Ctrl-D.", level=1) except self.custom_exceptions: etype,value,tb = sys.exc_info() self.CustomTB(etype,value,tb) diff --git a/IPython/core/magic.py b/IPython/core/magic.py index f333c51..dd3ed3d 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -3603,4 +3603,10 @@ Defaulting color scheme to 'NoColor'""" """ self.shell.enable_pylab(s) + def magic_tb(self, s): + """Print the last traceback with the currently active exception mode. + + See %xmode for changing exception reporting modes.""" + self.shell.showtraceback() + # end Magic diff --git a/IPython/core/tests/simpleerr.py b/IPython/core/tests/simpleerr.py new file mode 100644 index 0000000..34e6970 --- /dev/null +++ b/IPython/core/tests/simpleerr.py @@ -0,0 +1,32 @@ +"""Error script. DO NOT EDIT FURTHER! It will break exception doctests!!!""" +import sys + +def div0(): + "foo" + x = 1 + y = 0 + x/y + +def sysexit(stat, mode): + raise SystemExit(stat, 'Mode = %s' % mode) + +def bar(mode): + "bar" + if mode=='div': + div0() + elif mode=='exit': + try: + stat = int(sys.argv[2]) + except: + stat = 1 + sysexit(stat, mode) + else: + raise ValueError('Unknown mode') + +if __name__ == '__main__': + try: + mode = sys.argv[1] + except IndexError: + mode = 'div' + + bar(mode) diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py index e41cf11..f00204b 100644 --- a/IPython/core/tests/test_iplib.py +++ b/IPython/core/tests/test_iplib.py @@ -13,31 +13,15 @@ import tempfile import nose.tools as nt # our own packages -from IPython.core import iplib -from IPython.core import ipapi from IPython.testing import decorators as dec +from IPython.testing.globalipapp import get_ipython #----------------------------------------------------------------------------- # Globals #----------------------------------------------------------------------------- -# Useful global ipapi object and main IPython one. Unfortunately we have a -# long precedent of carrying the 'ipapi' global object which is injected into -# the system namespace as _ip, but that keeps a pointer to the actual IPython -# InteractiveShell instance, which is named IP. Since in testing we do need -# access to the real thing (we want to probe beyond what ipapi exposes), make -# here a global reference to each. In general, things that are exposed by the -# ipapi instance should be read from there, but we also will often need to use -# the actual IPython one. - -# Get the public instance of IPython, and if it's None, make one so we can use -# it for testing -ip = ipapi.get() -if ip is None: - # IPython not running yet, make one from the testing machinery for - # consistency when the test suite is being run via iptest - from IPython.testing.plugin import ipdoctest - ip = ipapi.get() +# Get the public instance of IPython +ip = get_ipython() #----------------------------------------------------------------------------- # Test functions @@ -73,3 +57,189 @@ def test_reset(): nvars_expected = 0 yield nt.assert_equals(len(ns), nvars_expected) + + +# Tests for reporting of exceptions in various modes, handling of SystemExit, +# and %tb functionality. This is really a mix of testing ultraTB and iplib. + +def doctest_tb_plain(): + """ +In [18]: xmode plain +Exception reporting mode: Plain + +In [19]: run simpleerr.py +Traceback (most recent call last): + ...line 32, in + bar(mode) + ...line 16, in bar + div0() + ...line 8, in div0 + x/y +ZeroDivisionError: integer division or modulo by zero + """ + + +def doctest_tb_context(): + """ +In [3]: xmode context +Exception reporting mode: Context + +In [4]: run simpleerr.py +--------------------------------------------------------------------------- +ZeroDivisionError Traceback (most recent call last) + +... in () + 30 mode = 'div' + 31 +---> 32 bar(mode) + 33 + 34 + +... in bar(mode) + 14 "bar" + 15 if mode=='div': +---> 16 div0() + 17 elif mode=='exit': + 18 try: + +... in div0() + 6 x = 1 + 7 y = 0 +----> 8 x/y + 9 + 10 def sysexit(stat, mode): + +ZeroDivisionError: integer division or modulo by zero +""" + + +def doctest_tb_verbose(): + """ +In [5]: xmode verbose +Exception reporting mode: Verbose + +In [6]: run simpleerr.py +--------------------------------------------------------------------------- +ZeroDivisionError Traceback (most recent call last) + +... in () + 30 mode = 'div' + 31 +---> 32 bar(mode) + global bar = + global mode = 'div' + 33 + 34 + +... in bar(mode='div') + 14 "bar" + 15 if mode=='div': +---> 16 div0() + global div0 = + 17 elif mode=='exit': + 18 try: + +... in div0() + 6 x = 1 + 7 y = 0 +----> 8 x/y + x = 1 + y = 0 + 9 + 10 def sysexit(stat, mode): + +ZeroDivisionError: integer division or modulo by zero + """ + + +def doctest_tb_sysexit(): + """ +In [17]: %xmode plain +Exception reporting mode: Plain + +In [18]: %run simpleerr.py exit +An exception has occurred, use %tb to see the full traceback. +SystemExit: (1, 'Mode = exit') + +In [19]: %run simpleerr.py exit 2 +An exception has occurred, use %tb to see the full traceback. +SystemExit: (2, 'Mode = exit') + +In [20]: %tb +Traceback (most recent call last): + File ... in + bar(mode) + File ... line 22, in bar + sysexit(stat, mode) + File ... line 11, in sysexit + raise SystemExit(stat, 'Mode = %s' % mode) +SystemExit: (2, 'Mode = exit') + +In [21]: %xmode context +Exception reporting mode: Context + +In [22]: %tb +--------------------------------------------------------------------------- +SystemExit Traceback (most recent call last) + +...() + 30 mode = 'div' + 31 +---> 32 bar(mode) + 33 + 34 + +...bar(mode) + 20 except: + 21 stat = 1 +---> 22 sysexit(stat, mode) + 23 else: + 24 raise ValueError('Unknown mode') + +...sysexit(stat, mode) + 9 + 10 def sysexit(stat, mode): +---> 11 raise SystemExit(stat, 'Mode = %s' % mode) + 12 + 13 def bar(mode): + +SystemExit: (2, 'Mode = exit') + +In [23]: %xmode verbose +Exception reporting mode: Verbose + +In [24]: %tb +--------------------------------------------------------------------------- +SystemExit Traceback (most recent call last) + +... in () + 30 mode = 'div' + 31 +---> 32 bar(mode) + global bar = + global mode = 'exit' + 33 + 34 + +... in bar(mode='exit') + 20 except: + 21 stat = 1 +---> 22 sysexit(stat, mode) + global sysexit = + stat = 2 + mode = 'exit' + 23 else: + 24 raise ValueError('Unknown mode') + +... in sysexit(stat=2, mode='exit') + 9 + 10 def sysexit(stat, mode): +---> 11 raise SystemExit(stat, 'Mode = %s' % mode) + global SystemExit = undefined + stat = 2 + mode = 'exit' + 12 + 13 def bar(mode): + +SystemExit: (2, 'Mode = exit') + """ diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index f27c681..3a517d2 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -312,6 +312,11 @@ def _format_traceback_lines(lnum, index, lines, Colors, lvals=None,scheme=None): # Module classes class TBTools: """Basic tools used by all traceback printer classes.""" + #: Default output stream, can be overridden at call time. A special value + #: of 'stdout' *as a string* can be given to force extraction of sys.stdout + #: at runtime. This allows testing exception printing with doctests, that + #: swap sys.stdout just at execution time. + out_stream = sys.stderr def __init__(self,color_scheme = 'NoColor',call_pdb=False): # Whether to call the interactive pdb debugger after printing @@ -379,13 +384,29 @@ class ListTB(TBTools): print >> Term.cerr, self.text(etype,value,elist) Term.cerr.flush() - def text(self,etype, value, elist,context=5): - """Return a color formatted string with the traceback info.""" + def text(self, etype, value, elist, context=5): + """Return a color formatted string with the traceback info. + + Parameters + ---------- + etype : exception type + Type of the exception raised. + + value : object + Data stored in the exception + + elist : list + List of frames, see class docstring for details. + + Returns + ------- + String with formatted exception. + """ Colors = self.Colors - out_string = ['%s%s%s\n' % (Colors.topline,'-'*60,Colors.Normal)] + out_string = [] if elist: - out_string.append('Traceback %s(most recent call last)%s:' % \ + out_string.append('Traceback %s(most recent call last)%s:' % (Colors.normalEm, Colors.Normal) + '\n') out_string.extend(self._format_list(elist)) lines = self._format_exception_only(etype, value) @@ -492,15 +513,29 @@ class ListTB(TBTools): else: list.append('%s\n' % str(stype)) - # vds:>> + # sync with user hooks if have_filedata: ipinst = ipapi.get() if ipinst is not None: ipinst.hooks.synchronize_with_editor(filename, lineno, 0) - # vds:<< return list + def show_exception_only(self, etype, value): + """Only print the exception type and message, without a traceback. + + Parameters + ---------- + etype : exception type + value : exception value + """ + # This method needs to use __call__ from *this* class, not the one from + # a subclass whose signature or behavior may be different + Term.cout.flush() + ostream = sys.stdout if self.out_stream == 'stdout' else Term.cerr + print >> ostream, ListTB.text(self, etype, value, []), + ostream.flush() + def _some_str(self, value): # Lifted from traceback.py try: @@ -980,6 +1015,7 @@ class AutoFormattedTB(FormattedTB): except: AutoTB() # or AutoTB(out=logfile) where logfile is an open file object """ + def __call__(self,etype=None,evalue=None,etb=None, out=None,tb_offset=None): """Print out a formatted exception traceback. @@ -990,9 +1026,9 @@ class AutoFormattedTB(FormattedTB): - tb_offset: the number of frames to skip over in the stack, on a per-call basis (this overrides temporarily the instance's tb_offset given at initialization time. """ - + if out is None: - out = Term.cerr + out = sys.stdout if self.out_stream=='stdout' else self.out_stream Term.cout.flush() if tb_offset is not None: tb_offset, self.tb_offset = self.tb_offset, tb_offset diff --git a/IPython/testing/globalipapp.py b/IPython/testing/globalipapp.py index bb2eba4..222c28a 100644 --- a/IPython/testing/globalipapp.py +++ b/IPython/testing/globalipapp.py @@ -20,6 +20,7 @@ import os import sys from . import tools +from IPython.utils.genutils import Term #----------------------------------------------------------------------------- # Functions @@ -96,7 +97,7 @@ class ipnsdict(dict): def get_ipython(): # This will get replaced by the real thing once we start IPython below - return None + return start_ipython() def start_ipython(): """Start a global IPython shell, which we need for IPython-specific syntax. @@ -133,6 +134,10 @@ def start_ipython(): ip = ipapp.IPythonApp(argv, user_ns=user_ns, user_global_ns=global_ns) ip.initialize() ip.shell.builtin_trap.set() + # Set stderr to stdout so nose can doctest exceptions + ## Term.cerr = sys.stdout + ## sys.stderr = sys.stdout + ip.shell.InteractiveTB.out_stream = 'stdout' # Butcher the logger ip.shell.log = lambda *a,**k: None @@ -143,8 +148,8 @@ def start_ipython(): sys.excepthook = _excepthook # So that ipython magics and aliases can be doctested (they work by making - # a call into a global _ip object) - + # a call into a global _ip object). Also make the top-level get_ipython + # now return this without calling here again _ip = ip.shell get_ipython = _ip.get_ipython __builtin__._ip = _ip @@ -155,11 +160,6 @@ def start_ipython(): # doctest machinery would miss them. ip.shell.system = xsys - # Also patch our %run function in. - ## im = new.instancemethod(_run_ns_sync,_ip, _ip.__class__) - ## ip.shell.magic_run_ori = _ip.magic_run - ## ip.shell.magic_run = im - # XXX - For some very bizarre reason, the loading of %history by default is # failing. This needs to be fixed later, but for now at least this ensures # that tests that use %hist run to completion. @@ -167,3 +167,5 @@ def start_ipython(): history.init_ipython(ip.shell) if not hasattr(ip.shell,'magic_history'): raise RuntimeError("Can't load magics, aborting") + + return _ip