Repository: ipython/ipython

Issue:
Cannot make multi-line code blocks in ipython
I just did a `pip install ipython` while teaching a class today and typed an if statement and then hit enter after the first line and the code executed.

This seems like a bug.

I did a `pip install ipython==6.5.0` and hitting Enter in a code block properly showed me the next indented line to type out.

I'm on Ubuntu 18.04 running ipython within tmux, though I doubt tmux is the issue here.

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 09e161464..cf184a9d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,6 +34,7 @@ after_success:
 
 matrix:
     include:
+        - { python: "3.6-dev", dist: xenial, sudo: true }
         - { python: "3.7", dist: xenial, sudo: true }
         - { python: "3.7-dev", dist: xenial, sudo: true }
         - { python: "nightly", dist: xenial, sudo: true }
diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py
index 0a0110039..5777559fb 100644
--- a/IPython/core/inputtransformer2.py
+++ b/IPython/core/inputtransformer2.py
@@ -234,6 +234,7 @@ def find(cls, tokens_by_line):
         for line in tokens_by_line:
             assign_ix = _find_assign_op(line)
             if (assign_ix is not None) \
+                    and not line[assign_ix].line.strip().startswith('=') \
                     and (len(line) >= assign_ix + 2) \
                     and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
                 ix = assign_ix + 1
@@ -369,11 +370,15 @@ def transform(self, lines):
         end_line = find_end_of_continued_line(lines, start_line)
         line = assemble_continued_line(lines, (start_line, start_col), end_line)
 
-        if line[:2] in ESCAPE_DOUBLES:
+        if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
             escape, content = line[:2], line[2:]
         else:
             escape, content = line[:1], line[1:]
-        call = tr[escape](content)
+
+        if escape in tr:
+            call = tr[escape](content)
+        else:
+            call = ''
 
         lines_before = lines[:start_line]
         new_line = indent + call + '\n'
@@ -466,9 +471,12 @@ def make_tokens_by_line(lines):
     except tokenize.TokenError:
         # Input ended in a multiline string or expression. That's OK for us.
         pass
+
+
     if not tokens_by_line[-1]:
         tokens_by_line.pop()
 
+
     return tokens_by_line
 
 def show_linewise_tokens(s: str):
@@ -575,10 +583,28 @@ def check_complete(self, cell: str):
           The number of spaces by which to indent the next line of code. If
           status is not 'incomplete', this is None.
         """
-        if not cell.endswith('\n'):
-            cell += '\n'  # Ensure the cell has a trailing newline
+        # Remember if the lines ends in a new line.
+        ends_with_newline = False
+        for character in reversed(cell):
+            if character == '\n':
+                ends_with_newline = True
+                break
+            elif character.strip():
+                break
+            else:
+                continue
+
+        if ends_with_newline:
+            # Append an newline for consistent tokenization
+            # See https://bugs.python.org/issue33899
+            cell += '\n'
+
         lines = cell.splitlines(keepends=True)
-        if lines[-1][:-1].endswith('\\'):
+
+        if not lines:
+            return 'complete', None
+
+        if lines[-1].endswith('\\'):
             # Explicit backslash continuation
             return 'incomplete', find_last_indent(lines)
 
@@ -603,35 +629,41 @@ def check_complete(self, cell: str):
             return 'invalid', None
 
         tokens_by_line = make_tokens_by_line(lines)
+
         if not tokens_by_line:
             return 'incomplete', find_last_indent(lines)
+
         if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
             # We're in a multiline string or expression
             return 'incomplete', find_last_indent(lines)
-        if len(tokens_by_line) == 1:
-            return 'incomplete', find_last_indent(lines)
-        # Find the last token on the previous line that's not NEWLINE or COMMENT
-        toks_last_line = tokens_by_line[-2]
-        ix = len(toks_last_line) - 1
-        while ix >= 0 and toks_last_line[ix].type in {tokenize.NEWLINE,
-                                                      tokenize.COMMENT}:
-            ix -= 1
-
-        if toks_last_line[ix].string == ':':
+
+        newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER}
+
+        # Remove newline_types for the list of tokens
+        while len(tokens_by_line) > 1 and len(tokens_by_line[-1]) == 1 \
+                and tokens_by_line[-1][-1].type in newline_types:
+            tokens_by_line.pop()
+
+        while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
+            tokens_by_line[-1].pop()
+
+        if len(tokens_by_line) == 1 and not tokens_by_line[-1]:
+            return 'incomplete', 0
+
+        if tokens_by_line[-1][-1].string == ':':
             # The last line starts a block (e.g. 'if foo:')
             ix = 0
-            while toks_last_line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
+            while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
                 ix += 1
-            indent = toks_last_line[ix].start[1]
+
+            indent = tokens_by_line[-1][ix].start[1]
             return 'incomplete', indent + 4
 
-        # If there's a blank line at the end, assume we're ready to execute.
-        if not lines[-1].strip():
-            return 'complete', None
+        if tokens_by_line[-1][0].line.endswith('\\'):
+            return 'incomplete', None
 
         # At this point, our checks think the code is complete (or invalid).
-        # We'll use codeop.compile_command to check this with the real parser.
-
+        # We'll use codeop.compile_command to check this with the real parser
         try:
             with warnings.catch_warnings():
                 warnings.simplefilter('error', SyntaxWarning)
@@ -642,6 +674,16 @@ def check_complete(self, cell: str):
         else:
             if res is None:
                 return 'incomplete', find_last_indent(lines)
+
+        if tokens_by_line[-1][-1].type == tokenize.DEDENT:
+            if ends_with_newline:
+                return 'complete', None
+            return 'incomplete', find_last_indent(lines)
+
+        # If there's a blank line at the end, assume we're ready to execute
+        if not lines[-1].strip():
+            return 'complete', None
+
         return 'complete', None
 
 


Test patch:
diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py
index 77df22c2c..6a57b681c 100644
--- a/IPython/core/tests/test_inputtransformer2.py
+++ b/IPython/core/tests/test_inputtransformer2.py
@@ -10,6 +10,8 @@
 from IPython.core import inputtransformer2 as ipt2
 from IPython.core.inputtransformer2 import make_tokens_by_line
 
+from textwrap import dedent
+
 MULTILINE_MAGIC = ("""\
 a = f()
 %foo \\
@@ -212,6 +214,16 @@ def test_check_complete():
     nt.assert_equal(cc("a = '''\n   hi"), ('incomplete', 3))
     nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None))
     nt.assert_equal(cc("a \\ "), ('invalid', None))  # Nothing allowed after backslash
+    nt.assert_equal(cc("1\\\n+2"), ('complete', None))
+    nt.assert_equal(cc("exit"), ('complete', None))
+
+    example = dedent("""
+        if True:
+            a=1""" )
+
+    nt.assert_equal(cc(example), ('incomplete', 4))
+    nt.assert_equal(cc(example+'\n'), ('complete', None))
+    nt.assert_equal(cc(example+'\n    '), ('complete', None))
 
     # no need to loop on all the letters/numbers.
     short = '12abAB'+string.printable[62:]