##// END OF EJS Templates
Fix a couple of edge cases, and update tests...
Matthias Bussonnier -
Show More
@@ -1,113 +1,114 b''
1 1 # http://travis-ci.org/#!/ipython/ipython
2 2 language: python
3 3 os: linux
4 4
5 5 addons:
6 6 apt:
7 7 packages:
8 8 - graphviz
9 9
10 10 python:
11 11 - 3.6
12 12
13 13 sudo: false
14 14
15 15 env:
16 16 global:
17 17 - PATH=$TRAVIS_BUILD_DIR/pandoc:$PATH
18 18
19 19 group: edge
20 20
21 21 before_install:
22 22 - |
23 23 # install Python on macOS
24 24 if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
25 25 env | sort
26 26 if ! which python$TRAVIS_PYTHON_VERSION; then
27 27 HOMEBREW_NO_AUTO_UPDATE=1 brew tap minrk/homebrew-python-frameworks
28 28 HOMEBREW_NO_AUTO_UPDATE=1 brew cask install python-framework-${TRAVIS_PYTHON_VERSION/./}
29 29 fi
30 30 python3 -m pip install virtualenv
31 31 python3 -m virtualenv -p $(which python$TRAVIS_PYTHON_VERSION) ~/travis-env
32 32 source ~/travis-env/bin/activate
33 33 fi
34 34 - python --version
35 35
36 36 install:
37 37 - pip install pip --upgrade
38 38 - pip install setuptools --upgrade
39 39 - pip install -e file://$PWD#egg=ipython[test] --upgrade
40 40 - pip install trio curio --upgrade --upgrade-strategy eager
41 - pip install pytest 'matplotlib !=3.2.0'
41 - pip install pytest 'matplotlib !=3.2.0' mypy
42 42 - pip install codecov check-manifest --upgrade
43 43
44 44 script:
45 45 - check-manifest
46 46 - |
47 47 if [[ "$TRAVIS_PYTHON_VERSION" == "nightly" ]]; then
48 48 # on nightly fake parso known the grammar
49 49 cp /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/parso/python/grammar37.txt /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/parso/python/grammar38.txt
50 50 fi
51 51 - cd /tmp && iptest --coverage xml && cd -
52 52 - pytest IPython
53 - mypy --ignore-missing-imports -m IPython.terminal.ptutils
53 54 # On the latest Python (on Linux) only, make sure that the docs build.
54 55 - |
55 56 if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
56 57 pip install -r docs/requirements.txt
57 58 python tools/fixup_whats_new_pr.py
58 59 make -C docs/ html SPHINXOPTS="-W"
59 60 fi
60 61
61 62 after_success:
62 63 - cp /tmp/ipy_coverage.xml ./
63 64 - cp /tmp/.coverage ./
64 65 - codecov
65 66
66 67 matrix:
67 68 include:
68 69 - arch: amd64
69 70 python: "3.7"
70 71 dist: xenial
71 72 sudo: true
72 73 - arch: amd64
73 74 python: "3.8"
74 75 dist: xenial
75 76 sudo: true
76 77 - arch: amd64
77 78 python: "nightly"
78 79 dist: xenial
79 80 sudo: true
80 81 - arch: arm64
81 82 python: "nightly"
82 83 dist: bionic
83 84 env: ARM64=True
84 85 sudo: true
85 86 - os: osx
86 87 language: generic
87 88 python: 3.6
88 89 env: TRAVIS_PYTHON_VERSION=3.6
89 90 - os: osx
90 91 language: generic
91 92 python: 3.7
92 93 env: TRAVIS_PYTHON_VERSION=3.7
93 94 allow_failures:
94 95 - python: nightly
95 96
96 97 before_deploy:
97 98 - rm -rf dist/
98 99 - python setup.py sdist
99 100 - python setup.py bdist_wheel
100 101
101 102 deploy:
102 103 provider: releases
103 104 api_key:
104 105 secure: Y/Ae9tYs5aoBU8bDjN2YrwGG6tCbezj/h3Lcmtx8HQavSbBgXnhnZVRb2snOKD7auqnqjfT/7QMm4ZyKvaOEgyggGktKqEKYHC8KOZ7yp8I5/UMDtk6j9TnXpSqqBxPiud4MDV76SfRYEQiaDoG4tGGvSfPJ9KcNjKrNvSyyxns=
105 106 file: dist/*
106 107 file_glob: true
107 108 skip_cleanup: true
108 109 on:
109 110 repo: ipython/ipython
110 111 all_branches: true # Backports are released from e.g. 5.x branch
111 112 tags: true
112 113 python: 3.6 # Any version should work, but we only need one
113 114 condition: $TRAVIS_OS_NAME = "linux"
@@ -1,190 +1,192 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 def _elide_point(string, *, min_elide=30):
26 def _elide_point(string:str, *, min_elide=30)->str:
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 45 if file_parts[-1] == '':
46 46 file_parts.pop()
47 47
48 48 if len(object_parts) > 3:
49 49 return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][0], object_parts[-2][-1], object_parts[-1])
50 50
51 51 elif len(file_parts) > 3:
52 52 return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][0], file_parts[-2][-1], file_parts[-1])
53 53
54 54 return string
55 55
56 def _elide_typed(string, typed, *, min_elide=30):
56 def _elide_typed(string:str, typed:str, *, min_elide:int=30)->str:
57 57 """
58 58 Elide the middle of a long string if the beginning has already been typed.
59 59 """
60 60
61 61 if len(string) < min_elide:
62 62 return string
63 63 cut_how_much = len(typed)-3
64 if cut_how_much < 7:
65 return string
64 66 if string.startswith(typed) and len(string)> len(typed):
65 67 return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}"
66 68 return string
67 69
68 def _elide(string, typed, min_elide=30):
70 def _elide(string:str, typed:str, min_elide=30)->str:
69 71 return _elide_typed(
70 72 _elide_point(string, min_elide=min_elide),
71 73 typed, min_elide=min_elide)
72 74
73 75
74 76
75 77 def _adjust_completion_text_based_on_context(text, body, offset):
76 78 if text.endswith('=') and len(body) > offset and body[offset] == '=':
77 79 return text[:-1]
78 80 else:
79 81 return text
80 82
81 83
82 84 class IPythonPTCompleter(Completer):
83 85 """Adaptor to provide IPython completions to prompt_toolkit"""
84 86 def __init__(self, ipy_completer=None, shell=None):
85 87 if shell is None and ipy_completer is None:
86 88 raise TypeError("Please pass shell=an InteractiveShell instance.")
87 89 self._ipy_completer = ipy_completer
88 90 self.shell = shell
89 91
90 92 @property
91 93 def ipy_completer(self):
92 94 if self._ipy_completer:
93 95 return self._ipy_completer
94 96 else:
95 97 return self.shell.Completer
96 98
97 99 def get_completions(self, document, complete_event):
98 100 if not document.current_line.strip():
99 101 return
100 102 # Some bits of our completion system may print stuff (e.g. if a module
101 103 # is imported). This context manager ensures that doesn't interfere with
102 104 # the prompt.
103 105
104 106 with patch_stdout(), provisionalcompleter():
105 107 body = document.text
106 108 cursor_row = document.cursor_position_row
107 109 cursor_col = document.cursor_position_col
108 110 cursor_position = document.cursor_position
109 111 offset = cursor_to_position(body, cursor_row, cursor_col)
110 112 try:
111 113 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
112 114 except Exception as e:
113 115 from traceback import print_tb
114 116 print_tb(e)
115 117
116 118 @staticmethod
117 119 def _get_completions(body, offset, cursor_position, ipyc):
118 120 """
119 121 Private equivalent of get_completions() use only for unit_testing.
120 122 """
121 123 debug = getattr(ipyc, 'debug', False)
122 124 completions = _deduplicate_completions(
123 125 body, ipyc.completions(body, offset))
124 126 for c in completions:
125 127 if not c.text:
126 128 # Guard against completion machinery giving us an empty string.
127 129 continue
128 130 text = unicodedata.normalize('NFC', c.text)
129 131 # When the first character of the completion has a zero length,
130 132 # then it's probably a decomposed unicode character. E.g. caused by
131 133 # the "\dot" completion. Try to compose again with the previous
132 134 # character.
133 135 if wcwidth(text[0]) == 0:
134 136 if cursor_position + c.start > 0:
135 137 char_before = body[c.start - 1]
136 138 fixed_text = unicodedata.normalize(
137 139 'NFC', char_before + text)
138 140
139 141 # Yield the modified completion instead, if this worked.
140 142 if wcwidth(text[0:1]) == 1:
141 143 yield Completion(fixed_text, start_position=c.start - offset - 1)
142 144 continue
143 145
144 146 # TODO: Use Jedi to determine meta_text
145 147 # (Jedi currently has a bug that results in incorrect information.)
146 148 # meta_text = ''
147 149 # yield Completion(m, start_position=start_pos,
148 150 # display_meta=meta_text)
149 151 display_text = c.text
150 152
151 153 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
152 154 if c.type == 'function':
153 155 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()', body[c.start:c.end]), display_meta=c.type+c.signature)
154 156 else:
155 157 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text, body[c.start:c.end]), display_meta=c.type)
156 158
157 159 class IPythonPTLexer(Lexer):
158 160 """
159 161 Wrapper around PythonLexer and BashLexer.
160 162 """
161 163 def __init__(self):
162 164 l = pygments_lexers
163 165 self.python_lexer = PygmentsLexer(l.Python3Lexer)
164 166 self.shell_lexer = PygmentsLexer(l.BashLexer)
165 167
166 168 self.magic_lexers = {
167 169 'HTML': PygmentsLexer(l.HtmlLexer),
168 170 'html': PygmentsLexer(l.HtmlLexer),
169 171 'javascript': PygmentsLexer(l.JavascriptLexer),
170 172 'js': PygmentsLexer(l.JavascriptLexer),
171 173 'perl': PygmentsLexer(l.PerlLexer),
172 174 'ruby': PygmentsLexer(l.RubyLexer),
173 175 'latex': PygmentsLexer(l.TexLexer),
174 176 }
175 177
176 178 def lex_document(self, document):
177 179 text = document.text.lstrip()
178 180
179 181 lexer = self.python_lexer
180 182
181 183 if text.startswith('!') or text.startswith('%%bash'):
182 184 lexer = self.shell_lexer
183 185
184 186 elif text.startswith('%%'):
185 187 for magic, l in self.magic_lexers.items():
186 188 if text.startswith('%%' + magic):
187 189 lexer = l
188 190 break
189 191
190 192 return lexer.lex_document(document)
@@ -1,175 +1,193 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 8 import os
9 9
10 10 from IPython.core.inputtransformer import InputTransformer
11 11 from IPython.testing import tools as tt
12 12 from IPython.utils.capture import capture_output
13 13
14 14 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
15 15 import nose.tools as nt
16 16
17 17 class TestElide(unittest.TestCase):
18 18
19 19 def test_elide(self):
20 _elide('concatenate((a1, a2, ...), axis') # do not raise
21 _elide('concatenate((a1, a2, ..), . axis') # do not raise
22 nt.assert_equal(_elide('aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh'), 'aaaa.b…g.hhhhhh')
23
20 _elide('concatenate((a1, a2, ...), axis', '') # do not raise
21 _elide('concatenate((a1, a2, ..), . axis', '') # do not raise
22 nt.assert_equal(_elide('aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh',''), 'aaaa.b…g.hhhhhh')
23
24 24 test_string = os.sep.join(['', 10*'a', 10*'b', 10*'c', ''])
25 25 expect_stirng = os.sep + 'a' + '\N{HORIZONTAL ELLIPSIS}' + 'b' + os.sep + 10*'c'
26 nt.assert_equal(_elide(test_string), expect_stirng)
27
26 nt.assert_equal(_elide(test_string, ''), expect_stirng)
27
28 def test_elide_typed_normal(self):
29 nt.assert_equal(_elide('the quick brown fox jumped over the lazy dog', 'the quick brown fox', min_elide=10), 'the…fox jumped over the lazy dog')
30
31
32 def test_elide_typed_short_match(self):
33 """
34 if the match is too short we don't elide.
35 avoid the "the...the"
36 """
37 nt.assert_equal(_elide('the quick brown fox jumped over the lazy dog', 'the', min_elide=10), 'the quick brown fox jumped over the lazy dog')
38
39 def test_elide_typed_no_match(self):
40 """
41 if the match is too short we don't elide.
42 avoid the "the...the"
43 """
44 # here we typed red instead of brown
45 nt.assert_equal(_elide('the quick brown fox jumped over the lazy dog', 'the quick red fox', min_elide=10), 'the quick brown fox jumped over the lazy dog')
28 46
29 47 class TestContextAwareCompletion(unittest.TestCase):
30 48
31 49 def test_adjust_completion_text_based_on_context(self):
32 50 # Adjusted case
33 51 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a=)', 7), 'arg1')
34 52
35 53 # Untouched cases
36 54 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a)', 7), 'arg1=')
37 55 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a', 7), 'arg1=')
38 56 nt.assert_equal(_adjust_completion_text_based_on_context('%magic', 'func1(a=)', 7), '%magic')
39 57 nt.assert_equal(_adjust_completion_text_based_on_context('func2', 'func1(a=)', 7), 'func2')
40 58
41 59 # Decorator for interaction loop tests -----------------------------------------
42 60
43 61 class mock_input_helper(object):
44 62 """Machinery for tests of the main interact loop.
45 63
46 64 Used by the mock_input decorator.
47 65 """
48 66 def __init__(self, testgen):
49 67 self.testgen = testgen
50 68 self.exception = None
51 69 self.ip = get_ipython()
52 70
53 71 def __enter__(self):
54 72 self.orig_prompt_for_code = self.ip.prompt_for_code
55 73 self.ip.prompt_for_code = self.fake_input
56 74 return self
57 75
58 76 def __exit__(self, etype, value, tb):
59 77 self.ip.prompt_for_code = self.orig_prompt_for_code
60 78
61 79 def fake_input(self):
62 80 try:
63 81 return next(self.testgen)
64 82 except StopIteration:
65 83 self.ip.keep_running = False
66 84 return u''
67 85 except:
68 86 self.exception = sys.exc_info()
69 87 self.ip.keep_running = False
70 88 return u''
71 89
72 90 def mock_input(testfunc):
73 91 """Decorator for tests of the main interact loop.
74 92
75 93 Write the test as a generator, yield-ing the input strings, which IPython
76 94 will see as if they were typed in at the prompt.
77 95 """
78 96 def test_method(self):
79 97 testgen = testfunc(self)
80 98 with mock_input_helper(testgen) as mih:
81 99 mih.ip.interact()
82 100
83 101 if mih.exception is not None:
84 102 # Re-raise captured exception
85 103 etype, value, tb = mih.exception
86 104 import traceback
87 105 traceback.print_tb(tb, file=sys.stdout)
88 106 del tb # Avoid reference loop
89 107 raise value
90 108
91 109 return test_method
92 110
93 111 # Test classes -----------------------------------------------------------------
94 112
95 113 class InteractiveShellTestCase(unittest.TestCase):
96 114 def rl_hist_entries(self, rl, n):
97 115 """Get last n readline history entries as a list"""
98 116 return [rl.get_history_item(rl.get_current_history_length() - x)
99 117 for x in range(n - 1, -1, -1)]
100 118
101 119 @mock_input
102 120 def test_inputtransformer_syntaxerror(self):
103 121 ip = get_ipython()
104 122 ip.input_transformers_post.append(syntax_error_transformer)
105 123
106 124 try:
107 125 #raise Exception
108 126 with tt.AssertPrints('4', suppress=False):
109 127 yield u'print(2*2)'
110 128
111 129 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
112 130 yield u'print(2345) # syntaxerror'
113 131
114 132 with tt.AssertPrints('16', suppress=False):
115 133 yield u'print(4*4)'
116 134
117 135 finally:
118 136 ip.input_transformers_post.remove(syntax_error_transformer)
119 137
120 138 def test_plain_text_only(self):
121 139 ip = get_ipython()
122 140 formatter = ip.display_formatter
123 141 assert formatter.active_types == ['text/plain']
124 142 assert not formatter.ipython_display_formatter.enabled
125 143
126 144 class Test(object):
127 145 def __repr__(self):
128 146 return "<Test %i>" % id(self)
129 147
130 148 def _repr_html_(self):
131 149 return '<html>'
132 150
133 151 # verify that HTML repr isn't computed
134 152 obj = Test()
135 153 data, _ = formatter.format(obj)
136 154 self.assertEqual(data, {'text/plain': repr(obj)})
137 155
138 156 class Test2(Test):
139 157 def _ipython_display_(self):
140 158 from IPython.display import display
141 159 display('<custom>')
142 160
143 161 # verify that _ipython_display_ shortcut isn't called
144 162 obj = Test2()
145 163 with capture_output() as captured:
146 164 data, _ = formatter.format(obj)
147 165
148 166 self.assertEqual(data, {'text/plain': repr(obj)})
149 167 assert captured.stdout == ''
150 168
151 169 def syntax_error_transformer(lines):
152 170 """Transformer that throws SyntaxError if 'syntaxerror' is in the code."""
153 171 for line in lines:
154 172 pos = line.find('syntaxerror')
155 173 if pos >= 0:
156 174 e = SyntaxError('input contains "syntaxerror"')
157 175 e.text = line
158 176 e.offset = pos + 1
159 177 raise e
160 178 return lines
161 179
162 180
163 181 class TerminalMagicsTestCase(unittest.TestCase):
164 182 def test_paste_magics_blankline(self):
165 183 """Test that code with a blank line doesn't get split (gh-3246)."""
166 184 ip = get_ipython()
167 185 s = ('def pasted_func(a):\n'
168 186 ' b = a+1\n'
169 187 '\n'
170 188 ' return b')
171 189
172 190 tm = ip.magics_manager.registry['TerminalMagics']
173 191 tm.store_or_execute(s, name=None)
174 192
175 193 self.assertEqual(ip.user_ns['pasted_func'](54), 55)
General Comments 0
You need to be logged in to leave comments. Login now