Repository: ipython/ipython

Issue:
Bug with autocall
Hi,

We are seeing the following bug with autocall (where below is showing valid code for a type hint):
```
 $ ipython
Python 3.11.8 (main, Mar 15 2024, 12:37:54) [GCC 10.3.1 20210422 (Red Hat 10.3.1-1)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import collections.abc

In [2]: collections.abc.Callable[[int], None]
Out[2]: collections.abc.Callable[[int], None]

In [3]: %autocall 1
   ...: 
Automatic calling is: Smart

In [4]: collections.abc.Callable[[int], None]
   ...: 
------> collections.abc.Callable([[int], None])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 collections.abc.Callable([[int], None])

TypeError: Callable() takes no arguments
```


Discussion:

Patch:
diff --git a/IPython/core/prefilter.py b/IPython/core/prefilter.py
index e611b4b8340..a29df0c27ad 100644
--- a/IPython/core/prefilter.py
+++ b/IPython/core/prefilter.py
@@ -476,8 +476,8 @@ def check(self, line_info):
         any python operator, we should simply execute the line (regardless of
         whether or not there's a possible autocall expansion).  This avoids
         spurious (and very confusing) geattr() accesses."""
-        if line_info.the_rest and line_info.the_rest[0] in '!=()<>,+*/%^&|':
-            return self.prefilter_manager.get_handler_by_name('normal')
+        if line_info.the_rest and line_info.the_rest[0] in "!=()<>,+*/%^&|":
+            return self.prefilter_manager.get_handler_by_name("normal")
         else:
             return None
 
@@ -512,6 +512,8 @@ def check(self, line_info):
             callable(oinfo.obj)
             and (not self.exclude_regexp.match(line_info.the_rest))
             and self.function_name_regexp.match(line_info.ifun)
+            and line_info.raw_the_rest.startswith(" ")
+            or not line_info.raw_the_rest.strip()
         ):
             return self.prefilter_manager.get_handler_by_name("auto")
         else:
diff --git a/IPython/core/splitinput.py b/IPython/core/splitinput.py
index 0cd70ec9100..33e462b3b80 100644
--- a/IPython/core/splitinput.py
+++ b/IPython/core/splitinput.py
@@ -76,7 +76,7 @@ def split_user_input(line, pattern=None):
 
     # print('line:<%s>' % line)  # dbg
     # print('pre <%s> ifun <%s> rest <%s>' % (pre,ifun.strip(),the_rest))  # dbg
-    return pre, esc or '', ifun.strip(), the_rest.lstrip()
+    return pre, esc or "", ifun.strip(), the_rest
 
 
 class LineInfo(object):
@@ -107,11 +107,15 @@ class LineInfo(object):
 
     the_rest
       Everything else on the line.
+
+    raw_the_rest
+      the_rest without whitespace stripped.
     """
     def __init__(self, line, continue_prompt=False):
         self.line            = line
         self.continue_prompt = continue_prompt
-        self.pre, self.esc, self.ifun, self.the_rest = split_user_input(line)
+        self.pre, self.esc, self.ifun, self.raw_the_rest = split_user_input(line)
+        self.the_rest = self.raw_the_rest.lstrip()
 
         self.pre_char       = self.pre.strip()
         if self.pre_char:
@@ -136,3 +140,6 @@ def ofind(self, ip) -> OInfo:
 
     def __str__(self):
         return "LineInfo [%s|%s|%s|%s]" %(self.pre, self.esc, self.ifun, self.the_rest)
+
+    def __repr__(self):
+        return "<" + str(self) + ">"


Test patch:
diff --git a/IPython/core/tests/test_handlers.py b/IPython/core/tests/test_handlers.py
index 604dadee1ab..905d9abe07b 100644
--- a/IPython/core/tests/test_handlers.py
+++ b/IPython/core/tests/test_handlers.py
@@ -7,17 +7,13 @@
 # our own packages
 from IPython.core import autocall
 from IPython.testing import tools as tt
+import pytest
+from collections.abc import Callable
 
 #-----------------------------------------------------------------------------
 # Globals
 #-----------------------------------------------------------------------------
 
-# Get the public instance of IPython
-
-failures = []
-num_tests = 0
-
-#-----------------------------------------------------------------------------
 # Test functions
 #-----------------------------------------------------------------------------
 
@@ -31,67 +27,49 @@ def __call__(self):
         return "called"
 
 
-def run(tests):
-    """Loop through a list of (pre, post) inputs, where pre is the string
-    handed to ipython, and post is how that string looks after it's been
-    transformed (i.e. ipython's notion of _i)"""
-    tt.check_pairs(ip.prefilter_manager.prefilter_lines, tests)
-
+@pytest.mark.parametrize(
+    "autocall, input, output",
+    [
+        # For many of the below, we're also checking that leading whitespace
+        # turns off the esc char, which it should unless there is a continuation
+        # line.
+        ("1", '"no change"', '"no change"'),  # normal
+        ("1", "lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),  # magic
+        # Only explicit escapes or instances of IPyAutocallable should get
+        # expanded
+        ("0", 'len "abc"', 'len "abc"'),
+        ("0", "autocallable", "autocallable()"),
+        # Don't add extra brackets (gh-1117)
+        ("0", "autocallable()", "autocallable()"),
+        ("1", 'len "abc"', 'len("abc")'),
+        ("1", 'len "abc";', 'len("abc");'),  # ; is special -- moves out of parens
+        # Autocall is turned off if first arg is [] and the object
+        # is both callable and indexable.  Like so:
+        ("1", "len [1,2]", "len([1,2])"),  # len doesn't support __getitem__...
+        ("1", "call_idx [1]", "call_idx [1]"),  # call_idx *does*..
+        ("1", "call_idx 1", "call_idx(1)"),
+        ("1", "len", "len"),  # only at 2 does it auto-call on single args
+        ("2", 'len "abc"', 'len("abc")'),
+        ("2", 'len "abc";', 'len("abc");'),
+        ("2", "len [1,2]", "len([1,2])"),
+        ("2", "call_idx [1]", "call_idx [1]"),
+        ("2", "call_idx 1", "call_idx(1)"),
+        # T his is what's different:
+        ("2", "len", "len()"),  # only at 2 does it auto-call on single args
+        ("0", "Callable[[int], None]", "Callable[[int], None]"),
+        ("1", "Callable[[int], None]", "Callable[[int], None]"),
+        ("1", "Callable[[int], None]", "Callable[[int], None]"),
+    ],
+)
+def test_handlers_I(autocall, input, output):
+    autocallable = Autocallable()
+    ip.user_ns["autocallable"] = autocallable
 
-def test_handlers():
     call_idx = CallableIndexable()
-    ip.user_ns['call_idx'] = call_idx
+    ip.user_ns["call_idx"] = call_idx
 
-    # For many of the below, we're also checking that leading whitespace
-    # turns off the esc char, which it should unless there is a continuation
-    # line.
-    run(
-        [('"no change"', '"no change"'),             # normal
-         (u"lsmagic",     "get_ipython().run_line_magic('lsmagic', '')"),   # magic
-         #("a = b # PYTHON-MODE", '_i'),          # emacs -- avoids _in cache
-         ])
+    ip.user_ns["Callable"] = Callable
 
-    # Objects which are instances of IPyAutocall are *always* autocalled
-    autocallable = Autocallable()
-    ip.user_ns['autocallable'] = autocallable
-
-    # auto
-    ip.run_line_magic("autocall", "0")
-    # Only explicit escapes or instances of IPyAutocallable should get
-    # expanded
-    run(
-        [
-            ('len "abc"', 'len "abc"'),
-            ("autocallable", "autocallable()"),
-            # Don't add extra brackets (gh-1117)
-            ("autocallable()", "autocallable()"),
-        ]
-    )
+    ip.run_line_magic("autocall", autocall)
+    assert ip.prefilter_manager.prefilter_lines(input) == output
     ip.run_line_magic("autocall", "1")
-    run(
-        [
-            ('len "abc"', 'len("abc")'),
-            ('len "abc";', 'len("abc");'),  # ; is special -- moves out of parens
-            # Autocall is turned off if first arg is [] and the object
-            # is both callable and indexable.  Like so:
-            ("len [1,2]", "len([1,2])"),  # len doesn't support __getitem__...
-            ("call_idx [1]", "call_idx [1]"),  # call_idx *does*..
-            ("call_idx 1", "call_idx(1)"),
-            ("len", "len"),  # only at 2 does it auto-call on single args
-        ]
-    )
-    ip.run_line_magic("autocall", "2")
-    run(
-        [
-            ('len "abc"', 'len("abc")'),
-            ('len "abc";', 'len("abc");'),
-            ("len [1,2]", "len([1,2])"),
-            ("call_idx [1]", "call_idx [1]"),
-            ("call_idx 1", "call_idx(1)"),
-            # This is what's different:
-            ("len", "len()"),  # only at 2 does it auto-call on single args
-        ]
-    )
-    ip.run_line_magic("autocall", "1")
-
-    assert failures == []
diff --git a/IPython/core/tests/test_prefilter.py b/IPython/core/tests/test_prefilter.py
index 91c3c868821..999cd43e6e8 100644
--- a/IPython/core/tests/test_prefilter.py
+++ b/IPython/core/tests/test_prefilter.py
@@ -48,7 +48,7 @@ def dummy_magic(line): pass
 
 def test_autocall_binops():
     """See https://github.com/ipython/ipython/issues/81"""
-    ip.magic('autocall 2')
+    ip.run_line_magic("autocall", "2")
     f = lambda x: x
     ip.user_ns['f'] = f
     try:
@@ -71,8 +71,8 @@ def test_autocall_binops():
         finally:
             pm.unregister_checker(ac)
     finally:
-        ip.magic('autocall 0')
-        del ip.user_ns['f']
+        ip.run_line_magic("autocall", "0")
+        del ip.user_ns["f"]
 
 
 def test_issue_114():
@@ -105,23 +105,35 @@ def __call__(self, x):
             return x
 
     # Create a callable broken object
-    ip.user_ns['x'] = X()
-    ip.magic('autocall 2')
+    ip.user_ns["x"] = X()
+    ip.run_line_magic("autocall", "2")
     try:
         # Even if x throws an attribute error when looking at its rewrite
         # attribute, we should not crash.  So the test here is simply making
         # the prefilter call and not having an exception.
         ip.prefilter('x 1')
     finally:
-        del ip.user_ns['x']
-        ip.magic('autocall 0')
+        del ip.user_ns["x"]
+        ip.run_line_magic("autocall", "0")
+
+
+def test_autocall_type_ann():
+    ip.run_cell("import collections.abc")
+    ip.run_line_magic("autocall", "1")
+    try:
+        assert (
+            ip.prefilter("collections.abc.Callable[[int], None]")
+            == "collections.abc.Callable[[int], None]"
+        )
+    finally:
+        ip.run_line_magic("autocall", "0")
 
 
 def test_autocall_should_support_unicode():
-    ip.magic('autocall 2')
-    ip.user_ns['π'] = lambda x: x
+    ip.run_line_magic("autocall", "2")
+    ip.user_ns["π"] = lambda x: x
     try:
         assert ip.prefilter("π 3") == "π(3)"
     finally:
-        ip.magic('autocall 0')
-        del ip.user_ns['π']
+        ip.run_line_magic("autocall", "0")
+        del ip.user_ns["π"]
diff --git a/IPython/core/tests/test_splitinput.py b/IPython/core/tests/test_splitinput.py
index 1462e7fa033..f5fc53fafe3 100644
--- a/IPython/core/tests/test_splitinput.py
+++ b/IPython/core/tests/test_splitinput.py
@@ -1,7 +1,8 @@
 # coding: utf-8
 
 from IPython.core.splitinput import split_user_input, LineInfo
-from IPython.testing import tools as tt
+
+import pytest
 
 tests = [
     ("x=1", ("", "", "x", "=1")),
@@ -19,18 +20,19 @@
     (";ls", ("", ";", "ls", "")),
     ("  ;ls", ("  ", ";", "ls", "")),
     ("f.g(x)", ("", "", "f.g", "(x)")),
-    ("f.g (x)", ("", "", "f.g", "(x)")),
+    ("f.g (x)", ("", "", "f.g", " (x)")),
     ("?%hist1", ("", "?", "%hist1", "")),
     ("?%%hist2", ("", "?", "%%hist2", "")),
     ("??%hist3", ("", "??", "%hist3", "")),
     ("??%%hist4", ("", "??", "%%hist4", "")),
     ("?x*", ("", "?", "x*", "")),
+    ("Pérez Fernando", ("", "", "Pérez", " Fernando")),
 ]
-tests.append(("Pérez Fernando", ("", "", "Pérez", "Fernando")))
 
 
-def test_split_user_input():
-    return tt.check_pairs(split_user_input, tests)
+@pytest.mark.parametrize("input, output", tests)
+def test_split_user_input(input, output):
+    assert split_user_input(input) == output
 
 
 def test_LineInfo():