##// END OF EJS Templates
Do not duplicate '=' when tab completing a kwarg with the cursor on '='
Steve Bartz -
Show More
@@ -1,154 +1,160 b''
1 """prompt-toolkit utilities
1 """prompt-toolkit utilities
2
2
3 Everything in this module is a private API,
3 Everything in this module is a private API,
4 not to be used outside IPython.
4 not to be used outside IPython.
5 """
5 """
6
6
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 import unicodedata
10 import unicodedata
11 from wcwidth import wcwidth
11 from wcwidth import wcwidth
12
12
13 from IPython.core.completer import (
13 from IPython.core.completer import (
14 provisionalcompleter, cursor_to_position,
14 provisionalcompleter, cursor_to_position,
15 _deduplicate_completions)
15 _deduplicate_completions)
16 from prompt_toolkit.completion import Completer, Completion
16 from prompt_toolkit.completion import Completer, Completion
17 from prompt_toolkit.layout.lexers import Lexer
17 from prompt_toolkit.layout.lexers import Lexer
18 from prompt_toolkit.layout.lexers import PygmentsLexer
18 from prompt_toolkit.layout.lexers import PygmentsLexer
19
19
20 import pygments.lexers as pygments_lexers
20 import pygments.lexers as pygments_lexers
21
21
22 _completion_sentinel = object()
22 _completion_sentinel = object()
23
23
24 def _elide(string, *, min_elide=30):
24 def _elide(string, *, min_elide=30):
25 """
25 """
26 If a string is long enough, and has at least 2 dots,
26 If a string is long enough, and has at least 2 dots,
27 replace the middle part with ellipses.
27 replace the middle part with ellipses.
28
28
29 If three consecutive dots, or two consecutive dots are encountered these are
29 If three consecutive dots, or two consecutive dots are encountered these are
30 replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode
30 replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode
31 equivalents
31 equivalents
32 """
32 """
33 string = string.replace('...','\N{HORIZONTAL ELLIPSIS}')
33 string = string.replace('...','\N{HORIZONTAL ELLIPSIS}')
34 string = string.replace('..','\N{TWO DOT LEADER}')
34 string = string.replace('..','\N{TWO DOT LEADER}')
35 if len(string) < min_elide:
35 if len(string) < min_elide:
36 return string
36 return string
37
37
38 parts = string.split('.')
38 parts = string.split('.')
39
39
40 if len(parts) <= 3:
40 if len(parts) <= 3:
41 return string
41 return string
42
42
43 return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(parts[0], parts[1][0], parts[-2][-1], parts[-1])
43 return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(parts[0], parts[1][0], parts[-2][-1], parts[-1])
44
44
45
45
46 def _adjust_completion_text_based_on_context(text, body, offset):
47 if text.endswith('=') and len(body) > offset and body[offset] is '=':
48 return text[:-1]
49 else:
50 return text
46
51
47
52
48 class IPythonPTCompleter(Completer):
53 class IPythonPTCompleter(Completer):
49 """Adaptor to provide IPython completions to prompt_toolkit"""
54 """Adaptor to provide IPython completions to prompt_toolkit"""
50 def __init__(self, ipy_completer=None, shell=None, patch_stdout=None):
55 def __init__(self, ipy_completer=None, shell=None, patch_stdout=None):
51 if shell is None and ipy_completer is None:
56 if shell is None and ipy_completer is None:
52 raise TypeError("Please pass shell=an InteractiveShell instance.")
57 raise TypeError("Please pass shell=an InteractiveShell instance.")
53 self._ipy_completer = ipy_completer
58 self._ipy_completer = ipy_completer
54 self.shell = shell
59 self.shell = shell
55 if patch_stdout is None:
60 if patch_stdout is None:
56 raise TypeError("Please pass patch_stdout")
61 raise TypeError("Please pass patch_stdout")
57 self.patch_stdout = patch_stdout
62 self.patch_stdout = patch_stdout
58
63
59 @property
64 @property
60 def ipy_completer(self):
65 def ipy_completer(self):
61 if self._ipy_completer:
66 if self._ipy_completer:
62 return self._ipy_completer
67 return self._ipy_completer
63 else:
68 else:
64 return self.shell.Completer
69 return self.shell.Completer
65
70
66 def get_completions(self, document, complete_event):
71 def get_completions(self, document, complete_event):
67 if not document.current_line.strip():
72 if not document.current_line.strip():
68 return
73 return
69 # Some bits of our completion system may print stuff (e.g. if a module
74 # Some bits of our completion system may print stuff (e.g. if a module
70 # is imported). This context manager ensures that doesn't interfere with
75 # is imported). This context manager ensures that doesn't interfere with
71 # the prompt.
76 # the prompt.
72
77
73 with self.patch_stdout(), provisionalcompleter():
78 with self.patch_stdout(), provisionalcompleter():
74 body = document.text
79 body = document.text
75 cursor_row = document.cursor_position_row
80 cursor_row = document.cursor_position_row
76 cursor_col = document.cursor_position_col
81 cursor_col = document.cursor_position_col
77 cursor_position = document.cursor_position
82 cursor_position = document.cursor_position
78 offset = cursor_to_position(body, cursor_row, cursor_col)
83 offset = cursor_to_position(body, cursor_row, cursor_col)
79 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
84 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
80
85
81 @staticmethod
86 @staticmethod
82 def _get_completions(body, offset, cursor_position, ipyc):
87 def _get_completions(body, offset, cursor_position, ipyc):
83 """
88 """
84 Private equivalent of get_completions() use only for unit_testing.
89 Private equivalent of get_completions() use only for unit_testing.
85 """
90 """
86 debug = getattr(ipyc, 'debug', False)
91 debug = getattr(ipyc, 'debug', False)
87 completions = _deduplicate_completions(
92 completions = _deduplicate_completions(
88 body, ipyc.completions(body, offset))
93 body, ipyc.completions(body, offset))
89 for c in completions:
94 for c in completions:
90 if not c.text:
95 if not c.text:
91 # Guard against completion machinery giving us an empty string.
96 # Guard against completion machinery giving us an empty string.
92 continue
97 continue
93 text = unicodedata.normalize('NFC', c.text)
98 text = unicodedata.normalize('NFC', c.text)
94 # When the first character of the completion has a zero length,
99 # When the first character of the completion has a zero length,
95 # then it's probably a decomposed unicode character. E.g. caused by
100 # then it's probably a decomposed unicode character. E.g. caused by
96 # the "\dot" completion. Try to compose again with the previous
101 # the "\dot" completion. Try to compose again with the previous
97 # character.
102 # character.
98 if wcwidth(text[0]) == 0:
103 if wcwidth(text[0]) == 0:
99 if cursor_position + c.start > 0:
104 if cursor_position + c.start > 0:
100 char_before = body[c.start - 1]
105 char_before = body[c.start - 1]
101 fixed_text = unicodedata.normalize(
106 fixed_text = unicodedata.normalize(
102 'NFC', char_before + text)
107 'NFC', char_before + text)
103
108
104 # Yield the modified completion instead, if this worked.
109 # Yield the modified completion instead, if this worked.
105 if wcwidth(text[0:1]) == 1:
110 if wcwidth(text[0:1]) == 1:
106 yield Completion(fixed_text, start_position=c.start - offset - 1)
111 yield Completion(fixed_text, start_position=c.start - offset - 1)
107 continue
112 continue
108
113
109 # TODO: Use Jedi to determine meta_text
114 # TODO: Use Jedi to determine meta_text
110 # (Jedi currently has a bug that results in incorrect information.)
115 # (Jedi currently has a bug that results in incorrect information.)
111 # meta_text = ''
116 # meta_text = ''
112 # yield Completion(m, start_position=start_pos,
117 # yield Completion(m, start_position=start_pos,
113 # display_meta=meta_text)
118 # display_meta=meta_text)
114 display_text = c.text
119 display_text = c.text
115
120
121 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
116 if c.type == 'function':
122 if c.type == 'function':
117 display_text = display_text + '()'
123 display_text = display_text + '()'
118
124
119 yield Completion(c.text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
125 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
120
126
121 class IPythonPTLexer(Lexer):
127 class IPythonPTLexer(Lexer):
122 """
128 """
123 Wrapper around PythonLexer and BashLexer.
129 Wrapper around PythonLexer and BashLexer.
124 """
130 """
125 def __init__(self):
131 def __init__(self):
126 l = pygments_lexers
132 l = pygments_lexers
127 self.python_lexer = PygmentsLexer(l.Python3Lexer)
133 self.python_lexer = PygmentsLexer(l.Python3Lexer)
128 self.shell_lexer = PygmentsLexer(l.BashLexer)
134 self.shell_lexer = PygmentsLexer(l.BashLexer)
129
135
130 self.magic_lexers = {
136 self.magic_lexers = {
131 'HTML': PygmentsLexer(l.HtmlLexer),
137 'HTML': PygmentsLexer(l.HtmlLexer),
132 'html': PygmentsLexer(l.HtmlLexer),
138 'html': PygmentsLexer(l.HtmlLexer),
133 'javascript': PygmentsLexer(l.JavascriptLexer),
139 'javascript': PygmentsLexer(l.JavascriptLexer),
134 'js': PygmentsLexer(l.JavascriptLexer),
140 'js': PygmentsLexer(l.JavascriptLexer),
135 'perl': PygmentsLexer(l.PerlLexer),
141 'perl': PygmentsLexer(l.PerlLexer),
136 'ruby': PygmentsLexer(l.RubyLexer),
142 'ruby': PygmentsLexer(l.RubyLexer),
137 'latex': PygmentsLexer(l.TexLexer),
143 'latex': PygmentsLexer(l.TexLexer),
138 }
144 }
139
145
140 def lex_document(self, cli, document):
146 def lex_document(self, cli, document):
141 text = document.text.lstrip()
147 text = document.text.lstrip()
142
148
143 lexer = self.python_lexer
149 lexer = self.python_lexer
144
150
145 if text.startswith('!') or text.startswith('%%bash'):
151 if text.startswith('!') or text.startswith('%%bash'):
146 lexer = self.shell_lexer
152 lexer = self.shell_lexer
147
153
148 elif text.startswith('%%'):
154 elif text.startswith('%%'):
149 for magic, l in self.magic_lexers.items():
155 for magic, l in self.magic_lexers.items():
150 if text.startswith('%%' + magic):
156 if text.startswith('%%' + magic):
151 lexer = l
157 lexer = l
152 break
158 break
153
159
154 return lexer.lex_document(cli, document)
160 return lexer.lex_document(cli, document)
@@ -1,163 +1,176 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Tests for the TerminalInteractiveShell and related pieces."""
2 """Tests for the TerminalInteractiveShell and related pieces."""
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import sys
6 import sys
7 import unittest
7 import unittest
8
8
9 from IPython.core.inputtransformer import InputTransformer
9 from IPython.core.inputtransformer import InputTransformer
10 from IPython.testing import tools as tt
10 from IPython.testing import tools as tt
11 from IPython.utils.capture import capture_output
11 from IPython.utils.capture import capture_output
12
12
13 from IPython.terminal.ptutils import _elide
13 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
14 import nose.tools as nt
14 import nose.tools as nt
15
15
16 class TestElide(unittest.TestCase):
16 class TestElide(unittest.TestCase):
17
17
18 def test_elide(self):
18 def test_elide(self):
19 _elide('concatenate((a1, a2, ...), axis') # do not raise
19 _elide('concatenate((a1, a2, ...), axis') # do not raise
20 _elide('concatenate((a1, a2, ..), . axis') # do not raise
20 _elide('concatenate((a1, a2, ..), . axis') # do not raise
21 nt.assert_equal(_elide('aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh'), 'aaaa.b…g.hhhhhh')
21 nt.assert_equal(_elide('aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh'), 'aaaa.b…g.hhhhhh')
22
22
23
24 class TestContextAwareCompletion(unittest.TestCase):
25
26 def test_adjust_completion_text_based_on_context(self):
27 # Adjusted case
28 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a=)', 7), 'arg1')
29
30 # Untouched cases
31 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a)', 7), 'arg1=')
32 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a', 7), 'arg1=')
33 nt.assert_equal(_adjust_completion_text_based_on_context('%magic', 'func1(a=)', 7), '%magic')
34 nt.assert_equal(_adjust_completion_text_based_on_context('func2', 'func1(a=)', 7), 'func2')
35
23 # Decorator for interaction loop tests -----------------------------------------
36 # Decorator for interaction loop tests -----------------------------------------
24
37
25 class mock_input_helper(object):
38 class mock_input_helper(object):
26 """Machinery for tests of the main interact loop.
39 """Machinery for tests of the main interact loop.
27
40
28 Used by the mock_input decorator.
41 Used by the mock_input decorator.
29 """
42 """
30 def __init__(self, testgen):
43 def __init__(self, testgen):
31 self.testgen = testgen
44 self.testgen = testgen
32 self.exception = None
45 self.exception = None
33 self.ip = get_ipython()
46 self.ip = get_ipython()
34
47
35 def __enter__(self):
48 def __enter__(self):
36 self.orig_prompt_for_code = self.ip.prompt_for_code
49 self.orig_prompt_for_code = self.ip.prompt_for_code
37 self.ip.prompt_for_code = self.fake_input
50 self.ip.prompt_for_code = self.fake_input
38 return self
51 return self
39
52
40 def __exit__(self, etype, value, tb):
53 def __exit__(self, etype, value, tb):
41 self.ip.prompt_for_code = self.orig_prompt_for_code
54 self.ip.prompt_for_code = self.orig_prompt_for_code
42
55
43 def fake_input(self):
56 def fake_input(self):
44 try:
57 try:
45 return next(self.testgen)
58 return next(self.testgen)
46 except StopIteration:
59 except StopIteration:
47 self.ip.keep_running = False
60 self.ip.keep_running = False
48 return u''
61 return u''
49 except:
62 except:
50 self.exception = sys.exc_info()
63 self.exception = sys.exc_info()
51 self.ip.keep_running = False
64 self.ip.keep_running = False
52 return u''
65 return u''
53
66
54 def mock_input(testfunc):
67 def mock_input(testfunc):
55 """Decorator for tests of the main interact loop.
68 """Decorator for tests of the main interact loop.
56
69
57 Write the test as a generator, yield-ing the input strings, which IPython
70 Write the test as a generator, yield-ing the input strings, which IPython
58 will see as if they were typed in at the prompt.
71 will see as if they were typed in at the prompt.
59 """
72 """
60 def test_method(self):
73 def test_method(self):
61 testgen = testfunc(self)
74 testgen = testfunc(self)
62 with mock_input_helper(testgen) as mih:
75 with mock_input_helper(testgen) as mih:
63 mih.ip.interact()
76 mih.ip.interact()
64
77
65 if mih.exception is not None:
78 if mih.exception is not None:
66 # Re-raise captured exception
79 # Re-raise captured exception
67 etype, value, tb = mih.exception
80 etype, value, tb = mih.exception
68 import traceback
81 import traceback
69 traceback.print_tb(tb, file=sys.stdout)
82 traceback.print_tb(tb, file=sys.stdout)
70 del tb # Avoid reference loop
83 del tb # Avoid reference loop
71 raise value
84 raise value
72
85
73 return test_method
86 return test_method
74
87
75 # Test classes -----------------------------------------------------------------
88 # Test classes -----------------------------------------------------------------
76
89
77 class InteractiveShellTestCase(unittest.TestCase):
90 class InteractiveShellTestCase(unittest.TestCase):
78 def rl_hist_entries(self, rl, n):
91 def rl_hist_entries(self, rl, n):
79 """Get last n readline history entries as a list"""
92 """Get last n readline history entries as a list"""
80 return [rl.get_history_item(rl.get_current_history_length() - x)
93 return [rl.get_history_item(rl.get_current_history_length() - x)
81 for x in range(n - 1, -1, -1)]
94 for x in range(n - 1, -1, -1)]
82
95
83 @mock_input
96 @mock_input
84 def test_inputtransformer_syntaxerror(self):
97 def test_inputtransformer_syntaxerror(self):
85 ip = get_ipython()
98 ip = get_ipython()
86 transformer = SyntaxErrorTransformer()
99 transformer = SyntaxErrorTransformer()
87 ip.input_splitter.python_line_transforms.append(transformer)
100 ip.input_splitter.python_line_transforms.append(transformer)
88 ip.input_transformer_manager.python_line_transforms.append(transformer)
101 ip.input_transformer_manager.python_line_transforms.append(transformer)
89
102
90 try:
103 try:
91 #raise Exception
104 #raise Exception
92 with tt.AssertPrints('4', suppress=False):
105 with tt.AssertPrints('4', suppress=False):
93 yield u'print(2*2)'
106 yield u'print(2*2)'
94
107
95 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
108 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
96 yield u'print(2345) # syntaxerror'
109 yield u'print(2345) # syntaxerror'
97
110
98 with tt.AssertPrints('16', suppress=False):
111 with tt.AssertPrints('16', suppress=False):
99 yield u'print(4*4)'
112 yield u'print(4*4)'
100
113
101 finally:
114 finally:
102 ip.input_splitter.python_line_transforms.remove(transformer)
115 ip.input_splitter.python_line_transforms.remove(transformer)
103 ip.input_transformer_manager.python_line_transforms.remove(transformer)
116 ip.input_transformer_manager.python_line_transforms.remove(transformer)
104
117
105 def test_plain_text_only(self):
118 def test_plain_text_only(self):
106 ip = get_ipython()
119 ip = get_ipython()
107 formatter = ip.display_formatter
120 formatter = ip.display_formatter
108 assert formatter.active_types == ['text/plain']
121 assert formatter.active_types == ['text/plain']
109 assert not formatter.ipython_display_formatter.enabled
122 assert not formatter.ipython_display_formatter.enabled
110
123
111 class Test(object):
124 class Test(object):
112 def __repr__(self):
125 def __repr__(self):
113 return "<Test %i>" % id(self)
126 return "<Test %i>" % id(self)
114
127
115 def _repr_html_(self):
128 def _repr_html_(self):
116 return '<html>'
129 return '<html>'
117
130
118 # verify that HTML repr isn't computed
131 # verify that HTML repr isn't computed
119 obj = Test()
132 obj = Test()
120 data, _ = formatter.format(obj)
133 data, _ = formatter.format(obj)
121 self.assertEqual(data, {'text/plain': repr(obj)})
134 self.assertEqual(data, {'text/plain': repr(obj)})
122
135
123 class Test2(Test):
136 class Test2(Test):
124 def _ipython_display_(self):
137 def _ipython_display_(self):
125 from IPython.display import display
138 from IPython.display import display
126 display('<custom>')
139 display('<custom>')
127
140
128 # verify that _ipython_display_ shortcut isn't called
141 # verify that _ipython_display_ shortcut isn't called
129 obj = Test2()
142 obj = Test2()
130 with capture_output() as captured:
143 with capture_output() as captured:
131 data, _ = formatter.format(obj)
144 data, _ = formatter.format(obj)
132
145
133 self.assertEqual(data, {'text/plain': repr(obj)})
146 self.assertEqual(data, {'text/plain': repr(obj)})
134 assert captured.stdout == ''
147 assert captured.stdout == ''
135
148
136
149
137
150
138 class SyntaxErrorTransformer(InputTransformer):
151 class SyntaxErrorTransformer(InputTransformer):
139 def push(self, line):
152 def push(self, line):
140 pos = line.find('syntaxerror')
153 pos = line.find('syntaxerror')
141 if pos >= 0:
154 if pos >= 0:
142 e = SyntaxError('input contains "syntaxerror"')
155 e = SyntaxError('input contains "syntaxerror"')
143 e.text = line
156 e.text = line
144 e.offset = pos + 1
157 e.offset = pos + 1
145 raise e
158 raise e
146 return line
159 return line
147
160
148 def reset(self):
161 def reset(self):
149 pass
162 pass
150
163
151 class TerminalMagicsTestCase(unittest.TestCase):
164 class TerminalMagicsTestCase(unittest.TestCase):
152 def test_paste_magics_blankline(self):
165 def test_paste_magics_blankline(self):
153 """Test that code with a blank line doesn't get split (gh-3246)."""
166 """Test that code with a blank line doesn't get split (gh-3246)."""
154 ip = get_ipython()
167 ip = get_ipython()
155 s = ('def pasted_func(a):\n'
168 s = ('def pasted_func(a):\n'
156 ' b = a+1\n'
169 ' b = a+1\n'
157 '\n'
170 '\n'
158 ' return b')
171 ' return b')
159
172
160 tm = ip.magics_manager.registry['TerminalMagics']
173 tm = ip.magics_manager.registry['TerminalMagics']
161 tm.store_or_execute(s, name=None)
174 tm.store_or_execute(s, name=None)
162
175
163 self.assertEqual(ip.user_ns['pasted_func'](54), 55)
176 self.assertEqual(ip.user_ns['pasted_func'](54), 55)
General Comments 0
You need to be logged in to leave comments. Login now