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