Repository: ipython/ipython

Issue:
set_custom_exc exception handlers don't catch SyntaxError
```
In [6]: ipython = get_ipython()

In [7]: def exc_handler(self, etype, value, tb, tb_offset=None):
   ...:     print(etype)
   ...:     return self.showtraceback()
   ...:

In [9]: ipython.set_custom_exc((BaseException,), exc_handler)

In [10]: bad
<class 'NameError'>
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-10-df207dc9143c> in <module>()
----> 1 bad

NameError: name 'bad' is not defined

In [11]: 1f
  File "<ipython-input-11-0cd11c36f170>", line 1
    1f
     ^
SyntaxError: invalid syntax
```

(notice no `<class 'SyntaxError'>`)

I vaguely remember noticing this before in the code because syntax errors are handled specially. 

This causes my [iTerm2 extension](https://iterm2-tools.readthedocs.org/en/latest/ipython.html) to not register syntax errors as "failed" prompts.



Discussion:
If I recall correctly, SyntaxErrors are handled differently, since they fail before execution starts. Should be fixable, though.

@asmeureur, is that ok if for now you need to register separately a handler for SyntaxErrors ? 

We can likely fix it for 5.0, but I'd like to have more time to think how `ipython.set_custom_exc` dealing also with syntax error would affect the rest of the machinery. 

That would mean for 5.0 I could likely expose a provisional `_extra_syntax_error_exc(self, etype, value, tb, tb_offset=None)` which is called before our actual handeling of SyntaxError, and refine the API for 6.0. 

How can I register handlers for syntax errors? 

> How can I register handlers for syntax errors?

Sorry, I was unclear. 

Is that ok if for 5.0 I add a separate specific method to register handler only  for `SyntaxError` that would be provisional?  Or do you **need** `ipython.set_custom_exc` to also handle `SyntaxError`s ?

I don't really care if it's separate, as long as it's documented. If it is possible to do the same thing with SyntaxError I don't see why you wouldn't just make `set_custom_exc` do that. 

Because, As far as I can tell, `self.showtraceback()` calls `self.showsyntaxerror()` on `SyntaxError`, and  to do what you ask,I need `self.showsyntaxerror()` to call call your handler, which would call  `self.showtraceback()`... which leads to recursion. 

but right now, `self.showtraceback()` is never called,  as `SyntaxError`  never reaches it, as  `showsyntaxerror()` is called at `compiler.ast_parse()` time, even before the code is run.

Agreed in the best word, `set_custom_exc` should do it, but it likely require **significant** refactor I'm not comfortable doing now. 

> Because, As far as I can tell, self.showtraceback() calls self.showsyntaxerror() on SyntaxError, and to do what you ask,I need self.showsyntaxerror() to call call your handler, which would call self.showtraceback()... which leads to recursion.
> 
> but right now, self.showtraceback() is never called, as SyntaxError never reaches it, as showsyntaxerror() is called at compiler.ast_parse() time, even before the code is run.
> 
> Agreed in the best word, set_custom_exc should do it, but it likely require significant refactor I'm not comfortable doing now.

Actually that's not completely true... let me see.


Resolving patch:
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -1604,7 +1604,7 @@ def init_traceback_handlers(self, custom_exceptions):
         self.InteractiveTB.set_mode(mode=self.xmode)
 
     def set_custom_exc(self, exc_tuple, handler):
-        """set_custom_exc(exc_tuple,handler)
+        """set_custom_exc(exc_tuple, handler)
 
         Set a custom exception handler, which will be called if any of the
         exceptions in exc_tuple occur in the mainloop (specifically, in the
@@ -1647,7 +1647,7 @@ def my_handler(self, etype, value, tb, tb_offset=None):
         assert type(exc_tuple)==type(()) , \
                "The custom exceptions must be given AS A TUPLE."
 
-        def dummy_handler(self,etype,value,tb,tb_offset=None):
+        def dummy_handler(self, etype, value, tb, tb_offset=None):
             print('*** Simple custom exception handler ***')
             print('Exception type :',etype)
             print('Exception value:',value)
@@ -2682,6 +2682,10 @@ def error_before_exec(value):
                 # Compile to bytecode
                 try:
                     code_ast = compiler.ast_parse(cell, filename=cell_name)
+                except self.custom_exceptions as e:
+                    etype, value, tb = sys.exc_info()
+                    self.CustomTB(etype, value, tb)
+                    return error_before_exec(e)
                 except IndentationError as e:
                     self.showindentationerror()
                     if store_history:


Test patch:
diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py
--- a/IPython/core/tests/test_interactiveshell.py
+++ b/IPython/core/tests/test_interactiveshell.py
@@ -434,6 +434,21 @@ def foo(self):
         found = ip._ofind('a.foo', [('locals', locals())])
         nt.assert_is(found['obj'], A.foo)
 
+    def test_custom_syntaxerror_exception(self):
+        called = []
+        def my_handler(shell, etype, value, tb, tb_offset=None):
+            called.append(etype)
+            shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
+
+        ip.set_custom_exc((SyntaxError,), my_handler)
+        try:
+            ip.run_cell("1f")
+            # Check that this was called, and only once.
+            self.assertEqual(called, [SyntaxError])
+        finally:
+            # Reset the custom exception hook
+            ip.set_custom_exc((), None)
+
     def test_custom_exception(self):
         called = []
         def my_handler(shell, etype, value, tb, tb_offset=None):