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 e611b4b83..a29df0c27 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 0cd70ec91..33e462b3b 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) + ">"
diff --git a/docs/source/config/custommagics.rst b/docs/source/config/custommagics.rst
index 4854970ef..0a37b858a 100644
--- a/docs/source/config/custommagics.rst
+++ b/docs/source/config/custommagics.rst
@@ -141,7 +141,7 @@ Accessing user namespace and local scope
 When creating line magics, you may need to access surrounding scope  to get user
 variables (e.g when called inside functions). IPython provides the
 ``@needs_local_scope`` decorator that can be imported from
-``IPython.core.magic``. When decorated with ``@needs_local_scope`` a magic will
+``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will
 be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope``
 can also be applied to cell magics even if cell magics cannot appear at local
 scope context.
@@ -153,7 +153,7 @@ Sometimes it may be useful to define a magic that can be silenced the same way
 that non-magic expressions can, i.e., by appending a semicolon at the end of the Python
 code to be executed. That can be achieved by decorating the magic function with
 the decorator ``@output_can_be_silenced`` that can be imported from
-``IPython.core.magic``. When this decorator is used, IPython will parse the Python
+``IPython.core.magics``. When this decorator is used, IPython will parse the Python
 code used by the magic and, if the last token is a ``;``, the output created by the
 magic will not show up on the screen. If you want to see an example of this decorator
 in action, take a look on the ``time`` magic defined in
diff --git a/tools/release b/tools/release
index 5164a2bb6..ffc7e63cb 100755
--- a/tools/release
+++ b/tools/release
@@ -1,10 +1,86 @@
 #!/usr/bin/env python3
 """IPython release script.
 
-Deprecated
-
+This should ONLY be run at real release time.
 """
-sys.exit("deprecated")
+from __future__ import print_function
+
+import os
+from glob import glob
+from pathlib import Path
+from subprocess import call
+import sys
+
+from toollib import (get_ipdir, cd, execfile, sh, archive,
+         archive_user, archive_dir)
+
+# Get main ipython dir, this will raise if it doesn't pass some checks
+ipdir = get_ipdir()
+tooldir = ipdir / 'tools'
+distdir = ipdir / 'dist'
+
+# Where I keep static backups of each release
+ipbackupdir = Path('~/ipython/backup').expanduser()
+if not ipbackupdir.exists():
+    ipbackupdir.mkdir(parents=True, exist_ok=True)
+
+# Start in main IPython dir
+cd(ipdir)
+
+# Load release info
+version = None
+execfile(Path('IPython','core','release.py'), globals())
+
+# Build site addresses for file uploads
+release_site = '%s/release/%s' % (archive, version)
+backup_site =  '%s/backup/' % archive
+
+# Start actual release process
+print()
+print('Releasing IPython')
+print('=================')
+print()
+print('Version:', version)
+print()
+print('Source IPython directory:', ipdir)
+print()
+
+# Perform local backup, go to tools dir to run it.
+cd(tooldir)
+
+if 'upload' in sys.argv:
+    cd(distdir)
+
+    # do not upload OS specific files like .DS_Store
+    to_upload = glob('*.whl')+glob('*.tar.gz')
+
+    # Make target dir if it doesn't exist
+    print('1. Uploading IPython to archive.ipython.org')
+    sh('ssh %s "mkdir -p %s/release/%s" ' % (archive_user, archive_dir, version))
+    sh('scp *.tar.gz *.whl %s' % release_site)
+
+    print('2. Uploading backup files...')
+    cd(ipbackupdir)
+    sh('scp `ls -1tr *tgz | tail -1` %s' % backup_site)
+
+    print('3. Uploading to PyPI using twine')
+    cd(distdir)
+    call(['twine', 'upload', '--verbose'] + to_upload)
+
+else:
+    # Build, but don't upload
+
+    # Make backup tarball
+    sh('python make_tarball.py')
+    sh('mv ipython-*.tgz %s' % ipbackupdir)
+
+    # Build release files
+    sh('./build_release')
+
+    cd(ipdir)
+
+    print("`./release upload` to upload source distribution on PyPI and ipython archive")
+    sys.exit(0)
 
 
 
diff --git a/tools/release_helper.sh b/tools/release_helper.sh
index 08c8f6b85..df1ca0d48 100644
--- a/tools/release_helper.sh
+++ b/tools/release_helper.sh
@@ -231,7 +231,7 @@ then
     echo
     echo $BLUE"Attempting to build package..."$NOR
 
-    tools/build_release
+    tools/release
 
 
     echo $RED'$ shasum -a 256 dist/*'
@@ -245,7 +245,7 @@ then
     echo
     echo $BLUE"Attempting to build package..."$NOR
 
-    tools/build_release
+    tools/release
 
     echo $RED"Check the shasum for SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH"
     echo $RED'$ shasum -a 256 dist/*'
@@ -254,6 +254,6 @@ then
 
     if ask_section "upload packages ?"
     then
-       twine upload --verbose dist/*.tar.gz dist/*.whl
+       tools/release upload
     fi
 fi


Test patch:
diff --git a/IPython/core/tests/test_handlers.py b/IPython/core/tests/test_handlers.py
index 604dadee1..905d9abe0 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 91c3c8688..999cd43e6 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 1462e7fa0..f5fc53faf 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():