From 80a9e0ed5425849877c874ea90f7febeeb095c8f Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:44:18 +0300 Subject: [PATCH] Update rlcompleter from 3.13.5 (#5990) --- Lib/rlcompleter.py | 46 ++++++++++++++++-------- Lib/test/test_rlcompleter.py | 70 ++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 26 deletions(-) diff --git a/Lib/rlcompleter.py b/Lib/rlcompleter.py index bca4a7bc5..23eb0020f 100644 --- a/Lib/rlcompleter.py +++ b/Lib/rlcompleter.py @@ -31,7 +31,11 @@ Notes: import atexit import builtins +import inspect +import keyword +import re import __main__ +import warnings __all__ = ["Completer"] @@ -85,10 +89,11 @@ class Completer: return None if state == 0: - if "." in text: - self.matches = self.attr_matches(text) - else: - self.matches = self.global_matches(text) + with warnings.catch_warnings(action="ignore"): + if "." in text: + self.matches = self.attr_matches(text) + else: + self.matches = self.global_matches(text) try: return self.matches[state] except IndexError: @@ -96,7 +101,13 @@ class Completer: def _callable_postfix(self, val, word): if callable(val): - word = word + "(" + word += "(" + try: + if not inspect.signature(val).parameters: + word += ")" + except ValueError: + pass + return word def global_matches(self, text): @@ -106,18 +117,17 @@ class Completer: defined in self.namespace that match. """ - import keyword matches = [] seen = {"__builtins__"} n = len(text) - for word in keyword.kwlist: + for word in keyword.kwlist + keyword.softkwlist: if word[:n] == text: seen.add(word) if word in {'finally', 'try'}: word = word + ':' elif word not in {'False', 'None', 'True', 'break', 'continue', 'pass', - 'else'}: + 'else', '_'}: word = word + ' ' matches.append(word) for nspace in [self.namespace, builtins.__dict__]: @@ -139,7 +149,6 @@ class Completer: with a __getattr__ hook is evaluated. """ - import re m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text) if not m: return [] @@ -169,13 +178,20 @@ class Completer: if (word[:n] == attr and not (noprefix and word[:n+1] == noprefix)): match = "%s.%s" % (expr, word) - try: - val = getattr(thisobject, word) - except Exception: - pass # Include even if attribute not set + if isinstance(getattr(type(thisobject), word, None), + property): + # bpo-44752: thisobject.word is a method decorated by + # `@property`. What follows applies a postfix if + # thisobject.word is callable, but know we know that + # this is not callable (because it is a property). + # Also, getattr(thisobject, word) will evaluate the + # property method, which is not desirable. + matches.append(match) + continue + if (value := getattr(thisobject, word, None)) is not None: + matches.append(self._callable_postfix(value, match)) else: - match = self._callable_postfix(val, match) - matches.append(match) + matches.append(match) if matches or not noprefix: break if noprefix == '_': diff --git a/Lib/test/test_rlcompleter.py b/Lib/test/test_rlcompleter.py index 0dc1080ca..ffadfee27 100644 --- a/Lib/test/test_rlcompleter.py +++ b/Lib/test/test_rlcompleter.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch import builtins import rlcompleter +from test.support import MISSING_C_DOCSTRINGS class CompleteMe: """ Trivial class used in testing rlcompleter.Completer. """ @@ -40,21 +41,41 @@ class TestRlcompleter(unittest.TestCase): # test with a customized namespace self.assertEqual(self.completer.global_matches('CompleteM'), - ['CompleteMe(']) + ['CompleteMe(' if MISSING_C_DOCSTRINGS else 'CompleteMe()']) self.assertEqual(self.completer.global_matches('eg'), ['egg(']) # XXX: see issue5256 self.assertEqual(self.completer.global_matches('CompleteM'), - ['CompleteMe(']) + ['CompleteMe(' if MISSING_C_DOCSTRINGS else 'CompleteMe()']) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_attr_matches(self): # test with builtins namespace self.assertEqual(self.stdcompleter.attr_matches('str.s'), ['str.{}('.format(x) for x in dir(str) if x.startswith('s')]) self.assertEqual(self.stdcompleter.attr_matches('tuple.foospamegg'), []) - expected = sorted({'None.%s%s' % (x, '(' if x != '__doc__' else '') - for x in dir(None)}) + + def create_expected_for_none(): + if not MISSING_C_DOCSTRINGS: + parentheses = ('__init_subclass__', '__class__') + else: + # When `--without-doc-strings` is used, `__class__` + # won't have a known signature. + parentheses = ('__init_subclass__',) + + items = set() + for x in dir(None): + if x in parentheses: + items.add(f'None.{x}()') + elif x == '__doc__': + items.add(f'None.{x}') + else: + items.add(f'None.{x}(') + return sorted(items) + + expected = create_expected_for_none() self.assertEqual(self.stdcompleter.attr_matches('None.'), expected) self.assertEqual(self.stdcompleter.attr_matches('None._'), expected) self.assertEqual(self.stdcompleter.attr_matches('None.__'), expected) @@ -64,7 +85,7 @@ class TestRlcompleter(unittest.TestCase): ['CompleteMe.spam']) self.assertEqual(self.completer.attr_matches('Completeme.egg'), []) self.assertEqual(self.completer.attr_matches('CompleteMe.'), - ['CompleteMe.mro(', 'CompleteMe.spam']) + ['CompleteMe.mro()', 'CompleteMe.spam']) self.assertEqual(self.completer.attr_matches('CompleteMe._'), ['CompleteMe._ham']) matches = self.completer.attr_matches('CompleteMe.__') @@ -81,18 +102,42 @@ class TestRlcompleter(unittest.TestCase): if x.startswith('s')]) def test_excessive_getattr(self): - # Ensure getattr() is invoked no more than once per attribute + """Ensure getattr() is invoked no more than once per attribute""" + + # note the special case for @property methods below; that is why + # we use __dir__ and __getattr__ in class Foo to create a "magic" + # class attribute 'bar'. This forces `getattr` to call __getattr__ + # (which is doesn't necessarily do). class Foo: calls = 0 - @property - def bar(self): - self.calls += 1 - return None + bar = '' + def __getattribute__(self, name): + if name == 'bar': + self.calls += 1 + return None + return super().__getattribute__(name) + f = Foo() completer = rlcompleter.Completer(dict(f=f)) self.assertEqual(completer.complete('f.b', 0), 'f.bar') self.assertEqual(f.calls, 1) + def test_property_method_not_called(self): + class Foo: + _bar = 0 + property_called = False + + @property + def bar(self): + self.property_called = True + return self._bar + + f = Foo() + completer = rlcompleter.Completer(dict(f=f)) + self.assertEqual(completer.complete('f.b', 0), 'f.bar') + self.assertFalse(f.property_called) + + def test_uncreated_attr(self): # Attributes like properties and slots should be completed even when # they haven't been created on an instance @@ -114,6 +159,9 @@ class TestRlcompleter(unittest.TestCase): self.assertEqual(completer.complete('el', 0), 'elif ') self.assertEqual(completer.complete('el', 1), 'else') self.assertEqual(completer.complete('tr', 0), 'try:') + self.assertEqual(completer.complete('_', 0), '_') + self.assertEqual(completer.complete('match', 0), 'match ') + self.assertEqual(completer.complete('case', 0), 'case ') def test_duplicate_globals(self): namespace = { @@ -134,7 +182,7 @@ class TestRlcompleter(unittest.TestCase): # No opening bracket "(" because we overrode the built-in class self.assertEqual(completer.complete('memoryview', 0), 'memoryview') self.assertIsNone(completer.complete('memoryview', 1)) - self.assertEqual(completer.complete('Ellipsis', 0), 'Ellipsis(') + self.assertEqual(completer.complete('Ellipsis', 0), 'Ellipsis()') self.assertIsNone(completer.complete('Ellipsis', 1)) if __name__ == '__main__':