Skip to content

Commit 98e7214

Browse files
annotate attributes with parens
fix tests
1 parent 2ecdf34 commit 98e7214

File tree

3 files changed

+65
-4
lines changed

3 files changed

+65
-4
lines changed

bpython/autocomplete.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,6 @@ def matches(self, cursor_offset, line, **kwargs):
253253
matches = set(''.join([r.word[:-i], m])
254254
for m in self.attr_matches(methodtext, locals_))
255255

256-
# TODO add open paren for methods via _callable_prefix
257-
matches = set(_callable_postfix(eval(m, locals_), m)
258-
for m in matches)
259256
# unless the first character is a _ filter out all attributes
260257
# starting with a _
261258
if r.word.split('.')[-1].startswith('__'):
@@ -664,6 +661,57 @@ def safe_eval(expr, namespace):
664661
raise EvaluationError
665662

666663

664+
attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)")
665+
666+
667+
def attr_matches(text, namespace):
668+
"""Taken from rlcompleter.py and bent to my will.
669+
"""
670+
671+
# Gna, Py 2.6's rlcompleter searches for __call__ inside the
672+
# instance instead of the type, so we monkeypatch to prevent
673+
# side-effects (__getattr__/__getattribute__)
674+
m = attr_matches_re.match(text)
675+
if not m:
676+
return []
677+
678+
expr, attr = m.group(1, 3)
679+
if expr.isdigit():
680+
# Special case: float literal, using attrs here will result in
681+
# a SyntaxError
682+
return []
683+
try:
684+
obj = safe_eval(expr, namespace)
685+
except EvaluationError:
686+
return []
687+
with inspection.AttrCleaner(obj):
688+
matches = attr_lookup(obj, expr, attr)
689+
return matches
690+
691+
692+
def attr_lookup(obj, expr, attr):
693+
"""Second half of original attr_matches method factored out so it can
694+
be wrapped in a safe try/finally block in case anything bad happens to
695+
restore the original __getattribute__ method."""
696+
words = dir(obj)
697+
if hasattr(obj, '__class__'):
698+
words.append('__class__')
699+
words = words + rlcompleter.get_class_members(obj.__class__)
700+
if not isinstance(obj.__class__, abc.ABCMeta):
701+
try:
702+
words.remove('__abstractmethods__')
703+
except ValueError:
704+
pass
705+
706+
matches = []
707+
n = len(attr)
708+
for word in words:
709+
if method_match(word, n, attr) and word != "__builtins__":
710+
attr_obj = getattr(obj, word) # scary!
711+
matches.append(_callable_postfix(attr_obj, "%s.%s" % (expr, word)))
712+
return matches
713+
714+
667715
def _callable_postfix(value, word):
668716
"""rlcompleter's _callable_postfix done right."""
669717
with inspection.AttrCleaner(value):

bpython/test/test_autocomplete.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ def test_catches_syntax_error(self):
3232
with self.assertRaises(autocomplete.EvaluationError):
3333
autocomplete.safe_eval('1re', {})
3434

35+
def test_doesnt_eval_properties(self):
36+
self.assertRaises(autocomplete.EvaluationError,
37+
autocomplete.safe_eval, '1re', {})
38+
p = Properties()
39+
autocomplete.safe_eval('p.asserts_when_called', {'p': p})
40+
3541

3642
class TestFormatters(unittest.TestCase):
3743

@@ -235,6 +241,13 @@ def method(self, x):
235241
'In Python 3 there are no old style classes')
236242

237243

244+
class Properties(Foo):
245+
246+
@property
247+
def asserts_when_called(self):
248+
raise AssertionError("getter method called")
249+
250+
238251
class TestAttrCompletion(unittest.TestCase):
239252
@classmethod
240253
def setUpClass(cls):

bpython/test/test_repl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ def test_simple_attribute_complete(self):
364364

365365
self.assertTrue(self.repl.complete())
366366
self.assertTrue(hasattr(self.repl.matches_iter, 'matches'))
367-
self.assertEqual(self.repl.matches_iter.matches, ['Foo.bar'])
367+
self.assertEqual(self.repl.matches_iter.matches, ['Foo.bar('])
368368

369369
def test_substring_attribute_complete(self):
370370
self.repl = FakeRepl({'autocomplete_mode': autocomplete.SUBSTRING})

0 commit comments

Comments
 (0)