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