From 51ffb81c2c7b49dfaf1e126cfb5836d4e5042b44 Mon Sep 17 00:00:00 2001 From: Pete Anderson Date: Tue, 14 Jul 2015 04:43:37 -0400 Subject: [PATCH 0001/1027] Keep autocomplete errors from crashing bpython Perhaps a popup of some sort informing the user that an error has occurred would be better than just swallowing the error as I've done here, but I feel like a misbehaving completer should crash the application. The completer that prompted this for me is FilenameCompletion. I've got a test file in my directory created with `touch $'with\xFFhigh ascii'. If I type an open quote and a w in bpython, it crashes. It's because From python, if I do: >>> import glob >>> glob.glob(u'w*') # this is what FileCompletion will end up calling [u'without high ascii', u'with\uf0ffhigh ascii'] >>> But if I do it from bpython: >>> import glob >>> glob.glob(u'w*'0 [u'without high ascii', 'with\xffhigh ascii'] >>> For some reason, glob is returning one unicode and one str. Then when get_completer calls sorted(matches), sorted throws up when it tries to decode the str from ASCII. I don't know why glob is behaving this way or what the fix is, but I do know that it's not worth crashing bpython whenever I type 'w --- bpython/autocomplete.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 754b3a394..5ee4f2fa8 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -544,10 +544,14 @@ def get_completer(completers, cursor_offset, line, **kwargs): double underscore methods like __len__ in method signatures """ - for completer in completers: - matches = completer.matches(cursor_offset, line, **kwargs) - if matches is not None: - return sorted(matches), (completer if matches else None) + try: + for completer in completers: + matches = completer.matches(cursor_offset, line, **kwargs) + if matches is not None: + return sorted(matches), (completer if matches else None) + except: + pass + return [], None From 668bf06f8b7ec8c239e43f359591257d7993e6c9 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 25 Nov 2015 23:58:21 -0500 Subject: [PATCH 0002/1027] Show __new__ docstrings. Fixes #572 --- bpython/inspection.py | 4 +++- bpython/repl.py | 20 +++++++++++++++----- bpython/test/test_repl.py | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 0dfb4feff..c4f05c086 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -231,7 +231,9 @@ def getfuncprops(func, f): try: is_bound_method = ((inspect.ismethod(f) and f.__self__ is not None) or (func_name == '__init__' and not - func.endswith('.__init__'))) + func.endswith('.__init__')) + or (func_name == '__new__' and not + func.endswith('.__new__'))) except: # if f is a method from a xmlrpclib.Server instance, func_name == # '__init__' throws xmlrpclib.Fault (see #202) diff --git a/bpython/repl.py b/bpython/repl.py index 433d2732a..dc13b2f6e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -527,11 +527,21 @@ def get_args(self): return False if inspect.isclass(f): - try: - if f.__init__ is not object.__init__: - f = f.__init__ - except AttributeError: - return None + class_f = None + + if (hasattr(f, '__init__') and + f.__init__ is not object.__init__): + class_f = f.__init__ + if ((not class_f or + not inspection.getfuncprops(func, class_f)) and + hasattr(f, '__new__') and + f.__new__ is not object.__new__ and + f.__new__.__class__ is not object.__new__.__class__): # py3 + class_f = f.__new__ + + if class_f: + f = class_f + self.current_func = f self.funcprops = inspection.getfuncprops(func, f) if self.funcprops: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 2bb13f4b8..08ed7564c 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -138,6 +138,14 @@ def setUp(self): self.repl.push(" def spam(self, a, b, c):\n", False) self.repl.push(" pass\n", False) self.repl.push("\n", False) + self.repl.push("class SpammitySpam(object):\n", False) + self.repl.push(" def __init__(self, a, b, c):\n", False) + self.repl.push(" pass\n", False) + self.repl.push("\n", False) + self.repl.push("class WonderfulSpam(object):\n", False) + self.repl.push(" def __new__(self, a, b, c):\n", False) + self.repl.push(" pass\n", False) + self.repl.push("\n", False) self.repl.push("o = Spam()\n", False) self.repl.push("\n", False) @@ -207,6 +215,13 @@ def test_nonexistent_name(self): self.set_input_line("spamspamspam(") self.assertFalse(self.repl.get_args()) + def test_issue572(self): + self.set_input_line("SpammitySpam(") + self.assertTrue(self.repl.get_args()) + + self.set_input_line("WonderfulSpam(") + self.assertTrue(self.repl.get_args()) + class TestGetSource(unittest.TestCase): def setUp(self): From f16a248b5e933c22e03401d4f2ef11ab3dc0a3ec Mon Sep 17 00:00:00 2001 From: Shawn Axsom Date: Fri, 27 Nov 2015 02:08:28 +0000 Subject: [PATCH 0003/1027] Array item completion is working with strings. Pressing tab works for autocompletion of array items now --- bpython/autocomplete.py | 39 +++++++++++++++++++++++++++++++++++++++ bpython/line.py | 24 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 9bb86d895..1cca269f4 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -335,6 +335,44 @@ def list_attributes(self, obj): return dir(obj) +class ArrayObjectMembersCompletion(BaseCompletionType): + + def __init__(self, shown_before_tab=True, mode=SIMPLE): + self._shown_before_tab = shown_before_tab + self.completer = AttrCompletion(mode=mode) + + def matches(self, cursor_offset, line, **kwargs): + if 'locals_' not in kwargs: + return None + locals_ = kwargs['locals_'] + + r = self.locate(cursor_offset, line) + if r is None: + return None + member_part = r[2] + _, _, dexpr = lineparts.current_array_with_indexer(cursor_offset, line) + try: + locals_['temp_val_from_array'] = safe_eval(dexpr, locals_) + except (EvaluationError, IndexError): + return set() + + temp_line = line.replace(member_part, 'temp_val_from_array.') + + matches = self.completer.matches(len(temp_line), temp_line, **kwargs) + matches_with_correct_name = \ + set(match.replace('temp_val_from_array.', member_part) for match in matches) + + del locals_['temp_val_from_array'] + + return matches_with_correct_name + + def locate(self, current_offset, line): + return lineparts.current_array_item_member_name(current_offset, line) + + def format(self, match): + return after_last_dot(match) + + class DictKeyCompletion(BaseCompletionType): def matches(self, cursor_offset, line, **kwargs): @@ -565,6 +603,7 @@ def get_default_completer(mode=SIMPLE): MagicMethodCompletion(mode=mode), MultilineJediCompletion(mode=mode), GlobalCompletion(mode=mode), + ArrayObjectMembersCompletion(mode=mode), CumulativeCompleter((AttrCompletion(mode=mode), ParameterNameCompletion(mode=mode)), mode=mode) diff --git a/bpython/line.py b/bpython/line.py index c1da3943b..52c7b2355 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -226,3 +226,27 @@ def current_string_literal_attr(cursor_offset, line): if m.start(4) <= cursor_offset and m.end(4) >= cursor_offset: return LinePart(m.start(4), m.end(4), m.group(4)) return None + + +current_array_with_indexer_re = LazyReCompile( + r'''([\w_][\w0-9._]*\[[a-zA-Z0-9_"']+\])\.(.*)''') + + +def current_array_with_indexer(cursor_offset, line): + """an array and indexer, e.g. foo[1]""" + matches = current_array_with_indexer_re.finditer(line) + for m in matches: + if m.start(1) <= cursor_offset and m.end(1) <= cursor_offset: + return LinePart(m.start(1), m.end(1), m.group(1)) + + +current_array_item_member_name_re = LazyReCompile( + r'''([\w_][\w0-9._]*\[[a-zA-Z0-9_"']+\]\.)(.*)''') + + +def current_array_item_member_name(cursor_offset, line): + """the member name after an array indexer, e.g. foo[1].bar""" + matches = current_array_item_member_name_re.finditer(line) + for m in matches: + if m.start(2) <= cursor_offset and m.end(2) >= cursor_offset: + return LinePart(m.start(1), m.end(2), m.group(1)) From b2867705927b7dbcd109f9382b5280e8da9a76fa Mon Sep 17 00:00:00 2001 From: Shawn Axsom Date: Fri, 27 Nov 2015 18:04:52 +0000 Subject: [PATCH 0004/1027] Able to handle nested arrays now when autocompleting for array items --- bpython/line.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 52c7b2355..15495d988 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -229,7 +229,7 @@ def current_string_literal_attr(cursor_offset, line): current_array_with_indexer_re = LazyReCompile( - r'''([\w_][\w0-9._]*\[[a-zA-Z0-9_"']+\])\.(.*)''') + r'''([\w_][\w0-9._]*(?:\[[a-zA-Z0-9_"']+\])+)\.(.*)''') def current_array_with_indexer(cursor_offset, line): @@ -241,7 +241,7 @@ def current_array_with_indexer(cursor_offset, line): current_array_item_member_name_re = LazyReCompile( - r'''([\w_][\w0-9._]*\[[a-zA-Z0-9_"']+\]\.)(.*)''') + r'''([\w_][\w0-9._]*(?:\[[a-zA-Z0-9_"']+\])+\.)(.*)''') def current_array_item_member_name(cursor_offset, line): From ce1eb1aa2eb29c75a7e6a3a21c27e04dba4cc8b9 Mon Sep 17 00:00:00 2001 From: Shawn Axsom Date: Fri, 27 Nov 2015 18:59:28 +0000 Subject: [PATCH 0005/1027] Add tests --- bpython/autocomplete.py | 4 ++-- bpython/test/test_autocomplete.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 1cca269f4..58711955a 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -335,7 +335,7 @@ def list_attributes(self, obj): return dir(obj) -class ArrayObjectMembersCompletion(BaseCompletionType): +class ArrayItemMembersCompletion(BaseCompletionType): def __init__(self, shown_before_tab=True, mode=SIMPLE): self._shown_before_tab = shown_before_tab @@ -603,7 +603,7 @@ def get_default_completer(mode=SIMPLE): MagicMethodCompletion(mode=mode), MultilineJediCompletion(mode=mode), GlobalCompletion(mode=mode), - ArrayObjectMembersCompletion(mode=mode), + ArrayItemMembersCompletion(mode=mode), CumulativeCompleter((AttrCompletion(mode=mode), ParameterNameCompletion(mode=mode)), mode=mode) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 24018f3d2..d0e75f878 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -263,6 +263,22 @@ def __getattr__(self, attr): self.com.matches(4, 'a.__', locals_=locals_)) +class TestArrayItemCompletion(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.com = autocomplete.ArrayItemMembersCompletion() + + def test_att_matches_found_on_instance(self): + self.assertSetEqual(self.com.matches(5, 'a[0].', locals_={'a': [Foo()]}), + set(['a[0].method', 'a[0].a', 'a[0].b'])) + + @skip_old_style + def test_att_matches_found_on_old_style_instance(self): + self.assertSetEqual(self.com.matches(5, 'a[0].', + locals_={'a': [OldStyleFoo()]}), + set(['a[0].method', 'a[0].a', 'a[0].b'])) + + class TestMagicMethodCompletion(unittest.TestCase): def test_magic_methods_complete_after_double_underscores(self): From 58f3edfb26875450bbd71b909cb003f6b942bdab Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 28 Nov 2015 15:21:22 -0500 Subject: [PATCH 0006/1027] Allow global completion in brackets Make DictKeyCompletion return None when no matches found so other completers can take over. Perhaps ideal would be comulative, but this seems like good behavior -- it's clear to the user in most cases that what's now being completed are keys, then other completion. --- bpython/autocomplete.py | 9 +++++---- bpython/test/test_autocomplete.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 58711955a..1370a7fa4 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -387,12 +387,13 @@ def matches(self, cursor_offset, line, **kwargs): try: obj = safe_eval(dexpr, locals_) except EvaluationError: - return set() + return None if isinstance(obj, dict) and obj.keys(): - return set("{0!r}]".format(k) for k in obj.keys() - if repr(k).startswith(r.word)) + matches = set("{0!r}]".format(k) for k in obj.keys() + if repr(k).startswith(r.word)) + return matches if matches else None else: - return set() + return None def locate(self, current_offset, line): return lineparts.current_dict_key(current_offset, line) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index d0e75f878..a8ec672de 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -190,20 +190,25 @@ def test_set_of_keys_returned_when_matches_found(self): self.assertSetEqual(com.matches(2, "d[", locals_=local), set(["'ab']", "'cd']"])) - def test_empty_set_returned_when_eval_error(self): + def test_none_returned_when_eval_error(self): com = autocomplete.DictKeyCompletion() local = {'e': {"ab": 1, "cd": 2}} - self.assertSetEqual(com.matches(2, "d[", locals_=local), set()) + self.assertEqual(com.matches(2, "d[", locals_=local), None) - def test_empty_set_returned_when_not_dict_type(self): + def test_none_returned_when_not_dict_type(self): com = autocomplete.DictKeyCompletion() local = {'l': ["ab", "cd"]} - self.assertSetEqual(com.matches(2, "l[", locals_=local), set()) + self.assertEqual(com.matches(2, "l[", locals_=local), None) + + def test_none_returned_when_no_matches_left(self): + com = autocomplete.DictKeyCompletion() + local = {'d': {"ab": 1, "cd": 2}} + self.assertEqual(com.matches(3, "d[r", locals_=local), None) def test_obj_that_does_not_allow_conversion_to_bool(self): com = autocomplete.DictKeyCompletion() local = {'mNumPy': MockNumPy()} - self.assertSetEqual(com.matches(7, "mNumPy[", locals_=local), set()) + self.assertEqual(com.matches(7, "mNumPy[", locals_=local), None) class Foo(object): From 05c83beb32b1c754e127fe40f8e09c7d2d3abf4d Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 28 Nov 2015 18:39:12 -0500 Subject: [PATCH 0007/1027] Avoid unsafe completion on array elements This commit removes functionality it would be nice to reimplement but wasn't safe: * completion on nested arrays ( list[1][2].a ) * completion on subclasses of list, tuple etc. --- bpython/autocomplete.py | 43 +++++++++++++++++++++------- bpython/line.py | 39 ++++++++++++++++--------- bpython/test/test_autocomplete.py | 26 +++++++++++++++++ bpython/test/test_line_properties.py | 40 ++++++++++++++++++++++++-- 4 files changed, 122 insertions(+), 26 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 1370a7fa4..e1c34090d 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -118,8 +118,10 @@ def matches(self, cursor_offset, line, **kwargs): raise NotImplementedError def locate(self, cursor_offset, line): - """Returns a start, stop, and word given a line and cursor, or None - if no target for this type of completion is found under the cursor""" + """Returns a Linepart namedtuple instance or None given cursor and line + + A Linepart namedtuple contains a start, stop, and word. None is returned + if no target for this type of completion is found under the cursor.""" raise NotImplementedError def format(self, word): @@ -346,28 +348,47 @@ def matches(self, cursor_offset, line, **kwargs): return None locals_ = kwargs['locals_'] - r = self.locate(cursor_offset, line) - if r is None: + full = self.locate(cursor_offset, line) + if full is None: return None - member_part = r[2] - _, _, dexpr = lineparts.current_array_with_indexer(cursor_offset, line) + + arr = lineparts.current_indexed_member_access_identifier( + cursor_offset, line) + index = lineparts.current_indexed_member_access_identifier_with_index( + cursor_offset, line) + member = lineparts.current_indexed_member_access_member( + cursor_offset, line) + try: - locals_['temp_val_from_array'] = safe_eval(dexpr, locals_) + obj = safe_eval(arr.word, locals_) + except EvaluationError: + return None + if type(obj) not in (list, tuple) + string_types: + # then is may be unsafe to do attribute lookup on it + return None + + try: + locals_['temp_val_from_array'] = safe_eval(index.word, locals_) except (EvaluationError, IndexError): - return set() + return None - temp_line = line.replace(member_part, 'temp_val_from_array.') + temp_line = line.replace(index.word, 'temp_val_from_array.') matches = self.completer.matches(len(temp_line), temp_line, **kwargs) + if matches is None: + return None + matches_with_correct_name = \ - set(match.replace('temp_val_from_array.', member_part) for match in matches) + set(match.replace('temp_val_from_array.', index.word+'.') + for match in matches if match[20:].startswith(member.word)) del locals_['temp_val_from_array'] return matches_with_correct_name def locate(self, current_offset, line): - return lineparts.current_array_item_member_name(current_offset, line) + a = lineparts.current_indexed_member_access(current_offset, line) + return a def format(self, match): return after_last_dot(match) diff --git a/bpython/line.py b/bpython/line.py index 15495d988..17599a5e1 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -228,25 +228,38 @@ def current_string_literal_attr(cursor_offset, line): return None -current_array_with_indexer_re = LazyReCompile( - r'''([\w_][\w0-9._]*(?:\[[a-zA-Z0-9_"']+\])+)\.(.*)''') +current_indexed_member_re = LazyReCompile( + r'''([a-zA-Z_][\w.]*)\[([a-zA-Z0-9_"']+)\]\.([\w.]*)''') -def current_array_with_indexer(cursor_offset, line): - """an array and indexer, e.g. foo[1]""" - matches = current_array_with_indexer_re.finditer(line) +def current_indexed_member_access(cursor_offset, line): + """An identifier being indexed and member accessed""" + matches = current_indexed_member_re.finditer(line) for m in matches: - if m.start(1) <= cursor_offset and m.end(1) <= cursor_offset: + if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset: + return LinePart(m.start(1), m.end(3), m.group()) + + +def current_indexed_member_access_identifier(cursor_offset, line): + """An identifier being indexed, e.g. foo in foo[1].bar""" + matches = current_indexed_member_re.finditer(line) + for m in matches: + if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset: return LinePart(m.start(1), m.end(1), m.group(1)) -current_array_item_member_name_re = LazyReCompile( - r'''([\w_][\w0-9._]*(?:\[[a-zA-Z0-9_"']+\])+\.)(.*)''') +def current_indexed_member_access_identifier_with_index(cursor_offset, line): + """An identifier being indexed with the index, e.g. foo[1] in foo[1].bar""" + matches = current_indexed_member_re.finditer(line) + for m in matches: + if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset: + return LinePart(m.start(1), m.end(2)+1, + "%s[%s]" % (m.group(1), m.group(2))) -def current_array_item_member_name(cursor_offset, line): - """the member name after an array indexer, e.g. foo[1].bar""" - matches = current_array_item_member_name_re.finditer(line) +def current_indexed_member_access_member(cursor_offset, line): + """The member name of an indexed object, e.g. bar in foo[1].bar""" + matches = current_indexed_member_re.finditer(line) for m in matches: - if m.start(2) <= cursor_offset and m.end(2) >= cursor_offset: - return LinePart(m.start(1), m.end(2), m.group(1)) + if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset: + return LinePart(m.start(3), m.end(3), m.group(3)) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index a8ec672de..37a52002d 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -283,6 +283,32 @@ def test_att_matches_found_on_old_style_instance(self): locals_={'a': [OldStyleFoo()]}), set(['a[0].method', 'a[0].a', 'a[0].b'])) + def test_other_getitem_methods_not_called(self): + class FakeList(object): + def __getitem__(inner_self, i): + self.fail("possibly side-effecting __getitem_ method called") + + self.com.matches(5, 'a[0].', locals_={'a': FakeList()}) + + def test_tuples_complete(self): + self.assertSetEqual(self.com.matches(5, 'a[0].', + locals_={'a': (Foo(),)}), + set(['a[0].method', 'a[0].a', 'a[0].b'])) + + @unittest.skip('TODO, subclasses do not complete yet') + def test_list_subclasses_complete(self): + class ListSubclass(list): pass + self.assertSetEqual(self.com.matches(5, 'a[0].', + locals_={'a': ListSubclass([Foo()])}), + set(['a[0].method', 'a[0].a', 'a[0].b'])) + + def test_getitem_not_called_in_list_subclasses_overriding_getitem(self): + class FakeList(list): + def __getitem__(inner_self, i): + self.fail("possibly side-effecting __getitem_ method called") + + self.com.matches(5, 'a[0].', locals_={'a': FakeList()}) + class TestMagicMethodCompletion(unittest.TestCase): diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 26eee9a07..7ea49f5d8 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -5,7 +5,9 @@ current_string, current_object, current_object_attribute, \ current_from_import_from, current_from_import_import, current_import, \ current_method_definition_name, current_single_word, \ - current_string_literal_attr + current_string_literal_attr, current_indexed_member_access_identifier, \ + current_indexed_member_access_identifier_with_index, \ + current_indexed_member_access_member def cursor(s): @@ -20,7 +22,7 @@ def decode(s): if not s.count('|') == 1: raise ValueError('match helper needs | to occur once') - if s.count('<') != s.count('>') or not s.count('<') in (0, 1): + if s.count('<') != s.count('>') or s.count('<') not in (0, 1): raise ValueError('match helper needs <, and > to occur just once') matches = list(re.finditer(r'[<>|]', s)) assert len(matches) in [1, 3], [m.group() for m in matches] @@ -305,6 +307,40 @@ def test_simple(self): self.assertAccess('"hey".asdf d|') self.assertAccess('"hey".<|>') +class TestCurrentIndexedMemberAccessIdentifier(LineTestCase): + def setUp(self): + self.func = current_indexed_member_access_identifier + + def test_simple(self): + self.assertAccess('[def].ghi|') + self.assertAccess('[def].|ghi') + self.assertAccess('[def].gh|i') + self.assertAccess('abc[def].gh |i') + self.assertAccess('abc[def]|') + + +class TestCurrentIndexedMemberAccessIdentifierWithIndex(LineTestCase): + def setUp(self): + self.func = current_indexed_member_access_identifier_with_index + + def test_simple(self): + self.assertAccess('.ghi|') + self.assertAccess('.|ghi') + self.assertAccess('.gh|i') + self.assertAccess('abc[def].gh |i') + self.assertAccess('abc[def]|') + + +class TestCurrentIndexedMemberAccessMember(LineTestCase): + def setUp(self): + self.func = current_indexed_member_access_member + + def test_simple(self): + self.assertAccess('abc[def].') + self.assertAccess('abc[def].<|ghi>') + self.assertAccess('abc[def].') + self.assertAccess('abc[def].gh |i') + self.assertAccess('abc[def]|') if __name__ == '__main__': unittest.main() From dc5e87fd91f3e4377f7623c2b7ed72e4b7cecc53 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 29 Nov 2015 23:29:53 +0100 Subject: [PATCH 0008/1027] Log exceptions from auto completers Also do not catch KeyboardInterrupt and SystemExit. Signed-off-by: Sebastian Ramacher --- bpython/autocomplete.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 762a946c0..91818d4a5 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -29,6 +29,7 @@ import abc import glob import keyword +import logging import os import re import rlcompleter @@ -609,13 +610,18 @@ def get_completer(completers, cursor_offset, line, **kwargs): double underscore methods like __len__ in method signatures """ - try: - for completer in completers: + for completer in completers: + try: matches = completer.matches(cursor_offset, line, **kwargs) - if matches is not None: - return sorted(matches), (completer if matches else None) - except: - pass + except Exception as e: + # Instead of crashing the UI, log exceptions from autocompleters. + logger = logging.getLogger(__name__) + logger.debug( + 'Completer {} failed with unhandled exception: {}'.format( + completer, e)) + continue + if matches is not None: + return sorted(matches), (completer if matches else None) return [], None From 404e5c718ebc3e58a655f65404aef36651da7f26 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 29 Nov 2015 23:39:15 +0100 Subject: [PATCH 0009/1027] No longer install Twisted for 2.6 Signed-off-by: Sebastian Ramacher --- .travis.install.sh | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.travis.install.sh b/.travis.install.sh index dadbd2373..f1fd0c7f8 100755 --- a/.travis.install.sh +++ b/.travis.install.sh @@ -20,15 +20,12 @@ if [[ $RUN == nosetests ]]; then # Python 2.6 specific dependencies if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install unittest2 + elif [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then + # dependencies for crasher tests + pip install Twisted urwid fi case $TRAVIS_PYTHON_VERSION in - 2*) - # dependencies for crasher tests - pip install Twisted urwid - # test specific dependencies - pip install mock - ;; - pypy) + 2*|pypy) # test specific dependencies pip install mock ;; From 599cfea433f60f750a3c019add211353a7bba4bc Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 29 Nov 2015 14:51:44 -0500 Subject: [PATCH 0010/1027] fix parameter name completion --- bpython/autocomplete.py | 14 +++++++------- bpython/test/test_autocomplete.py | 4 ++-- bpython/test/test_repl.py | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 91818d4a5..070aa981b 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -160,17 +160,17 @@ def format(self, word): return self._completers[0].format(word) def matches(self, cursor_offset, line, **kwargs): + return_value = None all_matches = set() for completer in self._completers: - # these have to be explicitely listed to deal with the different - # signatures of various matches() methods of completers matches = completer.matches(cursor_offset=cursor_offset, line=line, **kwargs) if matches is not None: all_matches.update(matches) + return_value = all_matches - return all_matches + return return_value class ImportCompletion(BaseCompletionType): @@ -634,11 +634,11 @@ def get_default_completer(mode=SIMPLE): FilenameCompletion(mode=mode), MagicMethodCompletion(mode=mode), MultilineJediCompletion(mode=mode), - GlobalCompletion(mode=mode), - ArrayItemMembersCompletion(mode=mode), - CumulativeCompleter((AttrCompletion(mode=mode), + CumulativeCompleter((GlobalCompletion(mode=mode), ParameterNameCompletion(mode=mode)), - mode=mode) + mode=mode), + ArrayItemMembersCompletion(mode=mode), + AttrCompletion(mode=mode), ) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 37a52002d..76c519511 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -107,10 +107,10 @@ def test_one_empty_completer_returns_empty(self): cumulative = autocomplete.CumulativeCompleter([a]) self.assertEqual(cumulative.matches(3, 'abc'), set()) - def test_one_none_completer_returns_empty(self): + def test_one_none_completer_returns_none(self): a = self.completer(None) cumulative = autocomplete.CumulativeCompleter([a]) - self.assertEqual(cumulative.matches(3, 'abc'), set()) + self.assertEqual(cumulative.matches(3, 'abc'), None) def test_two_completers_get_both(self): a = self.completer(['a']) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 08ed7564c..acd3a6889 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -383,7 +383,7 @@ def test_fuzzy_attribute_complete(self): self.assertTrue(hasattr(self.repl.matches_iter, 'matches')) self.assertEqual(self.repl.matches_iter.matches, ['Foo.bar']) - # 3. Edge Cases + # 3. Edge cases def test_updating_namespace_complete(self): self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE}) self.set_input_line("foo") @@ -400,6 +400,19 @@ def test_file_should_not_appear_in_complete(self): self.assertTrue(hasattr(self.repl.matches_iter, 'matches')) self.assertNotIn('__file__', self.repl.matches_iter.matches) + # 4. Parameter names + def test_paremeter_name_completion(self): + self.repl = FakeRepl({'autocomplete_mode': autocomplete.SIMPLE}) + self.set_input_line("foo(ab") + + code = "def foo(abc=1, abd=2, xyz=3):\n\tpass\n" + for line in code.split("\n"): + self.repl.push(line) + + self.assertTrue(self.repl.complete()) + self.assertTrue(hasattr(self.repl.matches_iter, 'matches')) + self.assertEqual(self.repl.matches_iter.matches, ['abc=', 'abd=', 'abs(']) + class TestCliRepl(unittest.TestCase): From 575cafaab9bf7f0339dd1541f6c6ce8b8885d9dd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 1 Dec 2015 10:45:46 +0100 Subject: [PATCH 0011/1027] Update appdata to latest spec Signed-off-by: Sebastian Ramacher --- data/bpython.appdata.xml | 43 ++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/data/bpython.appdata.xml b/data/bpython.appdata.xml index b24c09368..e3d5d32c9 100644 --- a/data/bpython.appdata.xml +++ b/data/bpython.appdata.xml @@ -1,8 +1,8 @@ - + - - bpython.desktop + + bpython.desktop CC0-1.0 MIT bpython interpreter @@ -23,15 +23,32 @@