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