Repository: joblib/joblib

Issue:
Wrong line pointed to in subprocess traceback
test.py
```py
from test_module import exception_on_even

from joblib import Parallel, delayed

if __name__ == '__main__':
    Parallel(n_jobs=2)(delayed(exception_on_even)(x) for x in [1, 2, 3])
```

test_module.py
```py
def exception_on_even(x):
    if x % 2 == 0:
        1/0
    else:
        return x
```

Excerpt from the Traceback when running `ipython test.py`:
```
---------------------------------------------------------------------------
Sub-process traceback:
---------------------------------------------------------------------------
ZeroDivisionError                                  Wed Nov  2 09:52:46 2016
PID: 7267                 Python 3.5.2: /home/lesteve/miniconda3/bin/python
...........................................................................
/home/lesteve/dev/joblib/joblib/parallel.py in __call__(self=<joblib.parallel.BatchedCalls object>)
    126     def __init__(self, iterator_slice):
    127         self.items = list(iterator_slice)
    128         self._size = len(self.items)
    129 
    130     def __call__(self):
--> 131         return [func(*args, **kwargs) for func, args, kwargs in self.items]
        self.items = [(<function exception_on_even>, (2,), {})]
    132 
    133     def __len__(self):
    134         return self._size
    135 

...........................................................................
/home/lesteve/dev/joblib/joblib/parallel.py in <listcomp>(.0=<list_iterator object>)
    126     def __init__(self, iterator_slice):
    127         self.items = list(iterator_slice)
    128         self._size = len(self.items)
    129 
    130     def __call__(self):
--> 131         return [func(*args, **kwargs) for func, args, kwargs in self.items]
        func = <function exception_on_even>
        args = (2,)
        kwargs = {}
    132 
    133     def __len__(self):
    134         return self._size
    135 

...........................................................................
/tmp/test_module.py in exception_on_even(x=2)
      1 
      2 
----> 3 
      4 def exception_on_even(x):
      5     if x % 2 == 0:
      6         1/0
      7     else:
      8         return x
      9 
     10 

ZeroDivisionError: division by zero
```

The arrow in the last frame points to line 3 whereas it should point to line 6.

Discussion:
lesteve: For the record, an excerpt of the output from the snippet in #420 (on this PR):

```
---------------------------------------------------------------------------
Sub-process traceback:
---------------------------------------------------------------------------
ZeroDivisionError                                  Wed Dec  7 15:16:32 2016
PID: 22133                Python 3.5.2: /home/lesteve/miniconda3/bin/python
...........................................................................
/home/lesteve/dev/joblib/joblib/parallel.py in __call__(self=<joblib.parallel.BatchedCalls object>)
    126     def __init__(self, iterator_slice):
    127         self.items = list(iterator_slice)
    128         self._size = len(self.items)
    129 
    130     def __call__(self):
--> 131         return [func(*args, **kwargs) for func, args, kwargs in self.items]
        self.items = [(<function exception_on_even>, (2,), {})]
    132 
    133     def __len__(self):
    134         return self._size
    135 

...........................................................................
/home/lesteve/dev/joblib/joblib/parallel.py in <listcomp>(.0=<list_iterator object>)
    126     def __init__(self, iterator_slice):
    127         self.items = list(iterator_slice)
    128         self._size = len(self.items)
    129 
    130     def __call__(self):
--> 131         return [func(*args, **kwargs) for func, args, kwargs in self.items]
        func = <function exception_on_even>
        args = (2,)
        kwargs = {}
    132 
    133     def __len__(self):
    134         return self._size
    135 

...........................................................................
/tmp/test_module.py in exception_on_even(x=2)
      1 def exception_on_even(x):
      2     if x % 2 == 0:
----> 3         1/0
      4     else:
      5         return x

ZeroDivisionError: division by zero
___________________________________________________________________________
```
lesteve: OK I am quite happy with the tests I added and I don't think there was any strong reason for the padding, so I am going to merge this one.

@GaelVaroquaux @ogrisel shout if you see any issue with this and I'll revert it.

Patch:
diff --git a/joblib/format_stack.py b/joblib/format_stack.py
index 3f3d106..4be93c1 100644
--- a/joblib/format_stack.py
+++ b/joblib/format_stack.py
@@ -135,15 +135,10 @@ def _fixed_getframes(etb, context=1, tb_offset=0):
     aux = traceback.extract_tb(etb)
     assert len(records) == len(aux)
     for i, (file, lnum, _, _) in enumerate(aux):
-        maybeStart = lnum - 1 - context // 2
-        start = max(maybeStart, 0)
+        maybe_start = lnum - 1 - context // 2
+        start = max(maybe_start, 0)
         end = start + context
         lines = linecache.getlines(file)[start:end]
-        # pad with empty lines if necessary
-        if maybeStart < 0:
-            lines = (['\n'] * -maybeStart) + lines
-        if len(lines) < context:
-            lines += ['\n'] * (context - len(lines))
         buf = list(records[i])
         buf[LNUM_POS] = lnum
         buf[INDEX_POS] = lnum - 1 - start
@@ -400,15 +395,10 @@ def format_outer_frames(context=5, stack_start=None, stack_end=None,
             if (os.path.basename(filename) in ('iplib.py', 'py3compat.py')
                         and func_name in ('execfile', 'safe_execfile', 'runcode')):
                 break
-        maybeStart = line_no - 1 - context // 2
-        start = max(maybeStart, 0)
+        maybe_start = line_no - 1 - context // 2
+        start = max(maybe_start, 0)
         end = start + context
         lines = linecache.getlines(filename)[start:end]
-        # pad with empty lines if necessary
-        if maybeStart < 0:
-            lines = (['\n'] * -maybeStart) + lines
-        if len(lines) < context:
-            lines += ['\n'] * (context - len(lines))
         buf = list(records[i])
         buf[LNUM_POS] = line_no
         buf[INDEX_POS] = line_no - 1 - start


Test patch:
diff --git a/joblib/test/test_format_stack.py b/joblib/test/test_format_stack.py
index 32fc8f5..baa8076 100644
--- a/joblib/test/test_format_stack.py
+++ b/joblib/test/test_format_stack.py
@@ -6,6 +6,8 @@ Unit tests for the stack formatting utilities
 # Copyright (c) 2010 Gael Varoquaux
 # License: BSD Style, 3 clauses.
 
+import imp
+import os
 import re
 import sys
 
@@ -75,6 +77,38 @@ def test_format_records():
                          re.MULTILINE)
 
 
+def test_format_records_file_with_less_lines_than_context(tmpdir):
+    # See https://github.com/joblib/joblib/issues/420
+    filename = os.path.join(tmpdir.strpath, 'small_file.py')
+    code_lines = ['def func():', '    1/0']
+    code = '\n'.join(code_lines)
+    open(filename, 'w').write(code)
+
+    small_file = imp.load_source('small_file', filename)
+    try:
+        small_file.func()
+    except ZeroDivisionError:
+        etb = sys.exc_info()[2]
+
+        records = _fixed_getframes(etb, context=10)
+        # Check that if context is bigger than the number of lines in
+        # the file you do not get padding
+        frame, tb_filename, line, func_name, context, _ = records[-1]
+        assert [l.rstrip() for l in context] == code_lines
+
+        formatted_records = format_records(records)
+        # 2 lines for header in the traceback: lines of ...... +
+        # filename with function
+        len_header = 2
+        nb_lines_formatted_records = len(formatted_records[1].splitlines())
+        assert (nb_lines_formatted_records == len_header + len(code_lines))
+        # Check exception stack
+        arrow_regex = r'^-+>\s+\d+\s+'
+        assert re.search(arrow_regex + r'1/0',
+                         formatted_records[1],
+                         re.MULTILINE)
+
+
 @with_numpy
 def test_format_exc_with_compiled_code():
     # Trying to tokenize compiled C code raise SyntaxError.