##// END OF EJS Templates
Pop the last if path ends with slash
Inception95 -
Show More
@@ -1,166 +1,167 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 file_parts.pop() if file_parts[-1] == '' else None
45
46
46 if len(object_parts) > 3:
47 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])
48 return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][0], object_parts[-2][-1], object_parts[-1])
48
49
49 elif len(file_parts) > 3:
50 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])
51 return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][0], file_parts[-2][-1], file_parts[-1])
51
52
52 return string
53 return string
53
54
54
55
55 def _adjust_completion_text_based_on_context(text, body, offset):
56 def _adjust_completion_text_based_on_context(text, body, offset):
56 if text.endswith('=') and len(body) > offset and body[offset] == '=':
57 if text.endswith('=') and len(body) > offset and body[offset] == '=':
57 return text[:-1]
58 return text[:-1]
58 else:
59 else:
59 return text
60 return text
60
61
61
62
62 class IPythonPTCompleter(Completer):
63 class IPythonPTCompleter(Completer):
63 """Adaptor to provide IPython completions to prompt_toolkit"""
64 """Adaptor to provide IPython completions to prompt_toolkit"""
64 def __init__(self, ipy_completer=None, shell=None):
65 def __init__(self, ipy_completer=None, shell=None):
65 if shell is None and ipy_completer is None:
66 if shell is None and ipy_completer is None:
66 raise TypeError("Please pass shell=an InteractiveShell instance.")
67 raise TypeError("Please pass shell=an InteractiveShell instance.")
67 self._ipy_completer = ipy_completer
68 self._ipy_completer = ipy_completer
68 self.shell = shell
69 self.shell = shell
69
70
70 @property
71 @property
71 def ipy_completer(self):
72 def ipy_completer(self):
72 if self._ipy_completer:
73 if self._ipy_completer:
73 return self._ipy_completer
74 return self._ipy_completer
74 else:
75 else:
75 return self.shell.Completer
76 return self.shell.Completer
76
77
77 def get_completions(self, document, complete_event):
78 def get_completions(self, document, complete_event):
78 if not document.current_line.strip():
79 if not document.current_line.strip():
79 return
80 return
80 # Some bits of our completion system may print stuff (e.g. if a module
81 # 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
82 # is imported). This context manager ensures that doesn't interfere with
82 # the prompt.
83 # the prompt.
83
84
84 with patch_stdout(), provisionalcompleter():
85 with patch_stdout(), provisionalcompleter():
85 body = document.text
86 body = document.text
86 cursor_row = document.cursor_position_row
87 cursor_row = document.cursor_position_row
87 cursor_col = document.cursor_position_col
88 cursor_col = document.cursor_position_col
88 cursor_position = document.cursor_position
89 cursor_position = document.cursor_position
89 offset = cursor_to_position(body, cursor_row, cursor_col)
90 offset = cursor_to_position(body, cursor_row, cursor_col)
90 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
91 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
91
92
92 @staticmethod
93 @staticmethod
93 def _get_completions(body, offset, cursor_position, ipyc):
94 def _get_completions(body, offset, cursor_position, ipyc):
94 """
95 """
95 Private equivalent of get_completions() use only for unit_testing.
96 Private equivalent of get_completions() use only for unit_testing.
96 """
97 """
97 debug = getattr(ipyc, 'debug', False)
98 debug = getattr(ipyc, 'debug', False)
98 completions = _deduplicate_completions(
99 completions = _deduplicate_completions(
99 body, ipyc.completions(body, offset))
100 body, ipyc.completions(body, offset))
100 for c in completions:
101 for c in completions:
101 if not c.text:
102 if not c.text:
102 # Guard against completion machinery giving us an empty string.
103 # Guard against completion machinery giving us an empty string.
103 continue
104 continue
104 text = unicodedata.normalize('NFC', c.text)
105 text = unicodedata.normalize('NFC', c.text)
105 # When the first character of the completion has a zero length,
106 # When the first character of the completion has a zero length,
106 # then it's probably a decomposed unicode character. E.g. caused by
107 # then it's probably a decomposed unicode character. E.g. caused by
107 # the "\dot" completion. Try to compose again with the previous
108 # the "\dot" completion. Try to compose again with the previous
108 # character.
109 # character.
109 if wcwidth(text[0]) == 0:
110 if wcwidth(text[0]) == 0:
110 if cursor_position + c.start > 0:
111 if cursor_position + c.start > 0:
111 char_before = body[c.start - 1]
112 char_before = body[c.start - 1]
112 fixed_text = unicodedata.normalize(
113 fixed_text = unicodedata.normalize(
113 'NFC', char_before + text)
114 'NFC', char_before + text)
114
115
115 # Yield the modified completion instead, if this worked.
116 # Yield the modified completion instead, if this worked.
116 if wcwidth(text[0:1]) == 1:
117 if wcwidth(text[0:1]) == 1:
117 yield Completion(fixed_text, start_position=c.start - offset - 1)
118 yield Completion(fixed_text, start_position=c.start - offset - 1)
118 continue
119 continue
119
120
120 # TODO: Use Jedi to determine meta_text
121 # TODO: Use Jedi to determine meta_text
121 # (Jedi currently has a bug that results in incorrect information.)
122 # (Jedi currently has a bug that results in incorrect information.)
122 # meta_text = ''
123 # meta_text = ''
123 # yield Completion(m, start_position=start_pos,
124 # yield Completion(m, start_position=start_pos,
124 # display_meta=meta_text)
125 # display_meta=meta_text)
125 display_text = c.text
126 display_text = c.text
126
127
127 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
128 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
128 if c.type == 'function':
129 if c.type == 'function':
129 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()'), display_meta=c.type+c.signature)
130 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()'), display_meta=c.type+c.signature)
130 else:
131 else:
131 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
132 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
132
133
133 class IPythonPTLexer(Lexer):
134 class IPythonPTLexer(Lexer):
134 """
135 """
135 Wrapper around PythonLexer and BashLexer.
136 Wrapper around PythonLexer and BashLexer.
136 """
137 """
137 def __init__(self):
138 def __init__(self):
138 l = pygments_lexers
139 l = pygments_lexers
139 self.python_lexer = PygmentsLexer(l.Python3Lexer)
140 self.python_lexer = PygmentsLexer(l.Python3Lexer)
140 self.shell_lexer = PygmentsLexer(l.BashLexer)
141 self.shell_lexer = PygmentsLexer(l.BashLexer)
141
142
142 self.magic_lexers = {
143 self.magic_lexers = {
143 'HTML': PygmentsLexer(l.HtmlLexer),
144 'HTML': PygmentsLexer(l.HtmlLexer),
144 'html': PygmentsLexer(l.HtmlLexer),
145 'html': PygmentsLexer(l.HtmlLexer),
145 'javascript': PygmentsLexer(l.JavascriptLexer),
146 'javascript': PygmentsLexer(l.JavascriptLexer),
146 'js': PygmentsLexer(l.JavascriptLexer),
147 'js': PygmentsLexer(l.JavascriptLexer),
147 'perl': PygmentsLexer(l.PerlLexer),
148 'perl': PygmentsLexer(l.PerlLexer),
148 'ruby': PygmentsLexer(l.RubyLexer),
149 'ruby': PygmentsLexer(l.RubyLexer),
149 'latex': PygmentsLexer(l.TexLexer),
150 'latex': PygmentsLexer(l.TexLexer),
150 }
151 }
151
152
152 def lex_document(self, document):
153 def lex_document(self, document):
153 text = document.text.lstrip()
154 text = document.text.lstrip()
154
155
155 lexer = self.python_lexer
156 lexer = self.python_lexer
156
157
157 if text.startswith('!') or text.startswith('%%bash'):
158 if text.startswith('!') or text.startswith('%%bash'):
158 lexer = self.shell_lexer
159 lexer = self.shell_lexer
159
160
160 elif text.startswith('%%'):
161 elif text.startswith('%%'):
161 for magic, l in self.magic_lexers.items():
162 for magic, l in self.magic_lexers.items():
162 if text.startswith('%%' + magic):
163 if text.startswith('%%' + magic):
163 lexer = l
164 lexer = l
164 break
165 break
165
166
166 return lexer.lex_document(document)
167 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')
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 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