From 9fbb325b8cdd1fb3292cf10d60a4ac0fa71849f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 26 Apr 2015 16:48:20 +0200 Subject: [PATCH 0001/1034] Test Python 3.5 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fa6d1945b..ab0053adb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" - "pypy" - "pypy3" From 51ffb81c2c7b49dfaf1e126cfb5836d4e5042b44 Mon Sep 17 00:00:00 2001 From: Pete Anderson Date: Tue, 14 Jul 2015 04:43:37 -0400 Subject: [PATCH 0002/1034] 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 a17999d57f5b8e9872dde39894c6764a839bd275 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 18 Aug 2015 21:03:07 +0200 Subject: [PATCH 0003/1034] Fix a typo Signed-off-by: Sebastian Ramacher --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 14aec69f2..0dfb4feff 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -214,7 +214,7 @@ def getpydocspec(f, func): if default: defaults.append(default) - return ArgSpec(args, varargs, varkwargs, default, kwonly_args, + return ArgSpec(args, varargs, varkwargs, defaults, kwonly_args, kwonly_defaults, None) From c08b42d6dcc31f136e556d9ca2c7549f638ea1bd Mon Sep 17 00:00:00 2001 From: Weston Vial Date: Wed, 26 Aug 2015 15:39:12 -0400 Subject: [PATCH 0004/1034] Fix bug #548 - Transpose when empty line crashes --- bpython/curtsiesfrontend/manual_readline.py | 6 ++++-- bpython/test/test_manual_readline.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index 02c7cbf03..24a1b9a27 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -280,11 +280,13 @@ def yank_prev_killed_text(cursor_offset, line, cut_buffer): @edit_keys.on(config='transpose_chars_key') def transpose_character_before_cursor(cursor_offset, line): + if cursor_offset == 0: + return cursor_offset, line return (min(len(line), cursor_offset + 1), - line[:cursor_offset-1] + + line[:cursor_offset - 1] + (line[cursor_offset] if len(line) > cursor_offset else '') + line[cursor_offset - 1] + - line[cursor_offset+1:]) + line[cursor_offset + 1:]) @edit_keys.on('') diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py index 6ef610638..f1e24b780 100644 --- a/bpython/test/test_manual_readline.py +++ b/bpython/test/test_manual_readline.py @@ -201,6 +201,16 @@ def test_transpose_character_before_cursor(self): "adf s|asdf", "adf as|sdf"], transpose_character_before_cursor) + def test_transpose_empty_line(self): + self.assertEquals(transpose_character_before_cursor(0, ''), + (0,'')) + + def test_transpose_first_character(self): + self.assertEquals(transpose_character_before_cursor(0, 'a'), + transpose_character_before_cursor(0, 'a')) + self.assertEquals(transpose_character_before_cursor(0, 'as'), + transpose_character_before_cursor(0, 'as')) + def test_transpose_word_before_cursor(self): pass From 3abb483b4a109cbfe61d8ed6ca7e5eba3f9fa38f Mon Sep 17 00:00:00 2001 From: Weston Vial Date: Wed, 26 Aug 2015 16:14:14 -0400 Subject: [PATCH 0005/1034] Transpose characters if cursor is at the end of the line. Mimics emacs behavior. --- bpython/curtsiesfrontend/manual_readline.py | 4 +++- bpython/test/test_manual_readline.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index 24a1b9a27..a919df14d 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -280,8 +280,10 @@ def yank_prev_killed_text(cursor_offset, line, cut_buffer): @edit_keys.on(config='transpose_chars_key') def transpose_character_before_cursor(cursor_offset, line): - if cursor_offset == 0: + if cursor_offset < 2: return cursor_offset, line + if cursor_offset == len(line): + return cursor_offset, line[:-2] + line[-1] + line[-2] return (min(len(line), cursor_offset + 1), line[:cursor_offset - 1] + (line[cursor_offset] if len(line) > cursor_offset else '') + diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py index f1e24b780..3c25e3bb5 100644 --- a/bpython/test/test_manual_readline.py +++ b/bpython/test/test_manual_readline.py @@ -207,9 +207,15 @@ def test_transpose_empty_line(self): def test_transpose_first_character(self): self.assertEquals(transpose_character_before_cursor(0, 'a'), - transpose_character_before_cursor(0, 'a')) + (0, 'a')) self.assertEquals(transpose_character_before_cursor(0, 'as'), - transpose_character_before_cursor(0, 'as')) + (0, 'as')) + + def test_transpose_end_of_line(self): + self.assertEquals(transpose_character_before_cursor(1, 'a'), + (1, 'a')) + self.assertEquals(transpose_character_before_cursor(2, 'as'), + (2, 'sa')) def test_transpose_word_before_cursor(self): pass From 563f5cb8a9dfeada479d479433b9a5e78f8fea23 Mon Sep 17 00:00:00 2001 From: Jeppe Toustrup Date: Thu, 1 Oct 2015 22:53:38 +0200 Subject: [PATCH 0006/1034] Filter out two underscore attributes in auto completion This change will require you to write two underscores in order to get autocompletion of attributes starting with two underscores, as requested in #528. Fixes #528 --- bpython/autocomplete.py | 7 ++++++- bpython/test/test_autocomplete.py | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 754b3a394..9bb86d895 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -253,7 +253,12 @@ def matches(self, cursor_offset, line, **kwargs): # TODO add open paren for methods via _callable_prefix (or decide not # to) unless the first character is a _ filter out all attributes # starting with a _ - if not r.word.split('.')[-1].startswith('_'): + if r.word.split('.')[-1].startswith('__'): + pass + elif r.word.split('.')[-1].startswith('_'): + matches = set(match for match in matches + if not match.split('.')[-1].startswith('__')) + else: matches = set(match for match in matches if not match.split('.')[-1].startswith('_')) return matches diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 8ec494a09..3681485db 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -245,12 +245,12 @@ def test_att_matches_found_on_old_style_instance(self): locals_={'a': OldStyleFoo()}), set(['a.method', 'a.a', 'a.b'])) self.assertIn(u'a.__dict__', - self.com.matches(3, 'a._', locals_={'a': OldStyleFoo()})) + self.com.matches(3, 'a.__', locals_={'a': OldStyleFoo()})) @skip_old_style def test_att_matches_found_on_old_style_class_object(self): self.assertIn(u'A.__dict__', - self.com.matches(3, 'A._', locals_={'A': OldStyleFoo})) + self.com.matches(3, 'A.__', locals_={'A': OldStyleFoo})) @skip_old_style def test_issue536(self): @@ -260,7 +260,7 @@ def __getattr__(self, attr): locals_ = {'a': OldStyleWithBrokenGetAttr()} self.assertIn(u'a.__module__', - self.com.matches(3, 'a._', locals_=locals_)) + self.com.matches(3, 'a.__', locals_=locals_)) class TestMagicMethodCompletion(unittest.TestCase): From 9f3460b6c4d6619c9080faf4b3e2e6755c7f354b Mon Sep 17 00:00:00 2001 From: Jeppe Toustrup Date: Mon, 5 Oct 2015 22:12:00 +0200 Subject: [PATCH 0007/1034] Correct auto complete tests after double underscore change --- bpython/test/test_autocomplete.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 3681485db..24018f3d2 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -245,12 +245,12 @@ def test_att_matches_found_on_old_style_instance(self): locals_={'a': OldStyleFoo()}), set(['a.method', 'a.a', 'a.b'])) self.assertIn(u'a.__dict__', - self.com.matches(3, 'a.__', locals_={'a': OldStyleFoo()})) + self.com.matches(4, 'a.__', locals_={'a': OldStyleFoo()})) @skip_old_style def test_att_matches_found_on_old_style_class_object(self): self.assertIn(u'A.__dict__', - self.com.matches(3, 'A.__', locals_={'A': OldStyleFoo})) + self.com.matches(4, 'A.__', locals_={'A': OldStyleFoo})) @skip_old_style def test_issue536(self): @@ -260,7 +260,7 @@ def __getattr__(self, attr): locals_ = {'a': OldStyleWithBrokenGetAttr()} self.assertIn(u'a.__module__', - self.com.matches(3, 'a.__', locals_=locals_)) + self.com.matches(4, 'a.__', locals_=locals_)) class TestMagicMethodCompletion(unittest.TestCase): From 4df988b5b7f192eb4ceec8217c7290f6fefe0d90 Mon Sep 17 00:00:00 2001 From: Shibo Yao Date: Wed, 7 Oct 2015 07:31:34 -0500 Subject: [PATCH 0008/1034] Fix python 3 compatibility Python 3 doesn't allow addition of dict_items and list, but union works. --- bpython/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/cli.py b/bpython/cli.py index b04a0636e..3d68b68ca 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -281,7 +281,7 @@ def make_colors(config): } if platform.system() == 'Windows': - c = dict(c.items() + + c = dict(c.items() | [ ('K', 8), ('R', 9), From 668bf06f8b7ec8c239e43f359591257d7993e6c9 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 25 Nov 2015 23:58:21 -0500 Subject: [PATCH 0009/1034] 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 0010/1034] 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 0011/1034] 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 0012/1034] 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 0013/1034] 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 0014/1034] 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 0015/1034] 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 0016/1034] 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 0017/1034] 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 0018/1034] 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 @@