1 # -*- coding: utf-8 -*- 2 """Tests for the TerminalInteractiveShell and related pieces.""" 3 # Copyright (c) IPython Development Team. 4 # Distributed under the terms of the Modified BSD License. 5 6 import sys 7 import unittest 8 import os 9 10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory 11 12 13 from IPython.testing import tools as tt 14 15 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context 16 from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory 17 18 19 class TestAutoSuggest(unittest.TestCase): 20 def test_changing_provider(self): 21 ip = get_ipython() 22 ip.autosuggestions_provider = None 23 self.assertEqual(ip.auto_suggest, None) 24 ip.autosuggestions_provider = "AutoSuggestFromHistory" 25 self.assertIsInstance(ip.auto_suggest, AutoSuggestFromHistory) 26 ip.autosuggestions_provider = "NavigableAutoSuggestFromHistory" 27 self.assertIsInstance(ip.auto_suggest, NavigableAutoSuggestFromHistory) 28 29 30 class TestElide(unittest.TestCase): 31 def test_elide(self): 32 _elide("concatenate((a1, a2, ...), axis", "") # do not raise 33 _elide("concatenate((a1, a2, ..), . axis", "") # do not raise 34 self.assertEqual( 35 _elide("aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh", ""), 36 "aaaa.b…g.hhhhhh", 37 ) 38 39 test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""]) 40 expect_string = ( 41 os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c" 42 ) 43 self.assertEqual(_elide(test_string, ""), expect_string) 44 45 def test_elide_typed_normal(self): 46 self.assertEqual( 47 _elide( 48 "the quick brown fox jumped over the lazy dog", 49 "the quick brown fox", 50 min_elide=10, 51 ), 52 "the…fox jumped over the lazy dog", 53 ) 54 55 def test_elide_typed_short_match(self): 56 """ 57 if the match is too short we don't elide. 58 avoid the "the...the" 59 """ 60 self.assertEqual( 61 _elide("the quick brown fox jumped over the lazy dog", "the", min_elide=10), 62 "the quick brown fox jumped over the lazy dog", 63 ) 64 65 def test_elide_typed_no_match(self): 66 """ 67 if the match is too short we don't elide. 68 avoid the "the...the" 69 """ 70 # here we typed red instead of brown 71 self.assertEqual( 72 _elide( 73 "the quick brown fox jumped over the lazy dog", 74 "the quick red fox", 75 min_elide=10, 76 ), 77 "the quick brown fox jumped over the lazy dog", 78 ) 79 80 81 class TestContextAwareCompletion(unittest.TestCase): 82 def test_adjust_completion_text_based_on_context(self): 83 # Adjusted case 84 self.assertEqual( 85 _adjust_completion_text_based_on_context("arg1=", "func1(a=)", 7), "arg1" 86 ) 87 88 # Untouched cases 89 self.assertEqual( 90 _adjust_completion_text_based_on_context("arg1=", "func1(a)", 7), "arg1=" 91 ) 92 self.assertEqual( 93 _adjust_completion_text_based_on_context("arg1=", "func1(a", 7), "arg1=" 94 ) 95 self.assertEqual( 96 _adjust_completion_text_based_on_context("%magic", "func1(a=)", 7), "%magic" 97 ) 98 self.assertEqual( 99 _adjust_completion_text_based_on_context("func2", "func1(a=)", 7), "func2" 100 ) 101 102 103 # Decorator for interaction loop tests ----------------------------------------- 104 105 106 class mock_input_helper(object): 107 """Machinery for tests of the main interact loop. 108 109 Used by the mock_input decorator. 110 """ 111 def __init__(self, testgen): 112 self.testgen = testgen 113 self.exception = None 114 self.ip = get_ipython() 115 116 def __enter__(self): 117 self.orig_prompt_for_code = self.ip.prompt_for_code 118 self.ip.prompt_for_code = self.fake_input 119 return self 120 121 def __exit__(self, etype, value, tb): 122 self.ip.prompt_for_code = self.orig_prompt_for_code 123 124 def fake_input(self): 125 try: 126 return next(self.testgen) 127 except StopIteration: 128 self.ip.keep_running = False 129 return u'' 130 except: 131 self.exception = sys.exc_info() 132 self.ip.keep_running = False 133 return u'' 134 135 def mock_input(testfunc): 136 """Decorator for tests of the main interact loop. 137 138 Write the test as a generator, yield-ing the input strings, which IPython 139 will see as if they were typed in at the prompt. 140 """ 141 def test_method(self): 142 testgen = testfunc(self) 143 with mock_input_helper(testgen) as mih: 144 mih.ip.interact() 145 146 if mih.exception is not None: 147 # Re-raise captured exception 148 etype, value, tb = mih.exception 149 import traceback 150 traceback.print_tb(tb, file=sys.stdout) 151 del tb # Avoid reference loop 152 raise value 153 154 return test_method 155 156 # Test classes ----------------------------------------------------------------- 157 158 class InteractiveShellTestCase(unittest.TestCase): 159 def rl_hist_entries(self, rl, n): 160 """Get last n readline history entries as a list""" 161 return [rl.get_history_item(rl.get_current_history_length() - x) 162 for x in range(n - 1, -1, -1)] 163 164 @mock_input 165 def test_inputtransformer_syntaxerror(self): 166 ip = get_ipython() 167 ip.input_transformers_post.append(syntax_error_transformer) 168 169 try: 170 #raise Exception 171 with tt.AssertPrints('4', suppress=False): 172 yield u'print(2*2)' 173 174 with tt.AssertPrints('SyntaxError: input contains', suppress=False): 175 yield u'print(2345) # syntaxerror' 176 177 with tt.AssertPrints('16', suppress=False): 178 yield u'print(4*4)' 179 180 finally: 181 ip.input_transformers_post.remove(syntax_error_transformer) 182 183 def test_repl_not_plain_text(self): 184 ip = get_ipython() 185 formatter = ip.display_formatter 186 assert formatter.active_types == ['text/plain'] 187 188 # terminal may have arbitrary mimetype handler to open external viewer 189 # or inline images. 190 assert formatter.ipython_display_formatter.enabled 191 192 class Test(object): 193 def __repr__(self): 194 return "<Test %i>" % id(self) 195 196 def _repr_html_(self): 197 return '<html>' 198 199 # verify that HTML repr isn't computed 200 obj = Test() 201 data, _ = formatter.format(obj) 202 self.assertEqual(data, {'text/plain': repr(obj)}) 203 204 class Test2(Test): 205 def _ipython_display_(self): 206 from IPython.display import display, HTML 207 208 display(HTML("<custom>")) 209 210 # verify that mimehandlers are called 211 called = False 212 213 def handler(data, metadata): 214 print("Handler called") 215 nonlocal called 216 called = True 217 218 ip.display_formatter.active_types.append("text/html") 219 ip.display_formatter.formatters["text/html"].enabled = True 220 ip.mime_renderers["text/html"] = handler 221 try: 222 obj = Test() 223 display(obj) 224 finally: 225 ip.display_formatter.formatters["text/html"].enabled = False 226 del ip.mime_renderers["text/html"] 227 228 assert called == True 229 230 231 def syntax_error_transformer(lines): 232 """Transformer that throws SyntaxError if 'syntaxerror' is in the code.""" 233 for line in lines: 234 pos = line.find('syntaxerror') 235 if pos >= 0: 236 e = SyntaxError('input contains "syntaxerror"') 237 e.text = line 238 e.offset = pos + 1 239 raise e 240 return lines 241 242 243 class TerminalMagicsTestCase(unittest.TestCase): 244 def test_paste_magics_blankline(self): 245 """Test that code with a blank line doesn't get split (gh-3246).""" 246 ip = get_ipython() 247 s = ('def pasted_func(a):\n' 248 ' b = a+1\n' 249 '\n' 250 ' return b') 251 252 tm = ip.magics_manager.registry['TerminalMagics'] 253 tm.store_or_execute(s, name=None) 254 255 self.assertEqual(ip.user_ns['pasted_func'](54), 55) 256