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