Repository: ipython/ipython

Issue:
TypeError on completions using version "8.6.0" 
I was using `IPCompleter.merge_completions = False` configuration, and after update to version `8.6.0` I got an error.

I created a fresh virtual environment:
```
poetry init
poetry add ipython
```
and still got the error:
```
Traceback (most recent call last):
  File "/tmp/foobar/.venv/lib/python3.10/site-packages/IPython/terminal/ptutils.py", line 122, in get_completions
    yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
  File "/tmp/foobar/.venv/lib/python3.10/site-packages/IPython/terminal/ptutils.py", line 138, in _get_completions
    for c in completions:
  File "/tmp/foobar/.venv/lib/python3.10/site-packages/IPython/core/completer.py", line 753, in _deduplicate_completions
    completions = list(completions)
  File "/tmp/foobar/.venv/lib/python3.10/site-packages/IPython/core/completer.py", line 2449, in completions
    for c in self._completions(text, offset, _timeout=self.jedi_compute_type_timeout/1000):
  File "/tmp/foobar/.venv/lib/python3.10/site-packages/IPython/core/completer.py", line 2499, in _completions
    results = self._complete(
  File "/tmp/foobar/.venv/lib/python3.10/site-packages/IPython/core/completer.py", line 2810, in _complete
    ) and len(result["completions"])
TypeError: object of type 'filter' has no len()
```

In the [documentation](https://ipython.readthedocs.io/en/stable/config/options/terminal.html) I read that as of version `8.6.0`, setting `IPCompleter.merge_completions` to False is an alias for: `IPCompleter.suppress_competing_matchers = True`. I used the new option and the error still happens.

After commented this line (to use the default value) the error stopped to happen.


Discussion:

Resolving patch:
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -671,6 +671,19 @@ def __call__(self, context: CompletionContext) -> MatcherResult:
 Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2]
 
 
+def has_any_completions(result: MatcherResult) -> bool:
+    """Check if any result includes any completions."""
+    if hasattr(result["completions"], "__len__"):
+        return len(result["completions"]) != 0
+    try:
+        old_iterator = result["completions"]
+        first = next(old_iterator)
+        result["completions"] = itertools.chain([first], old_iterator)
+        return True
+    except StopIteration:
+        return False
+
+
 def completion_matcher(
     *, priority: float = None, identifier: str = None, api_version: int = 1
 ):
@@ -1952,7 +1965,7 @@ def _jedi_matches(
             else:
                 return []
 
-    def python_matches(self, text:str)->List[str]:
+    def python_matches(self, text: str) -> Iterable[str]:
         """Match attributes or global python names"""
         if "." in text:
             try:
@@ -2807,7 +2820,7 @@ def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
                 should_suppress = (
                     (suppression_config is True)
                     or (suppression_recommended and (suppression_config is not False))
-                ) and len(result["completions"])
+                ) and has_any_completions(result)
 
                 if should_suppress:
                     suppression_exceptions = result.get("do_not_suppress", set())


Test patch:
diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py
--- a/IPython/core/tests/test_completer.py
+++ b/IPython/core/tests/test_completer.py
@@ -1396,6 +1396,65 @@ def configure(suppression_config):
             configure({"b_matcher": True})
             _("do not suppress", ["completion_b"])
 
+            configure(True)
+            _("do not suppress", ["completion_a"])
+
+    def test_matcher_suppression_with_iterator(self):
+        @completion_matcher(identifier="matcher_returning_iterator")
+        def matcher_returning_iterator(text):
+            return iter(["completion_iter"])
+
+        @completion_matcher(identifier="matcher_returning_list")
+        def matcher_returning_list(text):
+            return ["completion_list"]
+
+        with custom_matchers([matcher_returning_iterator, matcher_returning_list]):
+            ip = get_ipython()
+            c = ip.Completer
+
+            def _(text, expected):
+                c.use_jedi = False
+                s, matches = c.complete(text)
+                self.assertEqual(expected, matches)
+
+            def configure(suppression_config):
+                cfg = Config()
+                cfg.IPCompleter.suppress_competing_matchers = suppression_config
+                c.update_config(cfg)
+
+            configure(False)
+            _("---", ["completion_iter", "completion_list"])
+
+            configure(True)
+            _("---", ["completion_iter"])
+
+            configure(None)
+            _("--", ["completion_iter", "completion_list"])
+
+    def test_matcher_suppression_with_jedi(self):
+        ip = get_ipython()
+        c = ip.Completer
+        c.use_jedi = True
+
+        def configure(suppression_config):
+            cfg = Config()
+            cfg.IPCompleter.suppress_competing_matchers = suppression_config
+            c.update_config(cfg)
+
+        def _():
+            with provisionalcompleter():
+                matches = [completion.text for completion in c.completions("dict.", 5)]
+                self.assertIn("keys", matches)
+
+        configure(False)
+        _()
+
+        configure(True)
+        _()
+
+        configure(None)
+        _()
+
     def test_matcher_disabling(self):
         @completion_matcher(identifier="a_matcher")
         def a_matcher(text):