Repository: ipython/ipython

Issue:
set_custom_completer seems to be a no-op
In [interactive_shell.py](https://github.com/ipython/ipython/blob/a2685d78f2403f84e6cb915ae11a4f6033ccc5f6/IPython/core/interactiveshell.py#L2034), `set_custom_completer` does

    self.Completer.matchers.insert(pos,newcomp)

Looking at the source code of IPCompleter, though, [matchers](https://github.com/ipython/ipython/blob/eed56ba34aa3856b5726d60eedfe60a17e1c26c5/IPython/core/completer.py#L1114) seems to be a property returning an ephemeral list, so the insert is a no-op.

To reproduce:

    import IPython

    def foo(*args, **kwargs):
        pass

    print(len(IPython.get_ipython().Completer.matchers))
    IPython.get_ipython().set_custom_completer(foo, 0)
    print(len(IPython.get_ipython().Completer.matchers))

This should print the same number twice (in my version, 5, 5)


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index def0d930b..4139cc2b6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -72,7 +72,7 @@ matrix:
     - arch: arm64
       python: "3.7"
       dist: xenial
-      env: ARM64=True
+      env: ARM64=True IPYTHON_TESTING_TIMEOUT_SCALE=2
       sudo: true
     - arch: amd64
       python: "3.8"
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
index 0942798f3..9a81d637f 100644
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -626,6 +626,8 @@ def __init__(self, namespace=None, global_namespace=None, **kwargs):
         else:
             self.global_namespace = global_namespace
 
+        self.custom_matchers = []
+
         super(Completer, self).__init__(**kwargs)
 
     def complete(self, text, state):
@@ -1122,12 +1124,14 @@ def matchers(self):
 
         if self.use_jedi:
             return [
+                *self.custom_matchers,
                 self.file_matches,
                 self.magic_matches,
                 self.dict_key_matches,
             ]
         else:
             return [
+                *self.custom_matchers,
                 self.python_matches,
                 self.file_matches,
                 self.magic_matches,
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index 4cbc885cb..848a14a48 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -2218,7 +2218,7 @@ def set_custom_completer(self, completer, pos=0):
         list where you want the completer to be inserted."""
 
         newcomp = types.MethodType(completer,self.Completer)
-        self.Completer.matchers.insert(pos,newcomp)
+        self.Completer.custom_matchers.insert(pos,newcomp)
 
     def set_completer_frame(self, frame=None):
         """Set the frame of the completer."""


Test patch:
diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py
index 578ef27d3..496e3bd02 100644
--- a/IPython/core/tests/test_interactiveshell.py
+++ b/IPython/core/tests/test_interactiveshell.py
@@ -998,3 +998,21 @@ def test_should_run_async():
     assert not ip.should_run_async("a = 5")
     assert ip.should_run_async("await x")
     assert ip.should_run_async("import asyncio; await asyncio.sleep(1)")
+
+
+def test_set_custom_completer():
+    num_completers = len(ip.Completer.matchers)
+
+    def foo(*args, **kwargs):
+        return "I'm a completer!"
+
+    ip.set_custom_completer(foo, 0)
+
+    # check that we've really added a new completer
+    assert len(ip.Completer.matchers) == num_completers + 1
+
+    # check that the first completer is the function we defined
+    assert ip.Completer.matchers[0]() == "I'm a completer!"
+
+    # clean up
+    ip.Completer.custom_matchers.pop()