Repository: hhatto/autopep8

Issue:
SyntaxError rewriting specific combination of imports and defs
---

## Python Code
```python
def setup():
    pass

from a import b
from b import c
from b import d
```

## Command Line and Configuration

Command Line
```shell
$ autopep8 -i t.py
Traceback (most recent call last):
  File "/tmp/pyqtgraph/venv/bin/autopep8", line 10, in <module>
    sys.exit(main())
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 4200, in main
    ret = fix_multiple_files(args.files, args, sys.stdout)
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 4100, in fix_multiple_files
    ret = _fix_file((name, options, output))
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 4080, in _fix_file
    return fix_file(*parameters)
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 3454, in fix_file
    fixed_source = fix_lines(fixed_source, options, filename=filename)
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 3434, in fix_lines
    fixed_source = fix.fix()
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 593, in fix
    aggressive=self.options.aggressive))
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 535, in _fix_source
    modified_lines = fix(result)
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 889, in fix_e402
    line_index)
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 1442, in get_module_imports_on_top_of_file
    if cnt == import_line_index or is_future_import(line):
  File "/tmp/pyqtgraph/venv/lib/python3.6/site-packages/autopep8.py", line 1412, in is_future_import
    nodes = ast.parse(line)
  File "/usr/lib/python3.6/ast.py", line 35, in parse
    return compile(source, filename, mode, PyCF_ONLY_AST)
  File "<unknown>", line 2
    def setup():
               ^
SyntaxError: unexpected EOF while parsing
```

## Your Environment
```console
$ python --version --version
Python 3.6.8 (default, Jan 14 2019, 11:02:34) 
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]]
$ autopep8 --version
autopep8 1.4.4 (pycodestyle: 2.5.0)
```

## Analysis

it appears to be happening while checking for `__future__` imports -- the import reordering code is combining a `from` import line with the `def` line causing the "check if this import is a `__future__` import" code to parse too much

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 7139401..3dea285 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@
 .build-*/
 README.html
 __pycache__
+.mypy_cache/
 build
 .coverage
 coverage.xml
diff --git a/autopep8.py b/autopep8.py
index 01b90d6..8d73736 100755
--- a/autopep8.py
+++ b/autopep8.py
@@ -48,6 +48,7 @@ import difflib
 import fnmatch
 import inspect
 import io
+import itertools
 import keyword
 import locale
 import os
@@ -1415,10 +1416,24 @@ def get_module_imports_on_top_of_file(source, import_line_index):
                 return True
         return False
 
+    def has_future_import(source):
+        offset = 0
+        line = ''
+        for _, next_line in source:
+            for line_part in next_line.strip().splitlines(True):
+                line = line + line_part
+                try:
+                    return is_future_import(line), offset
+                except SyntaxError:
+                    continue
+            offset += 1
+        return False, offset
+
     allowed_try_keywords = ('try', 'except', 'else', 'finally')
     in_docstring = False
     docstring_kind = '"""'
-    for cnt, line in enumerate(source):
+    source_stream = iter(enumerate(source))
+    for cnt, line in source_stream:
         if not in_docstring:
             m = DOCSTRING_START_REGEX.match(line.lstrip())
             if m is not None:
@@ -1438,10 +1453,20 @@ def get_module_imports_on_top_of_file(source, import_line_index):
         elif line.startswith('#'):
             continue
 
-        if line.startswith('import ') or line.startswith('from '):
-            if cnt == import_line_index or is_future_import(line):
+        if line.startswith('import '):
+            if cnt == import_line_index:
                 continue
             return cnt
+        elif line.startswith('from '):
+            if cnt == import_line_index:
+                continue
+            hit, offset = has_future_import(
+                itertools.chain([(cnt, line)], source_stream)
+            )
+            if hit:
+                # move to the back
+                return cnt + offset + 1
+            return cnt
         elif pycodestyle.DUNDER_REGEX.match(line):
             continue
         elif any(line.startswith(kw) for kw in allowed_try_keywords):


Test patch:
diff --git a/test/test_autopep8.py b/test/test_autopep8.py
index d3203ba..fa3bdc6 100755
--- a/test/test_autopep8.py
+++ b/test/test_autopep8.py
@@ -2422,6 +2422,56 @@ a = 1     # (E305)
         with autopep8_context(line) as result:
             self.assertEqual(fixed, result)
 
+    def test_e401_with_multiline_from_import(self):
+        line = """\
+from os import (
+    chroot
+)
+def f():
+    pass
+from a import b
+from b import c
+from c import d
+"""
+        fixed = """\
+from a import b
+from c import d
+from b import c
+from os import (
+    chroot
+)
+
+
+def f():
+    pass
+"""
+        with autopep8_context(line) as result:
+            self.assertEqual(fixed, result)
+
+    def test_e402_with_multiline_from_future_import(self):
+        line = """\
+from __future__ import (
+    absolute_import,
+    print_function
+)
+def f():
+    pass
+import os
+"""
+        fixed = """\
+from __future__ import (
+    absolute_import,
+    print_function
+)
+import os
+
+
+def f():
+    pass
+"""
+        with autopep8_context(line) as result:
+            self.assertEqual(fixed, result)
+
     def test_e402_with_module_doc(self):
         line1 = '"""\nmodule doc\n"""\na = 1\nimport os\n'
         fixed1 = '"""\nmodule doc\n"""\nimport os\na = 1\n'