##// END OF EJS Templates
FIX: Typing annotations (#12683)...
M Bussonnier -
r28949:5d95565e merge
parent child Browse files
Show More
@@ -1,798 +1,799
1 """DEPRECATED: Input handling and transformation machinery.
1 """DEPRECATED: Input handling and transformation machinery.
2
2
3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
4
4
5 The first class in this module, :class:`InputSplitter`, is designed to tell when
5 The first class in this module, :class:`InputSplitter`, is designed to tell when
6 input from a line-oriented frontend is complete and should be executed, and when
6 input from a line-oriented frontend is complete and should be executed, and when
7 the user should be prompted for another line of code instead. The name 'input
7 the user should be prompted for another line of code instead. The name 'input
8 splitter' is largely for historical reasons.
8 splitter' is largely for historical reasons.
9
9
10 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
10 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
11 with full support for the extended IPython syntax (magics, system calls, etc).
11 with full support for the extended IPython syntax (magics, system calls, etc).
12 The code to actually do these transformations is in :mod:`IPython.core.inputtransformer`.
12 The code to actually do these transformations is in :mod:`IPython.core.inputtransformer`.
13 :class:`IPythonInputSplitter` feeds the raw code to the transformers in order
13 :class:`IPythonInputSplitter` feeds the raw code to the transformers in order
14 and stores the results.
14 and stores the results.
15
15
16 For more details, see the class docstrings below.
16 For more details, see the class docstrings below.
17 """
17 """
18
18 from __future__ import annotations
19 from __future__ import annotations
19
20
20 from warnings import warn
21 from warnings import warn
21
22
22 warn('IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2`',
23 warn('IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2`',
23 DeprecationWarning)
24 DeprecationWarning)
24
25
25 # Copyright (c) IPython Development Team.
26 # Copyright (c) IPython Development Team.
26 # Distributed under the terms of the Modified BSD License.
27 # Distributed under the terms of the Modified BSD License.
27 import ast
28 import ast
28 import codeop
29 import codeop
29 import io
30 import io
30 import re
31 import re
31 import sys
32 import sys
32 import tokenize
33 import tokenize
33 import warnings
34 import warnings
34
35
35 from typing import List, Tuple, Union, Optional, TYPE_CHECKING
36 from typing import List, Tuple, Union, Optional, TYPE_CHECKING
36 from types import CodeType
37 from types import CodeType
37
38
38 from IPython.core.inputtransformer import (leading_indent,
39 from IPython.core.inputtransformer import (leading_indent,
39 classic_prompt,
40 classic_prompt,
40 ipy_prompt,
41 ipy_prompt,
41 cellmagic,
42 cellmagic,
42 assemble_logical_lines,
43 assemble_logical_lines,
43 help_end,
44 help_end,
44 escaped_commands,
45 escaped_commands,
45 assign_from_magic,
46 assign_from_magic,
46 assign_from_system,
47 assign_from_system,
47 assemble_python_lines,
48 assemble_python_lines,
48 )
49 )
49 from IPython.utils import tokenutil
50 from IPython.utils import tokenutil
50
51
51 # These are available in this module for backwards compatibility.
52 # These are available in this module for backwards compatibility.
52 from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP,
53 from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP,
53 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,
54 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,
54 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES)
55 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES)
55
56
56 if TYPE_CHECKING:
57 if TYPE_CHECKING:
57 from typing_extensions import Self
58 from typing_extensions import Self
58 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
59 # Utilities
60 # Utilities
60 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
61
62
62 # FIXME: These are general-purpose utilities that later can be moved to the
63 # FIXME: These are general-purpose utilities that later can be moved to the
63 # general ward. Kept here for now because we're being very strict about test
64 # general ward. Kept here for now because we're being very strict about test
64 # coverage with this code, and this lets us ensure that we keep 100% coverage
65 # coverage with this code, and this lets us ensure that we keep 100% coverage
65 # while developing.
66 # while developing.
66
67
67 # compiled regexps for autoindent management
68 # compiled regexps for autoindent management
68 dedent_re = re.compile('|'.join([
69 dedent_re = re.compile('|'.join([
69 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
70 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
70 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
71 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
71 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
72 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
72 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
73 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
73 r'^\s+pass\s*$', # pass (optionally followed by trailing spaces)
74 r'^\s+pass\s*$', # pass (optionally followed by trailing spaces)
74 r'^\s+break\s*$', # break (optionally followed by trailing spaces)
75 r'^\s+break\s*$', # break (optionally followed by trailing spaces)
75 r'^\s+continue\s*$', # continue (optionally followed by trailing spaces)
76 r'^\s+continue\s*$', # continue (optionally followed by trailing spaces)
76 ]))
77 ]))
77 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
78 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
78
79
79 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
80 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
80 # before pure comments
81 # before pure comments
81 comment_line_re = re.compile(r'^\s*\#')
82 comment_line_re = re.compile(r'^\s*\#')
82
83
83
84
84 def num_ini_spaces(s):
85 def num_ini_spaces(s):
85 """Return the number of initial spaces in a string.
86 """Return the number of initial spaces in a string.
86
87
87 Note that tabs are counted as a single space. For now, we do *not* support
88 Note that tabs are counted as a single space. For now, we do *not* support
88 mixing of tabs and spaces in the user's input.
89 mixing of tabs and spaces in the user's input.
89
90
90 Parameters
91 Parameters
91 ----------
92 ----------
92 s : string
93 s : string
93
94
94 Returns
95 Returns
95 -------
96 -------
96 n : int
97 n : int
97 """
98 """
98 warnings.warn(
99 warnings.warn(
99 "`num_ini_spaces` is Pending Deprecation since IPython 8.17."
100 "`num_ini_spaces` is Pending Deprecation since IPython 8.17."
100 "It is considered for removal in in future version. "
101 "It is considered for removal in in future version. "
101 "Please open an issue if you believe it should be kept.",
102 "Please open an issue if you believe it should be kept.",
102 stacklevel=2,
103 stacklevel=2,
103 category=PendingDeprecationWarning,
104 category=PendingDeprecationWarning,
104 )
105 )
105 ini_spaces = ini_spaces_re.match(s)
106 ini_spaces = ini_spaces_re.match(s)
106 if ini_spaces:
107 if ini_spaces:
107 return ini_spaces.end()
108 return ini_spaces.end()
108 else:
109 else:
109 return 0
110 return 0
110
111
111 # Fake token types for partial_tokenize:
112 # Fake token types for partial_tokenize:
112 INCOMPLETE_STRING = tokenize.N_TOKENS
113 INCOMPLETE_STRING = tokenize.N_TOKENS
113 IN_MULTILINE_STATEMENT = tokenize.N_TOKENS + 1
114 IN_MULTILINE_STATEMENT = tokenize.N_TOKENS + 1
114
115
115 # The 2 classes below have the same API as TokenInfo, but don't try to look up
116 # The 2 classes below have the same API as TokenInfo, but don't try to look up
116 # a token type name that they won't find.
117 # a token type name that they won't find.
117 class IncompleteString:
118 class IncompleteString:
118 type = exact_type = INCOMPLETE_STRING
119 type = exact_type = INCOMPLETE_STRING
119 def __init__(self, s, start, end, line):
120 def __init__(self, s, start, end, line):
120 self.s = s
121 self.s = s
121 self.start = start
122 self.start = start
122 self.end = end
123 self.end = end
123 self.line = line
124 self.line = line
124
125
125 class InMultilineStatement:
126 class InMultilineStatement:
126 type = exact_type = IN_MULTILINE_STATEMENT
127 type = exact_type = IN_MULTILINE_STATEMENT
127 def __init__(self, pos, line):
128 def __init__(self, pos, line):
128 self.s = ''
129 self.s = ''
129 self.start = self.end = pos
130 self.start = self.end = pos
130 self.line = line
131 self.line = line
131
132
132 def partial_tokens(s):
133 def partial_tokens(s):
133 """Iterate over tokens from a possibly-incomplete string of code.
134 """Iterate over tokens from a possibly-incomplete string of code.
134
135
135 This adds two special token types: INCOMPLETE_STRING and
136 This adds two special token types: INCOMPLETE_STRING and
136 IN_MULTILINE_STATEMENT. These can only occur as the last token yielded, and
137 IN_MULTILINE_STATEMENT. These can only occur as the last token yielded, and
137 represent the two main ways for code to be incomplete.
138 represent the two main ways for code to be incomplete.
138 """
139 """
139 readline = io.StringIO(s).readline
140 readline = io.StringIO(s).readline
140 token = tokenize.TokenInfo(tokenize.NEWLINE, '', (1, 0), (1, 0), '')
141 token = tokenize.TokenInfo(tokenize.NEWLINE, '', (1, 0), (1, 0), '')
141 try:
142 try:
142 for token in tokenutil.generate_tokens_catch_errors(readline):
143 for token in tokenutil.generate_tokens_catch_errors(readline):
143 yield token
144 yield token
144 except tokenize.TokenError as e:
145 except tokenize.TokenError as e:
145 # catch EOF error
146 # catch EOF error
146 lines = s.splitlines(keepends=True)
147 lines = s.splitlines(keepends=True)
147 end = len(lines), len(lines[-1])
148 end = len(lines), len(lines[-1])
148 if 'multi-line string' in e.args[0]:
149 if 'multi-line string' in e.args[0]:
149 l, c = start = token.end
150 l, c = start = token.end
150 s = lines[l-1][c:] + ''.join(lines[l:])
151 s = lines[l-1][c:] + ''.join(lines[l:])
151 yield IncompleteString(s, start, end, lines[-1])
152 yield IncompleteString(s, start, end, lines[-1])
152 elif 'multi-line statement' in e.args[0]:
153 elif 'multi-line statement' in e.args[0]:
153 yield InMultilineStatement(end, lines[-1])
154 yield InMultilineStatement(end, lines[-1])
154 else:
155 else:
155 raise
156 raise
156
157
157 def find_next_indent(code) -> int:
158 def find_next_indent(code) -> int:
158 """Find the number of spaces for the next line of indentation"""
159 """Find the number of spaces for the next line of indentation"""
159 tokens = list(partial_tokens(code))
160 tokens = list(partial_tokens(code))
160 if tokens[-1].type == tokenize.ENDMARKER:
161 if tokens[-1].type == tokenize.ENDMARKER:
161 tokens.pop()
162 tokens.pop()
162 if not tokens:
163 if not tokens:
163 return 0
164 return 0
164
165
165 while tokens[-1].type in {
166 while tokens[-1].type in {
166 tokenize.DEDENT,
167 tokenize.DEDENT,
167 tokenize.NEWLINE,
168 tokenize.NEWLINE,
168 tokenize.COMMENT,
169 tokenize.COMMENT,
169 tokenize.ERRORTOKEN,
170 tokenize.ERRORTOKEN,
170 }:
171 }:
171 tokens.pop()
172 tokens.pop()
172
173
173 # Starting in Python 3.12, the tokenize module adds implicit newlines at the end
174 # Starting in Python 3.12, the tokenize module adds implicit newlines at the end
174 # of input. We need to remove those if we're in a multiline statement
175 # of input. We need to remove those if we're in a multiline statement
175 if tokens[-1].type == IN_MULTILINE_STATEMENT:
176 if tokens[-1].type == IN_MULTILINE_STATEMENT:
176 while tokens[-2].type in {tokenize.NL}:
177 while tokens[-2].type in {tokenize.NL}:
177 tokens.pop(-2)
178 tokens.pop(-2)
178
179
179
180
180 if tokens[-1].type == INCOMPLETE_STRING:
181 if tokens[-1].type == INCOMPLETE_STRING:
181 # Inside a multiline string
182 # Inside a multiline string
182 return 0
183 return 0
183
184
184 # Find the indents used before
185 # Find the indents used before
185 prev_indents = [0]
186 prev_indents = [0]
186 def _add_indent(n):
187 def _add_indent(n):
187 if n != prev_indents[-1]:
188 if n != prev_indents[-1]:
188 prev_indents.append(n)
189 prev_indents.append(n)
189
190
190 tokiter = iter(tokens)
191 tokiter = iter(tokens)
191 for tok in tokiter:
192 for tok in tokiter:
192 if tok.type in {tokenize.INDENT, tokenize.DEDENT}:
193 if tok.type in {tokenize.INDENT, tokenize.DEDENT}:
193 _add_indent(tok.end[1])
194 _add_indent(tok.end[1])
194 elif (tok.type == tokenize.NL):
195 elif (tok.type == tokenize.NL):
195 try:
196 try:
196 _add_indent(next(tokiter).start[1])
197 _add_indent(next(tokiter).start[1])
197 except StopIteration:
198 except StopIteration:
198 break
199 break
199
200
200 last_indent = prev_indents.pop()
201 last_indent = prev_indents.pop()
201
202
202 # If we've just opened a multiline statement (e.g. 'a = ['), indent more
203 # If we've just opened a multiline statement (e.g. 'a = ['), indent more
203 if tokens[-1].type == IN_MULTILINE_STATEMENT:
204 if tokens[-1].type == IN_MULTILINE_STATEMENT:
204 if tokens[-2].exact_type in {tokenize.LPAR, tokenize.LSQB, tokenize.LBRACE}:
205 if tokens[-2].exact_type in {tokenize.LPAR, tokenize.LSQB, tokenize.LBRACE}:
205 return last_indent + 4
206 return last_indent + 4
206 return last_indent
207 return last_indent
207
208
208 if tokens[-1].exact_type == tokenize.COLON:
209 if tokens[-1].exact_type == tokenize.COLON:
209 # Line ends with colon - indent
210 # Line ends with colon - indent
210 return last_indent + 4
211 return last_indent + 4
211
212
212 if last_indent:
213 if last_indent:
213 # Examine the last line for dedent cues - statements like return or
214 # Examine the last line for dedent cues - statements like return or
214 # raise which normally end a block of code.
215 # raise which normally end a block of code.
215 last_line_starts = 0
216 last_line_starts = 0
216 for i, tok in enumerate(tokens):
217 for i, tok in enumerate(tokens):
217 if tok.type == tokenize.NEWLINE:
218 if tok.type == tokenize.NEWLINE:
218 last_line_starts = i + 1
219 last_line_starts = i + 1
219
220
220 last_line_tokens = tokens[last_line_starts:]
221 last_line_tokens = tokens[last_line_starts:]
221 names = [t.string for t in last_line_tokens if t.type == tokenize.NAME]
222 names = [t.string for t in last_line_tokens if t.type == tokenize.NAME]
222 if names and names[0] in {'raise', 'return', 'pass', 'break', 'continue'}:
223 if names and names[0] in {'raise', 'return', 'pass', 'break', 'continue'}:
223 # Find the most recent indentation less than the current level
224 # Find the most recent indentation less than the current level
224 for indent in reversed(prev_indents):
225 for indent in reversed(prev_indents):
225 if indent < last_indent:
226 if indent < last_indent:
226 return indent
227 return indent
227
228
228 return last_indent
229 return last_indent
229
230
230
231
231 def last_blank(src):
232 def last_blank(src):
232 """Determine if the input source ends in a blank.
233 """Determine if the input source ends in a blank.
233
234
234 A blank is either a newline or a line consisting of whitespace.
235 A blank is either a newline or a line consisting of whitespace.
235
236
236 Parameters
237 Parameters
237 ----------
238 ----------
238 src : string
239 src : string
239 A single or multiline string.
240 A single or multiline string.
240 """
241 """
241 if not src: return False
242 if not src: return False
242 ll = src.splitlines()[-1]
243 ll = src.splitlines()[-1]
243 return (ll == '') or ll.isspace()
244 return (ll == '') or ll.isspace()
244
245
245
246
246 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
247 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
247 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
248 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
248
249
249 def last_two_blanks(src):
250 def last_two_blanks(src):
250 """Determine if the input source ends in two blanks.
251 """Determine if the input source ends in two blanks.
251
252
252 A blank is either a newline or a line consisting of whitespace.
253 A blank is either a newline or a line consisting of whitespace.
253
254
254 Parameters
255 Parameters
255 ----------
256 ----------
256 src : string
257 src : string
257 A single or multiline string.
258 A single or multiline string.
258 """
259 """
259 if not src: return False
260 if not src: return False
260 # The logic here is tricky: I couldn't get a regexp to work and pass all
261 # The logic here is tricky: I couldn't get a regexp to work and pass all
261 # the tests, so I took a different approach: split the source by lines,
262 # the tests, so I took a different approach: split the source by lines,
262 # grab the last two and prepend '###\n' as a stand-in for whatever was in
263 # grab the last two and prepend '###\n' as a stand-in for whatever was in
263 # the body before the last two lines. Then, with that structure, it's
264 # the body before the last two lines. Then, with that structure, it's
264 # possible to analyze with two regexps. Not the most elegant solution, but
265 # possible to analyze with two regexps. Not the most elegant solution, but
265 # it works. If anyone tries to change this logic, make sure to validate
266 # it works. If anyone tries to change this logic, make sure to validate
266 # the whole test suite first!
267 # the whole test suite first!
267 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
268 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
268 return (bool(last_two_blanks_re.match(new_src)) or
269 return (bool(last_two_blanks_re.match(new_src)) or
269 bool(last_two_blanks_re2.match(new_src)) )
270 bool(last_two_blanks_re2.match(new_src)) )
270
271
271
272
272 def remove_comments(src):
273 def remove_comments(src):
273 """Remove all comments from input source.
274 """Remove all comments from input source.
274
275
275 Note: comments are NOT recognized inside of strings!
276 Note: comments are NOT recognized inside of strings!
276
277
277 Parameters
278 Parameters
278 ----------
279 ----------
279 src : string
280 src : string
280 A single or multiline input string.
281 A single or multiline input string.
281
282
282 Returns
283 Returns
283 -------
284 -------
284 String with all Python comments removed.
285 String with all Python comments removed.
285 """
286 """
286
287
287 return re.sub('#.*', '', src)
288 return re.sub('#.*', '', src)
288
289
289
290
290 def get_input_encoding():
291 def get_input_encoding():
291 """Return the default standard input encoding.
292 """Return the default standard input encoding.
292
293
293 If sys.stdin has no encoding, 'ascii' is returned."""
294 If sys.stdin has no encoding, 'ascii' is returned."""
294 # There are strange environments for which sys.stdin.encoding is None. We
295 # There are strange environments for which sys.stdin.encoding is None. We
295 # ensure that a valid encoding is returned.
296 # ensure that a valid encoding is returned.
296 encoding = getattr(sys.stdin, 'encoding', None)
297 encoding = getattr(sys.stdin, 'encoding', None)
297 if encoding is None:
298 if encoding is None:
298 encoding = 'ascii'
299 encoding = 'ascii'
299 return encoding
300 return encoding
300
301
301 #-----------------------------------------------------------------------------
302 #-----------------------------------------------------------------------------
302 # Classes and functions for normal Python syntax handling
303 # Classes and functions for normal Python syntax handling
303 #-----------------------------------------------------------------------------
304 #-----------------------------------------------------------------------------
304
305
305 class InputSplitter(object):
306 class InputSplitter(object):
306 r"""An object that can accumulate lines of Python source before execution.
307 r"""An object that can accumulate lines of Python source before execution.
307
308
308 This object is designed to be fed python source line-by-line, using
309 This object is designed to be fed python source line-by-line, using
309 :meth:`push`. It will return on each push whether the currently pushed
310 :meth:`push`. It will return on each push whether the currently pushed
310 code could be executed already. In addition, it provides a method called
311 code could be executed already. In addition, it provides a method called
311 :meth:`push_accepts_more` that can be used to query whether more input
312 :meth:`push_accepts_more` that can be used to query whether more input
312 can be pushed into a single interactive block.
313 can be pushed into a single interactive block.
313
314
314 This is a simple example of how an interactive terminal-based client can use
315 This is a simple example of how an interactive terminal-based client can use
315 this tool::
316 this tool::
316
317
317 isp = InputSplitter()
318 isp = InputSplitter()
318 while isp.push_accepts_more():
319 while isp.push_accepts_more():
319 indent = ' '*isp.indent_spaces
320 indent = ' '*isp.indent_spaces
320 prompt = '>>> ' + indent
321 prompt = '>>> ' + indent
321 line = indent + raw_input(prompt)
322 line = indent + raw_input(prompt)
322 isp.push(line)
323 isp.push(line)
323 print('Input source was:\n', isp.source_reset())
324 print('Input source was:\n', isp.source_reset())
324 """
325 """
325 # A cache for storing the current indentation
326 # A cache for storing the current indentation
326 # The first value stores the most recently processed source input
327 # The first value stores the most recently processed source input
327 # The second value is the number of spaces for the current indentation
328 # The second value is the number of spaces for the current indentation
328 # If self.source matches the first value, the second value is a valid
329 # If self.source matches the first value, the second value is a valid
329 # current indentation. Otherwise, the cache is invalid and the indentation
330 # current indentation. Otherwise, the cache is invalid and the indentation
330 # must be recalculated.
331 # must be recalculated.
331 _indent_spaces_cache: Union[Tuple[None, None], Tuple[str, int]] = None, None
332 _indent_spaces_cache: Union[Tuple[None, None], Tuple[str, int]] = None, None
332 # String, indicating the default input encoding. It is computed by default
333 # String, indicating the default input encoding. It is computed by default
333 # at initialization time via get_input_encoding(), but it can be reset by a
334 # at initialization time via get_input_encoding(), but it can be reset by a
334 # client with specific knowledge of the encoding.
335 # client with specific knowledge of the encoding.
335 encoding = ''
336 encoding = ''
336 # String where the current full source input is stored, properly encoded.
337 # String where the current full source input is stored, properly encoded.
337 # Reading this attribute is the normal way of querying the currently pushed
338 # Reading this attribute is the normal way of querying the currently pushed
338 # source code, that has been properly encoded.
339 # source code, that has been properly encoded.
339 source: str = ""
340 source: str = ""
340 # Code object corresponding to the current source. It is automatically
341 # Code object corresponding to the current source. It is automatically
341 # synced to the source, so it can be queried at any time to obtain the code
342 # synced to the source, so it can be queried at any time to obtain the code
342 # object; it will be None if the source doesn't compile to valid Python.
343 # object; it will be None if the source doesn't compile to valid Python.
343 code: Optional[CodeType] = None
344 code: Optional[CodeType] = None
344
345
345 # Private attributes
346 # Private attributes
346
347
347 # List with lines of input accumulated so far
348 # List with lines of input accumulated so far
348 _buffer: List[str]
349 _buffer: List[str]
349 # Command compiler
350 # Command compiler
350 _compile: codeop.CommandCompiler
351 _compile: codeop.CommandCompiler
351 # Boolean indicating whether the current block is complete
352 # Boolean indicating whether the current block is complete
352 _is_complete: Optional[bool] = None
353 _is_complete: Optional[bool] = None
353 # Boolean indicating whether the current block has an unrecoverable syntax error
354 # Boolean indicating whether the current block has an unrecoverable syntax error
354 _is_invalid: bool = False
355 _is_invalid: bool = False
355
356
356 def __init__(self) -> None:
357 def __init__(self) -> None:
357 """Create a new InputSplitter instance."""
358 """Create a new InputSplitter instance."""
358 self._buffer = []
359 self._buffer = []
359 self._compile = codeop.CommandCompiler()
360 self._compile = codeop.CommandCompiler()
360 self.encoding = get_input_encoding()
361 self.encoding = get_input_encoding()
361
362
362 def reset(self):
363 def reset(self):
363 """Reset the input buffer and associated state."""
364 """Reset the input buffer and associated state."""
364 self._buffer[:] = []
365 self._buffer[:] = []
365 self.source = ''
366 self.source = ''
366 self.code = None
367 self.code = None
367 self._is_complete = False
368 self._is_complete = False
368 self._is_invalid = False
369 self._is_invalid = False
369
370
370 def source_reset(self):
371 def source_reset(self):
371 """Return the input source and perform a full reset.
372 """Return the input source and perform a full reset.
372 """
373 """
373 out = self.source
374 out = self.source
374 self.reset()
375 self.reset()
375 return out
376 return out
376
377
377 def check_complete(self, source):
378 def check_complete(self, source):
378 """Return whether a block of code is ready to execute, or should be continued
379 """Return whether a block of code is ready to execute, or should be continued
379
380
380 This is a non-stateful API, and will reset the state of this InputSplitter.
381 This is a non-stateful API, and will reset the state of this InputSplitter.
381
382
382 Parameters
383 Parameters
383 ----------
384 ----------
384 source : string
385 source : string
385 Python input code, which can be multiline.
386 Python input code, which can be multiline.
386
387
387 Returns
388 Returns
388 -------
389 -------
389 status : str
390 status : str
390 One of 'complete', 'incomplete', or 'invalid' if source is not a
391 One of 'complete', 'incomplete', or 'invalid' if source is not a
391 prefix of valid code.
392 prefix of valid code.
392 indent_spaces : int or None
393 indent_spaces : int or None
393 The number of spaces by which to indent the next line of code. If
394 The number of spaces by which to indent the next line of code. If
394 status is not 'incomplete', this is None.
395 status is not 'incomplete', this is None.
395 """
396 """
396 self.reset()
397 self.reset()
397 try:
398 try:
398 self.push(source)
399 self.push(source)
399 except SyntaxError:
400 except SyntaxError:
400 # Transformers in IPythonInputSplitter can raise SyntaxError,
401 # Transformers in IPythonInputSplitter can raise SyntaxError,
401 # which push() will not catch.
402 # which push() will not catch.
402 return 'invalid', None
403 return 'invalid', None
403 else:
404 else:
404 if self._is_invalid:
405 if self._is_invalid:
405 return 'invalid', None
406 return 'invalid', None
406 elif self.push_accepts_more():
407 elif self.push_accepts_more():
407 return 'incomplete', self.get_indent_spaces()
408 return 'incomplete', self.get_indent_spaces()
408 else:
409 else:
409 return 'complete', None
410 return 'complete', None
410 finally:
411 finally:
411 self.reset()
412 self.reset()
412
413
413 def push(self, lines:str) -> bool:
414 def push(self, lines:str) -> bool:
414 """Push one or more lines of input.
415 """Push one or more lines of input.
415
416
416 This stores the given lines and returns a status code indicating
417 This stores the given lines and returns a status code indicating
417 whether the code forms a complete Python block or not.
418 whether the code forms a complete Python block or not.
418
419
419 Any exceptions generated in compilation are swallowed, but if an
420 Any exceptions generated in compilation are swallowed, but if an
420 exception was produced, the method returns True.
421 exception was produced, the method returns True.
421
422
422 Parameters
423 Parameters
423 ----------
424 ----------
424 lines : string
425 lines : string
425 One or more lines of Python input.
426 One or more lines of Python input.
426
427
427 Returns
428 Returns
428 -------
429 -------
429 is_complete : boolean
430 is_complete : boolean
430 True if the current input source (the result of the current input
431 True if the current input source (the result of the current input
431 plus prior inputs) forms a complete Python execution block. Note that
432 plus prior inputs) forms a complete Python execution block. Note that
432 this value is also stored as a private attribute (``_is_complete``), so it
433 this value is also stored as a private attribute (``_is_complete``), so it
433 can be queried at any time.
434 can be queried at any time.
434 """
435 """
435 assert isinstance(lines, str)
436 assert isinstance(lines, str)
436 self._store(lines)
437 self._store(lines)
437 source = self.source
438 source = self.source
438
439
439 # Before calling _compile(), reset the code object to None so that if an
440 # Before calling _compile(), reset the code object to None so that if an
440 # exception is raised in compilation, we don't mislead by having
441 # exception is raised in compilation, we don't mislead by having
441 # inconsistent code/source attributes.
442 # inconsistent code/source attributes.
442 self.code, self._is_complete = None, None
443 self.code, self._is_complete = None, None
443 self._is_invalid = False
444 self._is_invalid = False
444
445
445 # Honor termination lines properly
446 # Honor termination lines properly
446 if source.endswith('\\\n'):
447 if source.endswith('\\\n'):
447 return False
448 return False
448
449
449 try:
450 try:
450 with warnings.catch_warnings():
451 with warnings.catch_warnings():
451 warnings.simplefilter('error', SyntaxWarning)
452 warnings.simplefilter('error', SyntaxWarning)
452 self.code = self._compile(source, symbol="exec")
453 self.code = self._compile(source, symbol="exec")
453 # Invalid syntax can produce any of a number of different errors from
454 # Invalid syntax can produce any of a number of different errors from
454 # inside the compiler, so we have to catch them all. Syntax errors
455 # inside the compiler, so we have to catch them all. Syntax errors
455 # immediately produce a 'ready' block, so the invalid Python can be
456 # immediately produce a 'ready' block, so the invalid Python can be
456 # sent to the kernel for evaluation with possible ipython
457 # sent to the kernel for evaluation with possible ipython
457 # special-syntax conversion.
458 # special-syntax conversion.
458 except (SyntaxError, OverflowError, ValueError, TypeError,
459 except (SyntaxError, OverflowError, ValueError, TypeError,
459 MemoryError, SyntaxWarning):
460 MemoryError, SyntaxWarning):
460 self._is_complete = True
461 self._is_complete = True
461 self._is_invalid = True
462 self._is_invalid = True
462 else:
463 else:
463 # Compilation didn't produce any exceptions (though it may not have
464 # Compilation didn't produce any exceptions (though it may not have
464 # given a complete code object)
465 # given a complete code object)
465 self._is_complete = self.code is not None
466 self._is_complete = self.code is not None
466
467
467 return self._is_complete
468 return self._is_complete
468
469
469 def push_accepts_more(self):
470 def push_accepts_more(self):
470 """Return whether a block of interactive input can accept more input.
471 """Return whether a block of interactive input can accept more input.
471
472
472 This method is meant to be used by line-oriented frontends, who need to
473 This method is meant to be used by line-oriented frontends, who need to
473 guess whether a block is complete or not based solely on prior and
474 guess whether a block is complete or not based solely on prior and
474 current input lines. The InputSplitter considers it has a complete
475 current input lines. The InputSplitter considers it has a complete
475 interactive block and will not accept more input when either:
476 interactive block and will not accept more input when either:
476
477
477 * A SyntaxError is raised
478 * A SyntaxError is raised
478
479
479 * The code is complete and consists of a single line or a single
480 * The code is complete and consists of a single line or a single
480 non-compound statement
481 non-compound statement
481
482
482 * The code is complete and has a blank line at the end
483 * The code is complete and has a blank line at the end
483
484
484 If the current input produces a syntax error, this method immediately
485 If the current input produces a syntax error, this method immediately
485 returns False but does *not* raise the syntax error exception, as
486 returns False but does *not* raise the syntax error exception, as
486 typically clients will want to send invalid syntax to an execution
487 typically clients will want to send invalid syntax to an execution
487 backend which might convert the invalid syntax into valid Python via
488 backend which might convert the invalid syntax into valid Python via
488 one of the dynamic IPython mechanisms.
489 one of the dynamic IPython mechanisms.
489 """
490 """
490
491
491 # With incomplete input, unconditionally accept more
492 # With incomplete input, unconditionally accept more
492 # A syntax error also sets _is_complete to True - see push()
493 # A syntax error also sets _is_complete to True - see push()
493 if not self._is_complete:
494 if not self._is_complete:
494 #print("Not complete") # debug
495 #print("Not complete") # debug
495 return True
496 return True
496
497
497 # The user can make any (complete) input execute by leaving a blank line
498 # The user can make any (complete) input execute by leaving a blank line
498 last_line = self.source.splitlines()[-1]
499 last_line = self.source.splitlines()[-1]
499 if (not last_line) or last_line.isspace():
500 if (not last_line) or last_line.isspace():
500 #print("Blank line") # debug
501 #print("Blank line") # debug
501 return False
502 return False
502
503
503 # If there's just a single line or AST node, and we're flush left, as is
504 # If there's just a single line or AST node, and we're flush left, as is
504 # the case after a simple statement such as 'a=1', we want to execute it
505 # the case after a simple statement such as 'a=1', we want to execute it
505 # straight away.
506 # straight away.
506 if self.get_indent_spaces() == 0:
507 if self.get_indent_spaces() == 0:
507 if len(self.source.splitlines()) <= 1:
508 if len(self.source.splitlines()) <= 1:
508 return False
509 return False
509
510
510 try:
511 try:
511 code_ast = ast.parse("".join(self._buffer))
512 code_ast = ast.parse("".join(self._buffer))
512 except Exception:
513 except Exception:
513 #print("Can't parse AST") # debug
514 #print("Can't parse AST") # debug
514 return False
515 return False
515 else:
516 else:
516 if len(code_ast.body) == 1 and \
517 if len(code_ast.body) == 1 and \
517 not hasattr(code_ast.body[0], 'body'):
518 not hasattr(code_ast.body[0], 'body'):
518 #print("Simple statement") # debug
519 #print("Simple statement") # debug
519 return False
520 return False
520
521
521 # General fallback - accept more code
522 # General fallback - accept more code
522 return True
523 return True
523
524
524 def get_indent_spaces(self) -> int:
525 def get_indent_spaces(self) -> int:
525 sourcefor, n = self._indent_spaces_cache
526 sourcefor, n = self._indent_spaces_cache
526 if sourcefor == self.source:
527 if sourcefor == self.source:
527 assert n is not None
528 assert n is not None
528 return n
529 return n
529
530
530 # self.source always has a trailing newline
531 # self.source always has a trailing newline
531 n = find_next_indent(self.source[:-1])
532 n = find_next_indent(self.source[:-1])
532 self._indent_spaces_cache = (self.source, n)
533 self._indent_spaces_cache = (self.source, n)
533 return n
534 return n
534
535
535 # Backwards compatibility. I think all code that used .indent_spaces was
536 # Backwards compatibility. I think all code that used .indent_spaces was
536 # inside IPython, but we can leave this here until IPython 7 in case any
537 # inside IPython, but we can leave this here until IPython 7 in case any
537 # other modules are using it. -TK, November 2017
538 # other modules are using it. -TK, November 2017
538 indent_spaces = property(get_indent_spaces)
539 indent_spaces = property(get_indent_spaces)
539
540
540 def _store(self, lines, buffer=None, store='source'):
541 def _store(self, lines, buffer=None, store='source'):
541 """Store one or more lines of input.
542 """Store one or more lines of input.
542
543
543 If input lines are not newline-terminated, a newline is automatically
544 If input lines are not newline-terminated, a newline is automatically
544 appended."""
545 appended."""
545
546
546 if buffer is None:
547 if buffer is None:
547 buffer = self._buffer
548 buffer = self._buffer
548
549
549 if lines.endswith('\n'):
550 if lines.endswith('\n'):
550 buffer.append(lines)
551 buffer.append(lines)
551 else:
552 else:
552 buffer.append(lines+'\n')
553 buffer.append(lines+'\n')
553 setattr(self, store, self._set_source(buffer))
554 setattr(self, store, self._set_source(buffer))
554
555
555 def _set_source(self, buffer):
556 def _set_source(self, buffer):
556 return u''.join(buffer)
557 return u''.join(buffer)
557
558
558
559
559 class IPythonInputSplitter(InputSplitter):
560 class IPythonInputSplitter(InputSplitter):
560 """An input splitter that recognizes all of IPython's special syntax."""
561 """An input splitter that recognizes all of IPython's special syntax."""
561
562
562 # String with raw, untransformed input.
563 # String with raw, untransformed input.
563 source_raw = ''
564 source_raw = ''
564
565
565 # Flag to track when a transformer has stored input that it hasn't given
566 # Flag to track when a transformer has stored input that it hasn't given
566 # back yet.
567 # back yet.
567 transformer_accumulating = False
568 transformer_accumulating = False
568
569
569 # Flag to track when assemble_python_lines has stored input that it hasn't
570 # Flag to track when assemble_python_lines has stored input that it hasn't
570 # given back yet.
571 # given back yet.
571 within_python_line = False
572 within_python_line = False
572
573
573 # Private attributes
574 # Private attributes
574
575
575 # List with lines of raw input accumulated so far.
576 # List with lines of raw input accumulated so far.
576 _buffer_raw: List[str]
577 _buffer_raw: List[str]
577
578
578 def __init__(self, line_input_checker=True, physical_line_transforms=None,
579 def __init__(self, line_input_checker=True, physical_line_transforms=None,
579 logical_line_transforms=None, python_line_transforms=None):
580 logical_line_transforms=None, python_line_transforms=None):
580 super(IPythonInputSplitter, self).__init__()
581 super(IPythonInputSplitter, self).__init__()
581 self._buffer_raw = []
582 self._buffer_raw = []
582 self._validate = True
583 self._validate = True
583
584
584 if physical_line_transforms is not None:
585 if physical_line_transforms is not None:
585 self.physical_line_transforms = physical_line_transforms
586 self.physical_line_transforms = physical_line_transforms
586 else:
587 else:
587 self.physical_line_transforms = [
588 self.physical_line_transforms = [
588 leading_indent(),
589 leading_indent(),
589 classic_prompt(),
590 classic_prompt(),
590 ipy_prompt(),
591 ipy_prompt(),
591 cellmagic(end_on_blank_line=line_input_checker),
592 cellmagic(end_on_blank_line=line_input_checker),
592 ]
593 ]
593
594
594 self.assemble_logical_lines = assemble_logical_lines()
595 self.assemble_logical_lines = assemble_logical_lines()
595 if logical_line_transforms is not None:
596 if logical_line_transforms is not None:
596 self.logical_line_transforms = logical_line_transforms
597 self.logical_line_transforms = logical_line_transforms
597 else:
598 else:
598 self.logical_line_transforms = [
599 self.logical_line_transforms = [
599 help_end(),
600 help_end(),
600 escaped_commands(),
601 escaped_commands(),
601 assign_from_magic(),
602 assign_from_magic(),
602 assign_from_system(),
603 assign_from_system(),
603 ]
604 ]
604
605
605 self.assemble_python_lines = assemble_python_lines()
606 self.assemble_python_lines = assemble_python_lines()
606 if python_line_transforms is not None:
607 if python_line_transforms is not None:
607 self.python_line_transforms = python_line_transforms
608 self.python_line_transforms = python_line_transforms
608 else:
609 else:
609 # We don't use any of these at present
610 # We don't use any of these at present
610 self.python_line_transforms = []
611 self.python_line_transforms = []
611
612
612 @property
613 @property
613 def transforms(self):
614 def transforms(self):
614 "Quick access to all transformers."
615 "Quick access to all transformers."
615 return self.physical_line_transforms + \
616 return self.physical_line_transforms + \
616 [self.assemble_logical_lines] + self.logical_line_transforms + \
617 [self.assemble_logical_lines] + self.logical_line_transforms + \
617 [self.assemble_python_lines] + self.python_line_transforms
618 [self.assemble_python_lines] + self.python_line_transforms
618
619
619 @property
620 @property
620 def transforms_in_use(self):
621 def transforms_in_use(self):
621 """Transformers, excluding logical line transformers if we're in a
622 """Transformers, excluding logical line transformers if we're in a
622 Python line."""
623 Python line."""
623 t = self.physical_line_transforms[:]
624 t = self.physical_line_transforms[:]
624 if not self.within_python_line:
625 if not self.within_python_line:
625 t += [self.assemble_logical_lines] + self.logical_line_transforms
626 t += [self.assemble_logical_lines] + self.logical_line_transforms
626 return t + [self.assemble_python_lines] + self.python_line_transforms
627 return t + [self.assemble_python_lines] + self.python_line_transforms
627
628
628 def reset(self):
629 def reset(self):
629 """Reset the input buffer and associated state."""
630 """Reset the input buffer and associated state."""
630 super(IPythonInputSplitter, self).reset()
631 super(IPythonInputSplitter, self).reset()
631 self._buffer_raw[:] = []
632 self._buffer_raw[:] = []
632 self.source_raw = ''
633 self.source_raw = ''
633 self.transformer_accumulating = False
634 self.transformer_accumulating = False
634 self.within_python_line = False
635 self.within_python_line = False
635
636
636 for t in self.transforms:
637 for t in self.transforms:
637 try:
638 try:
638 t.reset()
639 t.reset()
639 except SyntaxError:
640 except SyntaxError:
640 # Nothing that calls reset() expects to handle transformer
641 # Nothing that calls reset() expects to handle transformer
641 # errors
642 # errors
642 pass
643 pass
643
644
644 def flush_transformers(self: Self):
645 def flush_transformers(self: Self):
645 def _flush(transform, outs: List[str]):
646 def _flush(transform, outs: List[str]):
646 """yield transformed lines
647 """yield transformed lines
647
648
648 always strings, never None
649 always strings, never None
649
650
650 transform: the current transform
651 transform: the current transform
651 outs: an iterable of previously transformed inputs.
652 outs: an iterable of previously transformed inputs.
652 Each may be multiline, which will be passed
653 Each may be multiline, which will be passed
653 one line at a time to transform.
654 one line at a time to transform.
654 """
655 """
655 for out in outs:
656 for out in outs:
656 for line in out.splitlines():
657 for line in out.splitlines():
657 # push one line at a time
658 # push one line at a time
658 tmp = transform.push(line)
659 tmp = transform.push(line)
659 if tmp is not None:
660 if tmp is not None:
660 yield tmp
661 yield tmp
661
662
662 # reset the transform
663 # reset the transform
663 tmp = transform.reset()
664 tmp = transform.reset()
664 if tmp is not None:
665 if tmp is not None:
665 yield tmp
666 yield tmp
666
667
667 out: List[str] = []
668 out: List[str] = []
668 for t in self.transforms_in_use:
669 for t in self.transforms_in_use:
669 out = _flush(t, out)
670 out = _flush(t, out)
670
671
671 out = list(out)
672 out = list(out)
672 if out:
673 if out:
673 self._store('\n'.join(out))
674 self._store('\n'.join(out))
674
675
675 def raw_reset(self):
676 def raw_reset(self):
676 """Return raw input only and perform a full reset.
677 """Return raw input only and perform a full reset.
677 """
678 """
678 out = self.source_raw
679 out = self.source_raw
679 self.reset()
680 self.reset()
680 return out
681 return out
681
682
682 def source_reset(self):
683 def source_reset(self):
683 try:
684 try:
684 self.flush_transformers()
685 self.flush_transformers()
685 return self.source
686 return self.source
686 finally:
687 finally:
687 self.reset()
688 self.reset()
688
689
689 def push_accepts_more(self):
690 def push_accepts_more(self):
690 if self.transformer_accumulating:
691 if self.transformer_accumulating:
691 return True
692 return True
692 else:
693 else:
693 return super(IPythonInputSplitter, self).push_accepts_more()
694 return super(IPythonInputSplitter, self).push_accepts_more()
694
695
695 def transform_cell(self, cell):
696 def transform_cell(self, cell):
696 """Process and translate a cell of input.
697 """Process and translate a cell of input.
697 """
698 """
698 self.reset()
699 self.reset()
699 try:
700 try:
700 self.push(cell)
701 self.push(cell)
701 self.flush_transformers()
702 self.flush_transformers()
702 return self.source
703 return self.source
703 finally:
704 finally:
704 self.reset()
705 self.reset()
705
706
706 def push(self, lines:str) -> bool:
707 def push(self, lines:str) -> bool:
707 """Push one or more lines of IPython input.
708 """Push one or more lines of IPython input.
708
709
709 This stores the given lines and returns a status code indicating
710 This stores the given lines and returns a status code indicating
710 whether the code forms a complete Python block or not, after processing
711 whether the code forms a complete Python block or not, after processing
711 all input lines for special IPython syntax.
712 all input lines for special IPython syntax.
712
713
713 Any exceptions generated in compilation are swallowed, but if an
714 Any exceptions generated in compilation are swallowed, but if an
714 exception was produced, the method returns True.
715 exception was produced, the method returns True.
715
716
716 Parameters
717 Parameters
717 ----------
718 ----------
718 lines : string
719 lines : string
719 One or more lines of Python input.
720 One or more lines of Python input.
720
721
721 Returns
722 Returns
722 -------
723 -------
723 is_complete : boolean
724 is_complete : boolean
724 True if the current input source (the result of the current input
725 True if the current input source (the result of the current input
725 plus prior inputs) forms a complete Python execution block. Note that
726 plus prior inputs) forms a complete Python execution block. Note that
726 this value is also stored as a private attribute (_is_complete), so it
727 this value is also stored as a private attribute (_is_complete), so it
727 can be queried at any time.
728 can be queried at any time.
728 """
729 """
729 assert isinstance(lines, str)
730 assert isinstance(lines, str)
730 # We must ensure all input is pure unicode
731 # We must ensure all input is pure unicode
731 # ''.splitlines() --> [], but we need to push the empty line to transformers
732 # ''.splitlines() --> [], but we need to push the empty line to transformers
732 lines_list = lines.splitlines()
733 lines_list = lines.splitlines()
733 if not lines_list:
734 if not lines_list:
734 lines_list = ['']
735 lines_list = ['']
735
736
736 # Store raw source before applying any transformations to it. Note
737 # Store raw source before applying any transformations to it. Note
737 # that this must be done *after* the reset() call that would otherwise
738 # that this must be done *after* the reset() call that would otherwise
738 # flush the buffer.
739 # flush the buffer.
739 self._store(lines, self._buffer_raw, 'source_raw')
740 self._store(lines, self._buffer_raw, 'source_raw')
740
741
741 transformed_lines_list = []
742 transformed_lines_list = []
742 for line in lines_list:
743 for line in lines_list:
743 transformed = self._transform_line(line)
744 transformed = self._transform_line(line)
744 if transformed is not None:
745 if transformed is not None:
745 transformed_lines_list.append(transformed)
746 transformed_lines_list.append(transformed)
746
747
747 if transformed_lines_list:
748 if transformed_lines_list:
748 transformed_lines = '\n'.join(transformed_lines_list)
749 transformed_lines = '\n'.join(transformed_lines_list)
749 return super(IPythonInputSplitter, self).push(transformed_lines)
750 return super(IPythonInputSplitter, self).push(transformed_lines)
750 else:
751 else:
751 # Got nothing back from transformers - they must be waiting for
752 # Got nothing back from transformers - they must be waiting for
752 # more input.
753 # more input.
753 return False
754 return False
754
755
755 def _transform_line(self, line):
756 def _transform_line(self, line):
756 """Push a line of input code through the various transformers.
757 """Push a line of input code through the various transformers.
757
758
758 Returns any output from the transformers, or None if a transformer
759 Returns any output from the transformers, or None if a transformer
759 is accumulating lines.
760 is accumulating lines.
760
761
761 Sets self.transformer_accumulating as a side effect.
762 Sets self.transformer_accumulating as a side effect.
762 """
763 """
763 def _accumulating(dbg):
764 def _accumulating(dbg):
764 #print(dbg)
765 #print(dbg)
765 self.transformer_accumulating = True
766 self.transformer_accumulating = True
766 return None
767 return None
767
768
768 for transformer in self.physical_line_transforms:
769 for transformer in self.physical_line_transforms:
769 line = transformer.push(line)
770 line = transformer.push(line)
770 if line is None:
771 if line is None:
771 return _accumulating(transformer)
772 return _accumulating(transformer)
772
773
773 if not self.within_python_line:
774 if not self.within_python_line:
774 line = self.assemble_logical_lines.push(line)
775 line = self.assemble_logical_lines.push(line)
775 if line is None:
776 if line is None:
776 return _accumulating('acc logical line')
777 return _accumulating('acc logical line')
777
778
778 for transformer in self.logical_line_transforms:
779 for transformer in self.logical_line_transforms:
779 line = transformer.push(line)
780 line = transformer.push(line)
780 if line is None:
781 if line is None:
781 return _accumulating(transformer)
782 return _accumulating(transformer)
782
783
783 line = self.assemble_python_lines.push(line)
784 line = self.assemble_python_lines.push(line)
784 if line is None:
785 if line is None:
785 self.within_python_line = True
786 self.within_python_line = True
786 return _accumulating('acc python line')
787 return _accumulating('acc python line')
787 else:
788 else:
788 self.within_python_line = False
789 self.within_python_line = False
789
790
790 for transformer in self.python_line_transforms:
791 for transformer in self.python_line_transforms:
791 line = transformer.push(line)
792 line = transformer.push(line)
792 if line is None:
793 if line is None:
793 return _accumulating(transformer)
794 return _accumulating(transformer)
794
795
795 #print("transformers clear") #debug
796 #print("transformers clear") #debug
796 self.transformer_accumulating = False
797 self.transformer_accumulating = False
797 return line
798 return line
798
799
@@ -1,447 +1,448
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2
2
3 Line-based transformers are the simpler ones; token-based transformers are
3 Line-based transformers are the simpler ones; token-based transformers are
4 more complex. See test_inputtransformer2_line for tests for line-based
4 more complex. See test_inputtransformer2_line for tests for line-based
5 transformations.
5 transformations.
6 """
6 """
7
7 import platform
8 import platform
8 import string
9 import string
9 import sys
10 import sys
10 from textwrap import dedent
11 from textwrap import dedent
11
12
12 import pytest
13 import pytest
13
14
14 from IPython.core import inputtransformer2 as ipt2
15 from IPython.core import inputtransformer2 as ipt2
15 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
16 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
16
17
17 MULTILINE_MAGIC = (
18 MULTILINE_MAGIC = (
18 """\
19 """\
19 a = f()
20 a = f()
20 %foo \\
21 %foo \\
21 bar
22 bar
22 g()
23 g()
23 """.splitlines(
24 """.splitlines(
24 keepends=True
25 keepends=True
25 ),
26 ),
26 (2, 0),
27 (2, 0),
27 """\
28 """\
28 a = f()
29 a = f()
29 get_ipython().run_line_magic('foo', ' bar')
30 get_ipython().run_line_magic('foo', ' bar')
30 g()
31 g()
31 """.splitlines(
32 """.splitlines(
32 keepends=True
33 keepends=True
33 ),
34 ),
34 )
35 )
35
36
36 INDENTED_MAGIC = (
37 INDENTED_MAGIC = (
37 """\
38 """\
38 for a in range(5):
39 for a in range(5):
39 %ls
40 %ls
40 """.splitlines(
41 """.splitlines(
41 keepends=True
42 keepends=True
42 ),
43 ),
43 (2, 4),
44 (2, 4),
44 """\
45 """\
45 for a in range(5):
46 for a in range(5):
46 get_ipython().run_line_magic('ls', '')
47 get_ipython().run_line_magic('ls', '')
47 """.splitlines(
48 """.splitlines(
48 keepends=True
49 keepends=True
49 ),
50 ),
50 )
51 )
51
52
52 CRLF_MAGIC = (
53 CRLF_MAGIC = (
53 ["a = f()\n", "%ls\r\n", "g()\n"],
54 ["a = f()\n", "%ls\r\n", "g()\n"],
54 (2, 0),
55 (2, 0),
55 ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
56 ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
56 )
57 )
57
58
58 MULTILINE_MAGIC_ASSIGN = (
59 MULTILINE_MAGIC_ASSIGN = (
59 """\
60 """\
60 a = f()
61 a = f()
61 b = %foo \\
62 b = %foo \\
62 bar
63 bar
63 g()
64 g()
64 """.splitlines(
65 """.splitlines(
65 keepends=True
66 keepends=True
66 ),
67 ),
67 (2, 4),
68 (2, 4),
68 """\
69 """\
69 a = f()
70 a = f()
70 b = get_ipython().run_line_magic('foo', ' bar')
71 b = get_ipython().run_line_magic('foo', ' bar')
71 g()
72 g()
72 """.splitlines(
73 """.splitlines(
73 keepends=True
74 keepends=True
74 ),
75 ),
75 )
76 )
76
77
77 MULTILINE_SYSTEM_ASSIGN = ("""\
78 MULTILINE_SYSTEM_ASSIGN = ("""\
78 a = f()
79 a = f()
79 b = !foo \\
80 b = !foo \\
80 bar
81 bar
81 g()
82 g()
82 """.splitlines(keepends=True), (2, 4), """\
83 """.splitlines(keepends=True), (2, 4), """\
83 a = f()
84 a = f()
84 b = get_ipython().getoutput('foo bar')
85 b = get_ipython().getoutput('foo bar')
85 g()
86 g()
86 """.splitlines(keepends=True))
87 """.splitlines(keepends=True))
87
88
88 #####
89 #####
89
90
90 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = (
91 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = (
91 """\
92 """\
92 def test():
93 def test():
93 for i in range(1):
94 for i in range(1):
94 print(i)
95 print(i)
95 res =! ls
96 res =! ls
96 """.splitlines(
97 """.splitlines(
97 keepends=True
98 keepends=True
98 ),
99 ),
99 (4, 7),
100 (4, 7),
100 """\
101 """\
101 def test():
102 def test():
102 for i in range(1):
103 for i in range(1):
103 print(i)
104 print(i)
104 res =get_ipython().getoutput(\' ls\')
105 res =get_ipython().getoutput(\' ls\')
105 """.splitlines(
106 """.splitlines(
106 keepends=True
107 keepends=True
107 ),
108 ),
108 )
109 )
109
110
110 ######
111 ######
111
112
112 AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])
113 AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])
113
114
114 AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])
115 AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])
115
116
116 AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])
117 AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])
117
118
118 SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])
119 SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])
119
120
120 DETAILED_HELP = (
121 DETAILED_HELP = (
121 ["foo??\n"],
122 ["foo??\n"],
122 (1, 0),
123 (1, 0),
123 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
124 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
124 )
125 )
125
126
126 MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])
127 MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])
127
128
128 HELP_IN_EXPR = (
129 HELP_IN_EXPR = (
129 ["a = b + c?\n"],
130 ["a = b + c?\n"],
130 (1, 0),
131 (1, 0),
131 ["get_ipython().run_line_magic('pinfo', 'c')\n"],
132 ["get_ipython().run_line_magic('pinfo', 'c')\n"],
132 )
133 )
133
134
134 HELP_CONTINUED_LINE = (
135 HELP_CONTINUED_LINE = (
135 """\
136 """\
136 a = \\
137 a = \\
137 zip?
138 zip?
138 """.splitlines(
139 """.splitlines(
139 keepends=True
140 keepends=True
140 ),
141 ),
141 (1, 0),
142 (1, 0),
142 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
143 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
143 )
144 )
144
145
145 HELP_MULTILINE = (
146 HELP_MULTILINE = (
146 """\
147 """\
147 (a,
148 (a,
148 b) = zip?
149 b) = zip?
149 """.splitlines(
150 """.splitlines(
150 keepends=True
151 keepends=True
151 ),
152 ),
152 (1, 0),
153 (1, 0),
153 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
154 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
154 )
155 )
155
156
156 HELP_UNICODE = (
157 HELP_UNICODE = (
157 ["Ο€.foo?\n"],
158 ["Ο€.foo?\n"],
158 (1, 0),
159 (1, 0),
159 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"],
160 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"],
160 )
161 )
161
162
162
163
163 def null_cleanup_transformer(lines):
164 def null_cleanup_transformer(lines):
164 """
165 """
165 A cleanup transform that returns an empty list.
166 A cleanup transform that returns an empty list.
166 """
167 """
167 return []
168 return []
168
169
169
170
170 def test_check_make_token_by_line_never_ends_empty():
171 def test_check_make_token_by_line_never_ends_empty():
171 """
172 """
172 Check that not sequence of single or double characters ends up leading to en empty list of tokens
173 Check that not sequence of single or double characters ends up leading to en empty list of tokens
173 """
174 """
174 from string import printable
175 from string import printable
175
176
176 for c in printable:
177 for c in printable:
177 assert make_tokens_by_line(c)[-1] != []
178 assert make_tokens_by_line(c)[-1] != []
178 for k in printable:
179 for k in printable:
179 assert make_tokens_by_line(c + k)[-1] != []
180 assert make_tokens_by_line(c + k)[-1] != []
180
181
181
182
182 def check_find(transformer, case, match=True):
183 def check_find(transformer, case, match=True):
183 sample, expected_start, _ = case
184 sample, expected_start, _ = case
184 tbl = make_tokens_by_line(sample)
185 tbl = make_tokens_by_line(sample)
185 res = transformer.find(tbl)
186 res = transformer.find(tbl)
186 if match:
187 if match:
187 # start_line is stored 0-indexed, expected values are 1-indexed
188 # start_line is stored 0-indexed, expected values are 1-indexed
188 assert (res.start_line + 1, res.start_col) == expected_start
189 assert (res.start_line + 1, res.start_col) == expected_start
189 return res
190 return res
190 else:
191 else:
191 assert res is None
192 assert res is None
192
193
193
194
194 def check_transform(transformer_cls, case):
195 def check_transform(transformer_cls, case):
195 lines, start, expected = case
196 lines, start, expected = case
196 transformer = transformer_cls(start)
197 transformer = transformer_cls(start)
197 assert transformer.transform(lines) == expected
198 assert transformer.transform(lines) == expected
198
199
199
200
200 def test_continued_line():
201 def test_continued_line():
201 lines = MULTILINE_MAGIC_ASSIGN[0]
202 lines = MULTILINE_MAGIC_ASSIGN[0]
202 assert ipt2.find_end_of_continued_line(lines, 1) == 2
203 assert ipt2.find_end_of_continued_line(lines, 1) == 2
203
204
204 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar"
205 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar"
205
206
206
207
207 def test_find_assign_magic():
208 def test_find_assign_magic():
208 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
209 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
209 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
210 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
210 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
211 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
211
212
212
213
213 def test_transform_assign_magic():
214 def test_transform_assign_magic():
214 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
215 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
215
216
216
217
217 def test_find_assign_system():
218 def test_find_assign_system():
218 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
219 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
219 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
220 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
220 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
221 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
221 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
222 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
222 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
223 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
223
224
224
225
225 def test_transform_assign_system():
226 def test_transform_assign_system():
226 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
227 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
227 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
228 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
228
229
229
230
230 def test_find_magic_escape():
231 def test_find_magic_escape():
231 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
232 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
232 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
233 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
233 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
234 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
234
235
235
236
236 def test_transform_magic_escape():
237 def test_transform_magic_escape():
237 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
238 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
238 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
239 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
239 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
240 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
240
241
241
242
242 def test_find_autocalls():
243 def test_find_autocalls():
243 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
244 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
244 print("Testing %r" % case[0])
245 print("Testing %r" % case[0])
245 check_find(ipt2.EscapedCommand, case)
246 check_find(ipt2.EscapedCommand, case)
246
247
247
248
248 def test_transform_autocall():
249 def test_transform_autocall():
249 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
250 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
250 print("Testing %r" % case[0])
251 print("Testing %r" % case[0])
251 check_transform(ipt2.EscapedCommand, case)
252 check_transform(ipt2.EscapedCommand, case)
252
253
253
254
254 def test_find_help():
255 def test_find_help():
255 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
256 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
256 check_find(ipt2.HelpEnd, case)
257 check_find(ipt2.HelpEnd, case)
257
258
258 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
259 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
259 assert tf.q_line == 1
260 assert tf.q_line == 1
260 assert tf.q_col == 3
261 assert tf.q_col == 3
261
262
262 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
263 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
263 assert tf.q_line == 1
264 assert tf.q_line == 1
264 assert tf.q_col == 8
265 assert tf.q_col == 8
265
266
266 # ? in a comment does not trigger help
267 # ? in a comment does not trigger help
267 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
268 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
268 # Nor in a string
269 # Nor in a string
269 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
270 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
270
271
271
272
272 def test_transform_help():
273 def test_transform_help():
273 tf = ipt2.HelpEnd((1, 0), (1, 9))
274 tf = ipt2.HelpEnd((1, 0), (1, 9))
274 assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]
275 assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]
275
276
276 tf = ipt2.HelpEnd((1, 0), (2, 3))
277 tf = ipt2.HelpEnd((1, 0), (2, 3))
277 assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]
278 assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]
278
279
279 tf = ipt2.HelpEnd((1, 0), (2, 8))
280 tf = ipt2.HelpEnd((1, 0), (2, 8))
280 assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]
281 assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]
281
282
282 tf = ipt2.HelpEnd((1, 0), (1, 0))
283 tf = ipt2.HelpEnd((1, 0), (1, 0))
283 assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]
284 assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]
284
285
285
286
286 def test_find_assign_op_dedent():
287 def test_find_assign_op_dedent():
287 """
288 """
288 be careful that empty token like dedent are not counted as parens
289 be careful that empty token like dedent are not counted as parens
289 """
290 """
290
291
291 class Tk:
292 class Tk:
292 def __init__(self, s):
293 def __init__(self, s):
293 self.string = s
294 self.string = s
294
295
295 assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
296 assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
296 assert (
297 assert (
297 _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
298 _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
298 )
299 )
299
300
300
301
301 extra_closing_paren_param = (
302 extra_closing_paren_param = (
302 pytest.param("(\n))", "invalid", None)
303 pytest.param("(\n))", "invalid", None)
303 if sys.version_info >= (3, 12)
304 if sys.version_info >= (3, 12)
304 else pytest.param("(\n))", "incomplete", 0)
305 else pytest.param("(\n))", "incomplete", 0)
305 )
306 )
306 examples = [
307 examples = [
307 pytest.param("a = 1", "complete", None),
308 pytest.param("a = 1", "complete", None),
308 pytest.param("for a in range(5):", "incomplete", 4),
309 pytest.param("for a in range(5):", "incomplete", 4),
309 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
310 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
310 pytest.param("raise = 2", "invalid", None),
311 pytest.param("raise = 2", "invalid", None),
311 pytest.param("a = [1,\n2,", "incomplete", 0),
312 pytest.param("a = [1,\n2,", "incomplete", 0),
312 extra_closing_paren_param,
313 extra_closing_paren_param,
313 pytest.param("\\\r\n", "incomplete", 0),
314 pytest.param("\\\r\n", "incomplete", 0),
314 pytest.param("a = '''\n hi", "incomplete", 3),
315 pytest.param("a = '''\n hi", "incomplete", 3),
315 pytest.param("def a():\n x=1\n global x", "invalid", None),
316 pytest.param("def a():\n x=1\n global x", "invalid", None),
316 pytest.param(
317 pytest.param(
317 "a \\ ",
318 "a \\ ",
318 "invalid",
319 "invalid",
319 None,
320 None,
320 marks=pytest.mark.xfail(
321 marks=pytest.mark.xfail(
321 reason="Bug in python 3.9.8 – bpo 45738",
322 reason="Bug in python 3.9.8 – bpo 45738",
322 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
323 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
323 raises=SystemError,
324 raises=SystemError,
324 strict=True,
325 strict=True,
325 ),
326 ),
326 ), # Nothing allowed after backslash,
327 ), # Nothing allowed after backslash,
327 pytest.param("1\\\n+2", "complete", None),
328 pytest.param("1\\\n+2", "complete", None),
328 ]
329 ]
329
330
330
331
331 @pytest.mark.parametrize("code, expected, number", examples)
332 @pytest.mark.parametrize("code, expected, number", examples)
332 def test_check_complete_param(code, expected, number):
333 def test_check_complete_param(code, expected, number):
333 cc = ipt2.TransformerManager().check_complete
334 cc = ipt2.TransformerManager().check_complete
334 assert cc(code) == (expected, number)
335 assert cc(code) == (expected, number)
335
336
336
337
337 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
338 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
338 @pytest.mark.xfail(
339 @pytest.mark.xfail(
339 reason="Bug in python 3.9.8 – bpo 45738",
340 reason="Bug in python 3.9.8 – bpo 45738",
340 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
341 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
341 raises=SystemError,
342 raises=SystemError,
342 strict=True,
343 strict=True,
343 )
344 )
344 def test_check_complete():
345 def test_check_complete():
345 cc = ipt2.TransformerManager().check_complete
346 cc = ipt2.TransformerManager().check_complete
346
347
347 example = dedent(
348 example = dedent(
348 """
349 """
349 if True:
350 if True:
350 a=1"""
351 a=1"""
351 )
352 )
352
353
353 assert cc(example) == ("incomplete", 4)
354 assert cc(example) == ("incomplete", 4)
354 assert cc(example + "\n") == ("complete", None)
355 assert cc(example + "\n") == ("complete", None)
355 assert cc(example + "\n ") == ("complete", None)
356 assert cc(example + "\n ") == ("complete", None)
356
357
357 # no need to loop on all the letters/numbers.
358 # no need to loop on all the letters/numbers.
358 short = "12abAB" + string.printable[62:]
359 short = "12abAB" + string.printable[62:]
359 for c in short:
360 for c in short:
360 # test does not raise:
361 # test does not raise:
361 cc(c)
362 cc(c)
362 for k in short:
363 for k in short:
363 cc(c + k)
364 cc(c + k)
364
365
365 assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2)
366 assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2)
366
367
367
368
368 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
369 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
369 @pytest.mark.parametrize(
370 @pytest.mark.parametrize(
370 "value, expected",
371 "value, expected",
371 [
372 [
372 ('''def foo():\n """''', ("incomplete", 4)),
373 ('''def foo():\n """''', ("incomplete", 4)),
373 ("""async with example:\n pass""", ("incomplete", 4)),
374 ("""async with example:\n pass""", ("incomplete", 4)),
374 ("""async with example:\n pass\n """, ("complete", None)),
375 ("""async with example:\n pass\n """, ("complete", None)),
375 ],
376 ],
376 )
377 )
377 def test_check_complete_II(value, expected):
378 def test_check_complete_II(value, expected):
378 """
379 """
379 Test that multiple line strings are properly handled.
380 Test that multiple line strings are properly handled.
380
381
381 Separate test function for convenience
382 Separate test function for convenience
382
383
383 """
384 """
384 cc = ipt2.TransformerManager().check_complete
385 cc = ipt2.TransformerManager().check_complete
385 assert cc(value) == expected
386 assert cc(value) == expected
386
387
387
388
388 @pytest.mark.parametrize(
389 @pytest.mark.parametrize(
389 "value, expected",
390 "value, expected",
390 [
391 [
391 (")", ("invalid", None)),
392 (")", ("invalid", None)),
392 ("]", ("invalid", None)),
393 ("]", ("invalid", None)),
393 ("}", ("invalid", None)),
394 ("}", ("invalid", None)),
394 (")(", ("invalid", None)),
395 (")(", ("invalid", None)),
395 ("][", ("invalid", None)),
396 ("][", ("invalid", None)),
396 ("}{", ("invalid", None)),
397 ("}{", ("invalid", None)),
397 ("]()(", ("invalid", None)),
398 ("]()(", ("invalid", None)),
398 ("())(", ("invalid", None)),
399 ("())(", ("invalid", None)),
399 (")[](", ("invalid", None)),
400 (")[](", ("invalid", None)),
400 ("()](", ("invalid", None)),
401 ("()](", ("invalid", None)),
401 ],
402 ],
402 )
403 )
403 def test_check_complete_invalidates_sunken_brackets(value, expected):
404 def test_check_complete_invalidates_sunken_brackets(value, expected):
404 """
405 """
405 Test that a single line with more closing brackets than the opening ones is
406 Test that a single line with more closing brackets than the opening ones is
406 interpreted as invalid
407 interpreted as invalid
407 """
408 """
408 cc = ipt2.TransformerManager().check_complete
409 cc = ipt2.TransformerManager().check_complete
409 assert cc(value) == expected
410 assert cc(value) == expected
410
411
411
412
412 def test_null_cleanup_transformer():
413 def test_null_cleanup_transformer():
413 manager = ipt2.TransformerManager()
414 manager = ipt2.TransformerManager()
414 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
415 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
415 assert manager.transform_cell("") == ""
416 assert manager.transform_cell("") == ""
416
417
417
418
418 def test_side_effects_I():
419 def test_side_effects_I():
419 count = 0
420 count = 0
420
421
421 def counter(lines):
422 def counter(lines):
422 nonlocal count
423 nonlocal count
423 count += 1
424 count += 1
424 return lines
425 return lines
425
426
426 counter.has_side_effects = True
427 counter.has_side_effects = True
427
428
428 manager = ipt2.TransformerManager()
429 manager = ipt2.TransformerManager()
429 manager.cleanup_transforms.insert(0, counter)
430 manager.cleanup_transforms.insert(0, counter)
430 assert manager.check_complete("a=1\n") == ("complete", None)
431 assert manager.check_complete("a=1\n") == ("complete", None)
431 assert count == 0
432 assert count == 0
432
433
433
434
434 def test_side_effects_II():
435 def test_side_effects_II():
435 count = 0
436 count = 0
436
437
437 def counter(lines):
438 def counter(lines):
438 nonlocal count
439 nonlocal count
439 count += 1
440 count += 1
440 return lines
441 return lines
441
442
442 counter.has_side_effects = True
443 counter.has_side_effects = True
443
444
444 manager = ipt2.TransformerManager()
445 manager = ipt2.TransformerManager()
445 manager.line_transforms.insert(0, counter)
446 manager.line_transforms.insert(0, counter)
446 assert manager.check_complete("b=1\n") == ("complete", None)
447 assert manager.check_complete("b=1\n") == ("complete", None)
447 assert count == 0
448 assert count == 0
@@ -1,200 +1,202
1 import errno
1 import errno
2 import os
2 import os
3 import shutil
3 import shutil
4 import tempfile
4 import tempfile
5 import warnings
5 import warnings
6 from unittest.mock import patch
6 from unittest.mock import patch
7
7
8 from tempfile import TemporaryDirectory
8 from tempfile import TemporaryDirectory
9 from testpath import assert_isdir, assert_isfile, modified_env
9 from testpath import assert_isdir, assert_isfile, modified_env
10
10
11 from IPython import paths
11 from IPython import paths
12 from IPython.testing.decorators import skip_win32
12 from IPython.testing.decorators import skip_win32
13
13
14 TMP_TEST_DIR = os.path.realpath(tempfile.mkdtemp())
14 TMP_TEST_DIR = os.path.realpath(tempfile.mkdtemp())
15 HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir")
15 HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir")
16 XDG_TEST_DIR = os.path.join(HOME_TEST_DIR, "xdg_test_dir")
16 XDG_TEST_DIR = os.path.join(HOME_TEST_DIR, "xdg_test_dir")
17 XDG_CACHE_DIR = os.path.join(HOME_TEST_DIR, "xdg_cache_dir")
17 XDG_CACHE_DIR = os.path.join(HOME_TEST_DIR, "xdg_cache_dir")
18 IP_TEST_DIR = os.path.join(HOME_TEST_DIR,'.ipython')
18 IP_TEST_DIR = os.path.join(HOME_TEST_DIR,'.ipython')
19
19
20 def setup_module():
20 def setup_module():
21 """Setup testenvironment for the module:
21 """Setup testenvironment for the module:
22
22
23 - Adds dummy home dir tree
23 - Adds dummy home dir tree
24 """
24 """
25 # Do not mask exceptions here. In particular, catching WindowsError is a
25 # Do not mask exceptions here. In particular, catching WindowsError is a
26 # problem because that exception is only defined on Windows...
26 # problem because that exception is only defined on Windows...
27 os.makedirs(IP_TEST_DIR)
27 os.makedirs(IP_TEST_DIR)
28 os.makedirs(os.path.join(XDG_TEST_DIR, 'ipython'))
28 os.makedirs(os.path.join(XDG_TEST_DIR, 'ipython'))
29 os.makedirs(os.path.join(XDG_CACHE_DIR, 'ipython'))
29 os.makedirs(os.path.join(XDG_CACHE_DIR, 'ipython'))
30
30
31
31
32 def teardown_module():
32 def teardown_module():
33 """Teardown testenvironment for the module:
33 """Teardown testenvironment for the module:
34
34
35 - Remove dummy home dir tree
35 - Remove dummy home dir tree
36 """
36 """
37 # Note: we remove the parent test dir, which is the root of all test
37 # Note: we remove the parent test dir, which is the root of all test
38 # subdirs we may have created. Use shutil instead of os.removedirs, so
38 # subdirs we may have created. Use shutil instead of os.removedirs, so
39 # that non-empty directories are all recursively removed.
39 # that non-empty directories are all recursively removed.
40 shutil.rmtree(TMP_TEST_DIR)
40 shutil.rmtree(TMP_TEST_DIR)
41
41
42 def patch_get_home_dir(dirpath):
42 def patch_get_home_dir(dirpath):
43 return patch.object(paths, 'get_home_dir', return_value=dirpath)
43 return patch.object(paths, 'get_home_dir', return_value=dirpath)
44
44
45
45
46 def test_get_ipython_dir_1():
46 def test_get_ipython_dir_1():
47 """test_get_ipython_dir_1, Testcase to see if we can call get_ipython_dir without Exceptions."""
47 """test_get_ipython_dir_1, Testcase to see if we can call get_ipython_dir without Exceptions."""
48 env_ipdir = os.path.join("someplace", ".ipython")
48 env_ipdir = os.path.join("someplace", ".ipython")
49 with patch.object(paths, '_writable_dir', return_value=True), \
49 with patch.object(paths, '_writable_dir', return_value=True), \
50 modified_env({'IPYTHONDIR': env_ipdir}):
50 modified_env({'IPYTHONDIR': env_ipdir}):
51 ipdir = paths.get_ipython_dir()
51 ipdir = paths.get_ipython_dir()
52
52
53 assert ipdir == env_ipdir
53 assert ipdir == env_ipdir
54
54
55 def test_get_ipython_dir_2():
55 def test_get_ipython_dir_2():
56 """test_get_ipython_dir_2, Testcase to see if we can call get_ipython_dir without Exceptions."""
56 """test_get_ipython_dir_2, Testcase to see if we can call get_ipython_dir without Exceptions."""
57 with patch_get_home_dir('someplace'), \
57 with patch_get_home_dir('someplace'), \
58 patch.object(paths, 'get_xdg_dir', return_value=None), \
58 patch.object(paths, 'get_xdg_dir', return_value=None), \
59 patch.object(paths, '_writable_dir', return_value=True), \
59 patch.object(paths, '_writable_dir', return_value=True), \
60 patch('os.name', "posix"), \
60 patch('os.name', "posix"), \
61 modified_env({'IPYTHON_DIR': None,
61 modified_env({'IPYTHON_DIR': None,
62 'IPYTHONDIR': None,
62 'IPYTHONDIR': None,
63 'XDG_CONFIG_HOME': None
63 'XDG_CONFIG_HOME': None
64 }):
64 }):
65 ipdir = paths.get_ipython_dir()
65 ipdir = paths.get_ipython_dir()
66
66
67 assert ipdir == os.path.join("someplace", ".ipython")
67 assert ipdir == os.path.join("someplace", ".ipython")
68
68
69 def test_get_ipython_dir_3():
69 def test_get_ipython_dir_3():
70 """test_get_ipython_dir_3, use XDG if defined and exists, and .ipython doesn't exist."""
70 """test_get_ipython_dir_3, use XDG if defined and exists, and .ipython doesn't exist."""
71 tmphome = TemporaryDirectory()
71 tmphome = TemporaryDirectory()
72 try:
72 try:
73 with patch_get_home_dir(tmphome.name), \
73 with patch_get_home_dir(tmphome.name), \
74 patch('os.name', 'posix'), \
74 patch('os.name', 'posix'), \
75 modified_env({
75 modified_env({
76 'IPYTHON_DIR': None,
76 'IPYTHON_DIR': None,
77 'IPYTHONDIR': None,
77 'IPYTHONDIR': None,
78 'XDG_CONFIG_HOME': XDG_TEST_DIR,
78 'XDG_CONFIG_HOME': XDG_TEST_DIR,
79 }), warnings.catch_warnings(record=True) as w:
79 }), warnings.catch_warnings(record=True) as w:
80 ipdir = paths.get_ipython_dir()
80 ipdir = paths.get_ipython_dir()
81
81
82 assert ipdir == os.path.join(tmphome.name, XDG_TEST_DIR, "ipython")
82 assert ipdir == os.path.join(tmphome.name, XDG_TEST_DIR, "ipython")
83 assert len(w) == 0
83 assert len(w) == 0
84 finally:
84 finally:
85 tmphome.cleanup()
85 tmphome.cleanup()
86
86
87 def test_get_ipython_dir_4():
87 def test_get_ipython_dir_4():
88 """test_get_ipython_dir_4, warn if XDG and home both exist."""
88 """test_get_ipython_dir_4, warn if XDG and home both exist."""
89 with patch_get_home_dir(HOME_TEST_DIR), \
89 with patch_get_home_dir(HOME_TEST_DIR), \
90 patch('os.name', 'posix'):
90 patch('os.name', 'posix'):
91 try:
91 try:
92 os.mkdir(os.path.join(XDG_TEST_DIR, 'ipython'))
92 os.mkdir(os.path.join(XDG_TEST_DIR, 'ipython'))
93 except OSError as e:
93 except OSError as e:
94 if e.errno != errno.EEXIST:
94 if e.errno != errno.EEXIST:
95 raise
95 raise
96
96
97
97
98 with modified_env({
98 with modified_env({
99 'IPYTHON_DIR': None,
99 'IPYTHON_DIR': None,
100 'IPYTHONDIR': None,
100 'IPYTHONDIR': None,
101 'XDG_CONFIG_HOME': XDG_TEST_DIR,
101 'XDG_CONFIG_HOME': XDG_TEST_DIR,
102 }), warnings.catch_warnings(record=True) as w:
102 }), warnings.catch_warnings(record=True) as w:
103 ipdir = paths.get_ipython_dir()
103 ipdir = paths.get_ipython_dir()
104
104
105 assert len(w) == 1
105 assert len(w) == 1
106 assert "Ignoring" in str(w[0])
106 assert "Ignoring" in str(w[0])
107
107
108
108
109 def test_get_ipython_dir_5():
109 def test_get_ipython_dir_5():
110 """test_get_ipython_dir_5, use .ipython if exists and XDG defined, but doesn't exist."""
110 """test_get_ipython_dir_5, use .ipython if exists and XDG defined, but doesn't exist."""
111 with patch_get_home_dir(HOME_TEST_DIR), \
111 with patch_get_home_dir(HOME_TEST_DIR), \
112 patch('os.name', 'posix'):
112 patch('os.name', 'posix'):
113 try:
113 try:
114 os.rmdir(os.path.join(XDG_TEST_DIR, 'ipython'))
114 os.rmdir(os.path.join(XDG_TEST_DIR, 'ipython'))
115 except OSError as e:
115 except OSError as e:
116 if e.errno != errno.ENOENT:
116 if e.errno != errno.ENOENT:
117 raise
117 raise
118
118
119 with modified_env({
119 with modified_env({
120 'IPYTHON_DIR': None,
120 'IPYTHON_DIR': None,
121 'IPYTHONDIR': None,
121 'IPYTHONDIR': None,
122 'XDG_CONFIG_HOME': XDG_TEST_DIR,
122 'XDG_CONFIG_HOME': XDG_TEST_DIR,
123 }):
123 }):
124 ipdir = paths.get_ipython_dir()
124 ipdir = paths.get_ipython_dir()
125
125
126 assert ipdir == IP_TEST_DIR
126 assert ipdir == IP_TEST_DIR
127
127
128 def test_get_ipython_dir_6():
128 def test_get_ipython_dir_6():
129 """test_get_ipython_dir_6, use home over XDG if defined and neither exist."""
129 """test_get_ipython_dir_6, use home over XDG if defined and neither exist."""
130 xdg = os.path.join(HOME_TEST_DIR, 'somexdg')
130 xdg = os.path.join(HOME_TEST_DIR, 'somexdg')
131 os.mkdir(xdg)
131 os.mkdir(xdg)
132 shutil.rmtree(os.path.join(HOME_TEST_DIR, '.ipython'))
132 shutil.rmtree(os.path.join(HOME_TEST_DIR, '.ipython'))
133 print(paths._writable_dir)
133 print(paths._writable_dir)
134 with patch_get_home_dir(HOME_TEST_DIR), \
134 with patch_get_home_dir(HOME_TEST_DIR), \
135 patch.object(paths, 'get_xdg_dir', return_value=xdg), \
135 patch.object(paths, 'get_xdg_dir', return_value=xdg), \
136 patch('os.name', 'posix'), \
136 patch('os.name', 'posix'), \
137 modified_env({
137 modified_env({
138 'IPYTHON_DIR': None,
138 'IPYTHON_DIR': None,
139 'IPYTHONDIR': None,
139 'IPYTHONDIR': None,
140 'XDG_CONFIG_HOME': None,
140 'XDG_CONFIG_HOME': None,
141 }), warnings.catch_warnings(record=True) as w:
141 }), warnings.catch_warnings(record=True) as w:
142 ipdir = paths.get_ipython_dir()
142 ipdir = paths.get_ipython_dir()
143
143
144 assert ipdir == os.path.join(HOME_TEST_DIR, ".ipython")
144 assert ipdir == os.path.join(HOME_TEST_DIR, ".ipython")
145 assert len(w) == 0
145 assert len(w) == 0
146
146
147 def test_get_ipython_dir_7():
147 def test_get_ipython_dir_7():
148 """test_get_ipython_dir_7, test home directory expansion on IPYTHONDIR"""
148 """test_get_ipython_dir_7, test home directory expansion on IPYTHONDIR"""
149 home_dir = os.path.normpath(os.path.expanduser('~'))
149 home_dir = os.path.normpath(os.path.expanduser('~'))
150 with modified_env({'IPYTHONDIR': os.path.join('~', 'somewhere')}), \
150 with modified_env({'IPYTHONDIR': os.path.join('~', 'somewhere')}), \
151 patch.object(paths, '_writable_dir', return_value=True):
151 patch.object(paths, '_writable_dir', return_value=True):
152 ipdir = paths.get_ipython_dir()
152 ipdir = paths.get_ipython_dir()
153 assert ipdir == os.path.join(home_dir, "somewhere")
153 assert ipdir == os.path.join(home_dir, "somewhere")
154
154
155
155
156 @skip_win32
156 @skip_win32
157 def test_get_ipython_dir_8():
157 def test_get_ipython_dir_8():
158 """test_get_ipython_dir_8, test / home directory"""
158 """test_get_ipython_dir_8, test / home directory"""
159 if not os.access("/", os.W_OK):
159 if not os.access("/", os.W_OK):
160 # test only when HOME directory actually writable
160 # test only when HOME directory actually writable
161 return
161 return
162
162
163 with patch.object(paths, "_writable_dir", lambda path: bool(path)), patch.object(
163 with (
164 paths, "get_xdg_dir", return_value=None
164 patch.object(paths, "_writable_dir", lambda path: bool(path)),
165 ), modified_env(
165 patch.object(paths, "get_xdg_dir", return_value=None),
166 modified_env(
166 {
167 {
167 "IPYTHON_DIR": None,
168 "IPYTHON_DIR": None,
168 "IPYTHONDIR": None,
169 "IPYTHONDIR": None,
169 "HOME": "/",
170 "HOME": "/",
170 }
171 }
172 ),
171 ):
173 ):
172 assert paths.get_ipython_dir() == "/.ipython"
174 assert paths.get_ipython_dir() == "/.ipython"
173
175
174
176
175 def test_get_ipython_cache_dir():
177 def test_get_ipython_cache_dir():
176 with modified_env({'HOME': HOME_TEST_DIR}):
178 with modified_env({'HOME': HOME_TEST_DIR}):
177 if os.name == "posix":
179 if os.name == "posix":
178 # test default
180 # test default
179 os.makedirs(os.path.join(HOME_TEST_DIR, ".cache"))
181 os.makedirs(os.path.join(HOME_TEST_DIR, ".cache"))
180 with modified_env({'XDG_CACHE_HOME': None}):
182 with modified_env({'XDG_CACHE_HOME': None}):
181 ipdir = paths.get_ipython_cache_dir()
183 ipdir = paths.get_ipython_cache_dir()
182 assert os.path.join(HOME_TEST_DIR, ".cache", "ipython") == ipdir
184 assert os.path.join(HOME_TEST_DIR, ".cache", "ipython") == ipdir
183 assert_isdir(ipdir)
185 assert_isdir(ipdir)
184
186
185 # test env override
187 # test env override
186 with modified_env({"XDG_CACHE_HOME": XDG_CACHE_DIR}):
188 with modified_env({"XDG_CACHE_HOME": XDG_CACHE_DIR}):
187 ipdir = paths.get_ipython_cache_dir()
189 ipdir = paths.get_ipython_cache_dir()
188 assert_isdir(ipdir)
190 assert_isdir(ipdir)
189 assert ipdir == os.path.join(XDG_CACHE_DIR, "ipython")
191 assert ipdir == os.path.join(XDG_CACHE_DIR, "ipython")
190 else:
192 else:
191 assert paths.get_ipython_cache_dir() == paths.get_ipython_dir()
193 assert paths.get_ipython_cache_dir() == paths.get_ipython_dir()
192
194
193 def test_get_ipython_package_dir():
195 def test_get_ipython_package_dir():
194 ipdir = paths.get_ipython_package_dir()
196 ipdir = paths.get_ipython_package_dir()
195 assert_isdir(ipdir)
197 assert_isdir(ipdir)
196
198
197
199
198 def test_get_ipython_module_path():
200 def test_get_ipython_module_path():
199 ipapp_path = paths.get_ipython_module_path('IPython.terminal.ipapp')
201 ipapp_path = paths.get_ipython_module_path('IPython.terminal.ipapp')
200 assert_isfile(ipapp_path)
202 assert_isfile(ipapp_path)
@@ -1,422 +1,423
1 """
1 """
2 This module contains factory functions that attempt
2 This module contains factory functions that attempt
3 to return Qt submodules from the various python Qt bindings.
3 to return Qt submodules from the various python Qt bindings.
4
4
5 It also protects against double-importing Qt with different
5 It also protects against double-importing Qt with different
6 bindings, which is unstable and likely to crash
6 bindings, which is unstable and likely to crash
7
7
8 This is used primarily by qt and qt_for_kernel, and shouldn't
8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 be accessed directly from the outside
9 be accessed directly from the outside
10 """
10 """
11
11 import importlib.abc
12 import importlib.abc
12 import sys
13 import sys
13 import os
14 import os
14 import types
15 import types
15 from functools import partial, lru_cache
16 from functools import partial, lru_cache
16 import operator
17 import operator
17
18
18 # ### Available APIs.
19 # ### Available APIs.
19 # Qt6
20 # Qt6
20 QT_API_PYQT6 = "pyqt6"
21 QT_API_PYQT6 = "pyqt6"
21 QT_API_PYSIDE6 = "pyside6"
22 QT_API_PYSIDE6 = "pyside6"
22
23
23 # Qt5
24 # Qt5
24 QT_API_PYQT5 = 'pyqt5'
25 QT_API_PYQT5 = 'pyqt5'
25 QT_API_PYSIDE2 = 'pyside2'
26 QT_API_PYSIDE2 = 'pyside2'
26
27
27 # Qt4
28 # Qt4
28 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
29 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
29 QT_API_PYQT = "pyqt" # Force version 2
30 QT_API_PYQT = "pyqt" # Force version 2
30 QT_API_PYQTv1 = "pyqtv1" # Force version 2
31 QT_API_PYQTv1 = "pyqtv1" # Force version 2
31 QT_API_PYSIDE = "pyside"
32 QT_API_PYSIDE = "pyside"
32
33
33 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
34 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
34
35
35 api_to_module = {
36 api_to_module = {
36 # Qt6
37 # Qt6
37 QT_API_PYQT6: "PyQt6",
38 QT_API_PYQT6: "PyQt6",
38 QT_API_PYSIDE6: "PySide6",
39 QT_API_PYSIDE6: "PySide6",
39 # Qt5
40 # Qt5
40 QT_API_PYQT5: "PyQt5",
41 QT_API_PYQT5: "PyQt5",
41 QT_API_PYSIDE2: "PySide2",
42 QT_API_PYSIDE2: "PySide2",
42 # Qt4
43 # Qt4
43 QT_API_PYSIDE: "PySide",
44 QT_API_PYSIDE: "PySide",
44 QT_API_PYQT: "PyQt4",
45 QT_API_PYQT: "PyQt4",
45 QT_API_PYQTv1: "PyQt4",
46 QT_API_PYQTv1: "PyQt4",
46 # default
47 # default
47 QT_API_PYQT_DEFAULT: "PyQt6",
48 QT_API_PYQT_DEFAULT: "PyQt6",
48 }
49 }
49
50
50
51
51 class ImportDenier(importlib.abc.MetaPathFinder):
52 class ImportDenier(importlib.abc.MetaPathFinder):
52 """Import Hook that will guard against bad Qt imports
53 """Import Hook that will guard against bad Qt imports
53 once IPython commits to a specific binding
54 once IPython commits to a specific binding
54 """
55 """
55
56
56 def __init__(self):
57 def __init__(self):
57 self.__forbidden = set()
58 self.__forbidden = set()
58
59
59 def forbid(self, module_name):
60 def forbid(self, module_name):
60 sys.modules.pop(module_name, None)
61 sys.modules.pop(module_name, None)
61 self.__forbidden.add(module_name)
62 self.__forbidden.add(module_name)
62
63
63 def find_spec(self, fullname, path, target=None):
64 def find_spec(self, fullname, path, target=None):
64 if path:
65 if path:
65 return
66 return
66 if fullname in self.__forbidden:
67 if fullname in self.__forbidden:
67 raise ImportError(
68 raise ImportError(
68 """
69 """
69 Importing %s disabled by IPython, which has
70 Importing %s disabled by IPython, which has
70 already imported an Incompatible QT Binding: %s
71 already imported an Incompatible QT Binding: %s
71 """
72 """
72 % (fullname, loaded_api())
73 % (fullname, loaded_api())
73 )
74 )
74
75
75
76
76 ID = ImportDenier()
77 ID = ImportDenier()
77 sys.meta_path.insert(0, ID)
78 sys.meta_path.insert(0, ID)
78
79
79
80
80 def commit_api(api):
81 def commit_api(api):
81 """Commit to a particular API, and trigger ImportErrors on subsequent
82 """Commit to a particular API, and trigger ImportErrors on subsequent
82 dangerous imports"""
83 dangerous imports"""
83 modules = set(api_to_module.values())
84 modules = set(api_to_module.values())
84
85
85 modules.remove(api_to_module[api])
86 modules.remove(api_to_module[api])
86 for mod in modules:
87 for mod in modules:
87 ID.forbid(mod)
88 ID.forbid(mod)
88
89
89
90
90 def loaded_api():
91 def loaded_api():
91 """Return which API is loaded, if any
92 """Return which API is loaded, if any
92
93
93 If this returns anything besides None,
94 If this returns anything besides None,
94 importing any other Qt binding is unsafe.
95 importing any other Qt binding is unsafe.
95
96
96 Returns
97 Returns
97 -------
98 -------
98 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
99 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
99 """
100 """
100 if sys.modules.get("PyQt6.QtCore"):
101 if sys.modules.get("PyQt6.QtCore"):
101 return QT_API_PYQT6
102 return QT_API_PYQT6
102 elif sys.modules.get("PySide6.QtCore"):
103 elif sys.modules.get("PySide6.QtCore"):
103 return QT_API_PYSIDE6
104 return QT_API_PYSIDE6
104 elif sys.modules.get("PyQt5.QtCore"):
105 elif sys.modules.get("PyQt5.QtCore"):
105 return QT_API_PYQT5
106 return QT_API_PYQT5
106 elif sys.modules.get("PySide2.QtCore"):
107 elif sys.modules.get("PySide2.QtCore"):
107 return QT_API_PYSIDE2
108 return QT_API_PYSIDE2
108 elif sys.modules.get("PyQt4.QtCore"):
109 elif sys.modules.get("PyQt4.QtCore"):
109 if qtapi_version() == 2:
110 if qtapi_version() == 2:
110 return QT_API_PYQT
111 return QT_API_PYQT
111 else:
112 else:
112 return QT_API_PYQTv1
113 return QT_API_PYQTv1
113 elif sys.modules.get("PySide.QtCore"):
114 elif sys.modules.get("PySide.QtCore"):
114 return QT_API_PYSIDE
115 return QT_API_PYSIDE
115
116
116 return None
117 return None
117
118
118
119
119 def has_binding(api):
120 def has_binding(api):
120 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
121 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
121
122
122 Parameters
123 Parameters
123 ----------
124 ----------
124 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
125 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
125 Which module to check for
126 Which module to check for
126
127
127 Returns
128 Returns
128 -------
129 -------
129 True if the relevant module appears to be importable
130 True if the relevant module appears to be importable
130 """
131 """
131 module_name = api_to_module[api]
132 module_name = api_to_module[api]
132 from importlib.util import find_spec
133 from importlib.util import find_spec
133
134
134 required = ['QtCore', 'QtGui', 'QtSvg']
135 required = ['QtCore', 'QtGui', 'QtSvg']
135 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
136 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
136 # QT5 requires QtWidgets too
137 # QT5 requires QtWidgets too
137 required.append('QtWidgets')
138 required.append('QtWidgets')
138
139
139 for submod in required:
140 for submod in required:
140 try:
141 try:
141 spec = find_spec('%s.%s' % (module_name, submod))
142 spec = find_spec('%s.%s' % (module_name, submod))
142 except ImportError:
143 except ImportError:
143 # Package (e.g. PyQt5) not found
144 # Package (e.g. PyQt5) not found
144 return False
145 return False
145 else:
146 else:
146 if spec is None:
147 if spec is None:
147 # Submodule (e.g. PyQt5.QtCore) not found
148 # Submodule (e.g. PyQt5.QtCore) not found
148 return False
149 return False
149
150
150 if api == QT_API_PYSIDE:
151 if api == QT_API_PYSIDE:
151 # We can also safely check PySide version
152 # We can also safely check PySide version
152 import PySide
153 import PySide
153
154
154 return PySide.__version_info__ >= (1, 0, 3)
155 return PySide.__version_info__ >= (1, 0, 3)
155
156
156 return True
157 return True
157
158
158
159
159 def qtapi_version():
160 def qtapi_version():
160 """Return which QString API has been set, if any
161 """Return which QString API has been set, if any
161
162
162 Returns
163 Returns
163 -------
164 -------
164 The QString API version (1 or 2), or None if not set
165 The QString API version (1 or 2), or None if not set
165 """
166 """
166 try:
167 try:
167 import sip
168 import sip
168 except ImportError:
169 except ImportError:
169 # as of PyQt5 5.11, sip is no longer available as a top-level
170 # as of PyQt5 5.11, sip is no longer available as a top-level
170 # module and needs to be imported from the PyQt5 namespace
171 # module and needs to be imported from the PyQt5 namespace
171 try:
172 try:
172 from PyQt5 import sip
173 from PyQt5 import sip
173 except ImportError:
174 except ImportError:
174 return
175 return
175 try:
176 try:
176 return sip.getapi('QString')
177 return sip.getapi('QString')
177 except ValueError:
178 except ValueError:
178 return
179 return
179
180
180
181
181 def can_import(api):
182 def can_import(api):
182 """Safely query whether an API is importable, without importing it"""
183 """Safely query whether an API is importable, without importing it"""
183 if not has_binding(api):
184 if not has_binding(api):
184 return False
185 return False
185
186
186 current = loaded_api()
187 current = loaded_api()
187 if api == QT_API_PYQT_DEFAULT:
188 if api == QT_API_PYQT_DEFAULT:
188 return current in [QT_API_PYQT6, None]
189 return current in [QT_API_PYQT6, None]
189 else:
190 else:
190 return current in [api, None]
191 return current in [api, None]
191
192
192
193
193 def import_pyqt4(version=2):
194 def import_pyqt4(version=2):
194 """
195 """
195 Import PyQt4
196 Import PyQt4
196
197
197 Parameters
198 Parameters
198 ----------
199 ----------
199 version : 1, 2, or None
200 version : 1, 2, or None
200 Which QString/QVariant API to use. Set to None to use the system
201 Which QString/QVariant API to use. Set to None to use the system
201 default
202 default
202 ImportErrors raised within this function are non-recoverable
203 ImportErrors raised within this function are non-recoverable
203 """
204 """
204 # The new-style string API (version=2) automatically
205 # The new-style string API (version=2) automatically
205 # converts QStrings to Unicode Python strings. Also, automatically unpacks
206 # converts QStrings to Unicode Python strings. Also, automatically unpacks
206 # QVariants to their underlying objects.
207 # QVariants to their underlying objects.
207 import sip
208 import sip
208
209
209 if version is not None:
210 if version is not None:
210 sip.setapi('QString', version)
211 sip.setapi('QString', version)
211 sip.setapi('QVariant', version)
212 sip.setapi('QVariant', version)
212
213
213 from PyQt4 import QtGui, QtCore, QtSvg
214 from PyQt4 import QtGui, QtCore, QtSvg
214
215
215 if QtCore.PYQT_VERSION < 0x040700:
216 if QtCore.PYQT_VERSION < 0x040700:
216 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
217 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
217 QtCore.PYQT_VERSION_STR)
218 QtCore.PYQT_VERSION_STR)
218
219
219 # Alias PyQt-specific functions for PySide compatibility.
220 # Alias PyQt-specific functions for PySide compatibility.
220 QtCore.Signal = QtCore.pyqtSignal
221 QtCore.Signal = QtCore.pyqtSignal
221 QtCore.Slot = QtCore.pyqtSlot
222 QtCore.Slot = QtCore.pyqtSlot
222
223
223 # query for the API version (in case version == None)
224 # query for the API version (in case version == None)
224 version = sip.getapi('QString')
225 version = sip.getapi('QString')
225 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
226 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
226 return QtCore, QtGui, QtSvg, api
227 return QtCore, QtGui, QtSvg, api
227
228
228
229
229 def import_pyqt5():
230 def import_pyqt5():
230 """
231 """
231 Import PyQt5
232 Import PyQt5
232
233
233 ImportErrors raised within this function are non-recoverable
234 ImportErrors raised within this function are non-recoverable
234 """
235 """
235
236
236 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
237 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
237
238
238 # Alias PyQt-specific functions for PySide compatibility.
239 # Alias PyQt-specific functions for PySide compatibility.
239 QtCore.Signal = QtCore.pyqtSignal
240 QtCore.Signal = QtCore.pyqtSignal
240 QtCore.Slot = QtCore.pyqtSlot
241 QtCore.Slot = QtCore.pyqtSlot
241
242
242 # Join QtGui and QtWidgets for Qt4 compatibility.
243 # Join QtGui and QtWidgets for Qt4 compatibility.
243 QtGuiCompat = types.ModuleType('QtGuiCompat')
244 QtGuiCompat = types.ModuleType('QtGuiCompat')
244 QtGuiCompat.__dict__.update(QtGui.__dict__)
245 QtGuiCompat.__dict__.update(QtGui.__dict__)
245 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
246 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
246
247
247 api = QT_API_PYQT5
248 api = QT_API_PYQT5
248 return QtCore, QtGuiCompat, QtSvg, api
249 return QtCore, QtGuiCompat, QtSvg, api
249
250
250
251
251 def import_pyqt6():
252 def import_pyqt6():
252 """
253 """
253 Import PyQt6
254 Import PyQt6
254
255
255 ImportErrors raised within this function are non-recoverable
256 ImportErrors raised within this function are non-recoverable
256 """
257 """
257
258
258 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
259 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
259
260
260 # Alias PyQt-specific functions for PySide compatibility.
261 # Alias PyQt-specific functions for PySide compatibility.
261 QtCore.Signal = QtCore.pyqtSignal
262 QtCore.Signal = QtCore.pyqtSignal
262 QtCore.Slot = QtCore.pyqtSlot
263 QtCore.Slot = QtCore.pyqtSlot
263
264
264 # Join QtGui and QtWidgets for Qt4 compatibility.
265 # Join QtGui and QtWidgets for Qt4 compatibility.
265 QtGuiCompat = types.ModuleType("QtGuiCompat")
266 QtGuiCompat = types.ModuleType("QtGuiCompat")
266 QtGuiCompat.__dict__.update(QtGui.__dict__)
267 QtGuiCompat.__dict__.update(QtGui.__dict__)
267 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
268 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
268
269
269 api = QT_API_PYQT6
270 api = QT_API_PYQT6
270 return QtCore, QtGuiCompat, QtSvg, api
271 return QtCore, QtGuiCompat, QtSvg, api
271
272
272
273
273 def import_pyside():
274 def import_pyside():
274 """
275 """
275 Import PySide
276 Import PySide
276
277
277 ImportErrors raised within this function are non-recoverable
278 ImportErrors raised within this function are non-recoverable
278 """
279 """
279 from PySide import QtGui, QtCore, QtSvg
280 from PySide import QtGui, QtCore, QtSvg
280 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
281 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
281
282
282 def import_pyside2():
283 def import_pyside2():
283 """
284 """
284 Import PySide2
285 Import PySide2
285
286
286 ImportErrors raised within this function are non-recoverable
287 ImportErrors raised within this function are non-recoverable
287 """
288 """
288 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
289 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
289
290
290 # Join QtGui and QtWidgets for Qt4 compatibility.
291 # Join QtGui and QtWidgets for Qt4 compatibility.
291 QtGuiCompat = types.ModuleType('QtGuiCompat')
292 QtGuiCompat = types.ModuleType('QtGuiCompat')
292 QtGuiCompat.__dict__.update(QtGui.__dict__)
293 QtGuiCompat.__dict__.update(QtGui.__dict__)
293 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
294 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
294 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
295 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
295
296
296 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
297 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
297
298
298
299
299 def import_pyside6():
300 def import_pyside6():
300 """
301 """
301 Import PySide6
302 Import PySide6
302
303
303 ImportErrors raised within this function are non-recoverable
304 ImportErrors raised within this function are non-recoverable
304 """
305 """
305
306
306 def get_attrs(module):
307 def get_attrs(module):
307 return {
308 return {
308 name: getattr(module, name)
309 name: getattr(module, name)
309 for name in dir(module)
310 for name in dir(module)
310 if not name.startswith("_")
311 if not name.startswith("_")
311 }
312 }
312
313
313 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
314 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
314
315
315 # Join QtGui and QtWidgets for Qt4 compatibility.
316 # Join QtGui and QtWidgets for Qt4 compatibility.
316 QtGuiCompat = types.ModuleType("QtGuiCompat")
317 QtGuiCompat = types.ModuleType("QtGuiCompat")
317 QtGuiCompat.__dict__.update(QtGui.__dict__)
318 QtGuiCompat.__dict__.update(QtGui.__dict__)
318 if QtCore.__version_info__ < (6, 7):
319 if QtCore.__version_info__ < (6, 7):
319 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
320 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
320 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
321 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
321 else:
322 else:
322 QtGuiCompat.__dict__.update(get_attrs(QtWidgets))
323 QtGuiCompat.__dict__.update(get_attrs(QtWidgets))
323 QtGuiCompat.__dict__.update(get_attrs(QtPrintSupport))
324 QtGuiCompat.__dict__.update(get_attrs(QtPrintSupport))
324
325
325 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
326 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
326
327
327
328
328 def load_qt(api_options):
329 def load_qt(api_options):
329 """
330 """
330 Attempt to import Qt, given a preference list
331 Attempt to import Qt, given a preference list
331 of permissible bindings
332 of permissible bindings
332
333
333 It is safe to call this function multiple times.
334 It is safe to call this function multiple times.
334
335
335 Parameters
336 Parameters
336 ----------
337 ----------
337 api_options : List of strings
338 api_options : List of strings
338 The order of APIs to try. Valid items are 'pyside', 'pyside2',
339 The order of APIs to try. Valid items are 'pyside', 'pyside2',
339 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
340 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
340
341
341 Returns
342 Returns
342 -------
343 -------
343 A tuple of QtCore, QtGui, QtSvg, QT_API
344 A tuple of QtCore, QtGui, QtSvg, QT_API
344 The first three are the Qt modules. The last is the
345 The first three are the Qt modules. The last is the
345 string indicating which module was loaded.
346 string indicating which module was loaded.
346
347
347 Raises
348 Raises
348 ------
349 ------
349 ImportError, if it isn't possible to import any requested
350 ImportError, if it isn't possible to import any requested
350 bindings (either because they aren't installed, or because
351 bindings (either because they aren't installed, or because
351 an incompatible library has already been installed)
352 an incompatible library has already been installed)
352 """
353 """
353 loaders = {
354 loaders = {
354 # Qt6
355 # Qt6
355 QT_API_PYQT6: import_pyqt6,
356 QT_API_PYQT6: import_pyqt6,
356 QT_API_PYSIDE6: import_pyside6,
357 QT_API_PYSIDE6: import_pyside6,
357 # Qt5
358 # Qt5
358 QT_API_PYQT5: import_pyqt5,
359 QT_API_PYQT5: import_pyqt5,
359 QT_API_PYSIDE2: import_pyside2,
360 QT_API_PYSIDE2: import_pyside2,
360 # Qt4
361 # Qt4
361 QT_API_PYSIDE: import_pyside,
362 QT_API_PYSIDE: import_pyside,
362 QT_API_PYQT: import_pyqt4,
363 QT_API_PYQT: import_pyqt4,
363 QT_API_PYQTv1: partial(import_pyqt4, version=1),
364 QT_API_PYQTv1: partial(import_pyqt4, version=1),
364 # default
365 # default
365 QT_API_PYQT_DEFAULT: import_pyqt6,
366 QT_API_PYQT_DEFAULT: import_pyqt6,
366 }
367 }
367
368
368 for api in api_options:
369 for api in api_options:
369
370
370 if api not in loaders:
371 if api not in loaders:
371 raise RuntimeError(
372 raise RuntimeError(
372 "Invalid Qt API %r, valid values are: %s" %
373 "Invalid Qt API %r, valid values are: %s" %
373 (api, ", ".join(["%r" % k for k in loaders.keys()])))
374 (api, ", ".join(["%r" % k for k in loaders.keys()])))
374
375
375 if not can_import(api):
376 if not can_import(api):
376 continue
377 continue
377
378
378 #cannot safely recover from an ImportError during this
379 #cannot safely recover from an ImportError during this
379 result = loaders[api]()
380 result = loaders[api]()
380 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
381 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
381 commit_api(api)
382 commit_api(api)
382 return result
383 return result
383 else:
384 else:
384 # Clear the environment variable since it doesn't work.
385 # Clear the environment variable since it doesn't work.
385 if "QT_API" in os.environ:
386 if "QT_API" in os.environ:
386 del os.environ["QT_API"]
387 del os.environ["QT_API"]
387
388
388 raise ImportError(
389 raise ImportError(
389 """
390 """
390 Could not load requested Qt binding. Please ensure that
391 Could not load requested Qt binding. Please ensure that
391 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
392 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
392 PySide6 is available, and only one is imported per session.
393 PySide6 is available, and only one is imported per session.
393
394
394 Currently-imported Qt library: %r
395 Currently-imported Qt library: %r
395 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
396 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
396 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
397 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
397 PySide2 installed: %s
398 PySide2 installed: %s
398 PySide6 installed: %s
399 PySide6 installed: %s
399 Tried to load: %r
400 Tried to load: %r
400 """
401 """
401 % (
402 % (
402 loaded_api(),
403 loaded_api(),
403 has_binding(QT_API_PYQT5),
404 has_binding(QT_API_PYQT5),
404 has_binding(QT_API_PYQT6),
405 has_binding(QT_API_PYQT6),
405 has_binding(QT_API_PYSIDE2),
406 has_binding(QT_API_PYSIDE2),
406 has_binding(QT_API_PYSIDE6),
407 has_binding(QT_API_PYSIDE6),
407 api_options,
408 api_options,
408 )
409 )
409 )
410 )
410
411
411
412
412 def enum_factory(QT_API, QtCore):
413 def enum_factory(QT_API, QtCore):
413 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
414 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
414
415
415 @lru_cache(None)
416 @lru_cache(None)
416 def _enum(name):
417 def _enum(name):
417 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
418 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
418 return operator.attrgetter(
419 return operator.attrgetter(
419 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
420 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
420 )(sys.modules[QtCore.__package__])
421 )(sys.modules[QtCore.__package__])
421
422
422 return _enum
423 return _enum
@@ -1,101 +1,102
1 """ Utilities for accessing the platform's clipboard.
1 """ Utilities for accessing the platform's clipboard.
2 """
2 """
3
3 import os
4 import os
4 import subprocess
5 import subprocess
5
6
6 from IPython.core.error import TryNext
7 from IPython.core.error import TryNext
7 import IPython.utils.py3compat as py3compat
8 import IPython.utils.py3compat as py3compat
8
9
9
10
10 class ClipboardEmpty(ValueError):
11 class ClipboardEmpty(ValueError):
11 pass
12 pass
12
13
13
14
14 def win32_clipboard_get():
15 def win32_clipboard_get():
15 """ Get the current clipboard's text on Windows.
16 """ Get the current clipboard's text on Windows.
16
17
17 Requires Mark Hammond's pywin32 extensions.
18 Requires Mark Hammond's pywin32 extensions.
18 """
19 """
19 try:
20 try:
20 import win32clipboard
21 import win32clipboard
21 except ImportError as e:
22 except ImportError as e:
22 raise TryNext("Getting text from the clipboard requires the pywin32 "
23 raise TryNext("Getting text from the clipboard requires the pywin32 "
23 "extensions: http://sourceforge.net/projects/pywin32/") from e
24 "extensions: http://sourceforge.net/projects/pywin32/") from e
24 win32clipboard.OpenClipboard()
25 win32clipboard.OpenClipboard()
25 try:
26 try:
26 text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
27 text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
27 except (TypeError, win32clipboard.error):
28 except (TypeError, win32clipboard.error):
28 try:
29 try:
29 text = win32clipboard.GetClipboardData(win32clipboard.CF_TEXT)
30 text = win32clipboard.GetClipboardData(win32clipboard.CF_TEXT)
30 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
31 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
31 except (TypeError, win32clipboard.error) as e:
32 except (TypeError, win32clipboard.error) as e:
32 raise ClipboardEmpty from e
33 raise ClipboardEmpty from e
33 finally:
34 finally:
34 win32clipboard.CloseClipboard()
35 win32clipboard.CloseClipboard()
35 return text
36 return text
36
37
37
38
38 def osx_clipboard_get() -> str:
39 def osx_clipboard_get() -> str:
39 """ Get the clipboard's text on OS X.
40 """ Get the clipboard's text on OS X.
40 """
41 """
41 p = subprocess.Popen(['pbpaste', '-Prefer', 'ascii'],
42 p = subprocess.Popen(['pbpaste', '-Prefer', 'ascii'],
42 stdout=subprocess.PIPE)
43 stdout=subprocess.PIPE)
43 bytes_, stderr = p.communicate()
44 bytes_, stderr = p.communicate()
44 # Text comes in with old Mac \r line endings. Change them to \n.
45 # Text comes in with old Mac \r line endings. Change them to \n.
45 bytes_ = bytes_.replace(b'\r', b'\n')
46 bytes_ = bytes_.replace(b'\r', b'\n')
46 text = py3compat.decode(bytes_)
47 text = py3compat.decode(bytes_)
47 return text
48 return text
48
49
49
50
50 def tkinter_clipboard_get():
51 def tkinter_clipboard_get():
51 """ Get the clipboard's text using Tkinter.
52 """ Get the clipboard's text using Tkinter.
52
53
53 This is the default on systems that are not Windows or OS X. It may
54 This is the default on systems that are not Windows or OS X. It may
54 interfere with other UI toolkits and should be replaced with an
55 interfere with other UI toolkits and should be replaced with an
55 implementation that uses that toolkit.
56 implementation that uses that toolkit.
56 """
57 """
57 try:
58 try:
58 from tkinter import Tk, TclError
59 from tkinter import Tk, TclError
59 except ImportError as e:
60 except ImportError as e:
60 raise TryNext("Getting text from the clipboard on this platform requires tkinter.") from e
61 raise TryNext("Getting text from the clipboard on this platform requires tkinter.") from e
61
62
62 root = Tk()
63 root = Tk()
63 root.withdraw()
64 root.withdraw()
64 try:
65 try:
65 text = root.clipboard_get()
66 text = root.clipboard_get()
66 except TclError as e:
67 except TclError as e:
67 raise ClipboardEmpty from e
68 raise ClipboardEmpty from e
68 finally:
69 finally:
69 root.destroy()
70 root.destroy()
70 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
71 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
71 return text
72 return text
72
73
73
74
74 def wayland_clipboard_get():
75 def wayland_clipboard_get():
75 """Get the clipboard's text under Wayland using wl-paste command.
76 """Get the clipboard's text under Wayland using wl-paste command.
76
77
77 This requires Wayland and wl-clipboard installed and running.
78 This requires Wayland and wl-clipboard installed and running.
78 """
79 """
79 if os.environ.get("XDG_SESSION_TYPE") != "wayland":
80 if os.environ.get("XDG_SESSION_TYPE") != "wayland":
80 raise TryNext("wayland is not detected")
81 raise TryNext("wayland is not detected")
81
82
82 try:
83 try:
83 with subprocess.Popen(["wl-paste"], stdout=subprocess.PIPE) as p:
84 with subprocess.Popen(["wl-paste"], stdout=subprocess.PIPE) as p:
84 raw, err = p.communicate()
85 raw, err = p.communicate()
85 if p.wait():
86 if p.wait():
86 raise TryNext(err)
87 raise TryNext(err)
87 except FileNotFoundError as e:
88 except FileNotFoundError as e:
88 raise TryNext(
89 raise TryNext(
89 "Getting text from the clipboard under Wayland requires the wl-clipboard "
90 "Getting text from the clipboard under Wayland requires the wl-clipboard "
90 "extension: https://github.com/bugaevc/wl-clipboard"
91 "extension: https://github.com/bugaevc/wl-clipboard"
91 ) from e
92 ) from e
92
93
93 if not raw:
94 if not raw:
94 raise ClipboardEmpty
95 raise ClipboardEmpty
95
96
96 try:
97 try:
97 text = py3compat.decode(raw)
98 text = py3compat.decode(raw)
98 except UnicodeDecodeError as e:
99 except UnicodeDecodeError as e:
99 raise ClipboardEmpty from e
100 raise ClipboardEmpty from e
100
101
101 return text
102 return text
@@ -1,155 +1,155
1 """
1 """
2 Handlers for IPythonDirective's @doctest pseudo-decorator.
2 Handlers for IPythonDirective's @doctest pseudo-decorator.
3
3
4 The Sphinx extension that provides support for embedded IPython code provides
4 The Sphinx extension that provides support for embedded IPython code provides
5 a pseudo-decorator @doctest, which treats the input/output block as a
5 a pseudo-decorator @doctest, which treats the input/output block as a
6 doctest, raising a RuntimeError during doc generation if the actual output
6 doctest, raising a RuntimeError during doc generation if the actual output
7 (after running the input) does not match the expected output.
7 (after running the input) does not match the expected output.
8
8
9 An example usage is:
9 An example usage is:
10
10
11 .. code-block:: rst
11 .. code-block:: rst
12
12
13 .. ipython::
13 .. ipython::
14
14
15 In [1]: x = 1
15 In [1]: x = 1
16
16
17 @doctest
17 @doctest
18 In [2]: x + 2
18 In [2]: x + 2
19 Out[3]: 3
19 Out[3]: 3
20
20
21 One can also provide arguments to the decorator. The first argument should be
21 One can also provide arguments to the decorator. The first argument should be
22 the name of a custom handler. The specification of any other arguments is
22 the name of a custom handler. The specification of any other arguments is
23 determined by the handler. For example,
23 determined by the handler. For example,
24
24
25 .. code-block:: rst
25 .. code-block:: rst
26
26
27 .. ipython::
27 .. ipython::
28
28
29 @doctest float
29 @doctest float
30 In [154]: 0.1 + 0.2
30 In [154]: 0.1 + 0.2
31 Out[154]: 0.3
31 Out[154]: 0.3
32
32
33 allows the actual output ``0.30000000000000004`` to match the expected output
33 allows the actual output ``0.30000000000000004`` to match the expected output
34 due to a comparison with `np.allclose`.
34 due to a comparison with `np.allclose`.
35
35
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
37 should have the following function signature::
37 should have the following function signature::
38
38
39 handler(sphinx_shell, args, input_lines, found, submitted)
39 handler(sphinx_shell, args, input_lines, found, submitted)
40
40
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
43 a list of the lines relevant to the current doctest, `found` is a string
43 a list of the lines relevant to the current doctest, `found` is a string
44 containing the output from the IPython shell, and `submitted` is a string
44 containing the output from the IPython shell, and `submitted` is a string
45 containing the expected output from the IPython shell.
45 containing the expected output from the IPython shell.
46
46
47 Handlers must be registered in the `doctests` dict at the end of this module.
47 Handlers must be registered in the `doctests` dict at the end of this module.
48
48
49 """
49 """
50
50
51 def str_to_array(s):
51 def str_to_array(s):
52 """
52 """
53 Simplistic converter of strings from repr to float NumPy arrays.
53 Simplistic converter of strings from repr to float NumPy arrays.
54
54
55 If the repr representation has ellipsis in it, then this will fail.
55 If the repr representation has ellipsis in it, then this will fail.
56
56
57 Parameters
57 Parameters
58 ----------
58 ----------
59 s : str
59 s : str
60 The repr version of a NumPy array.
60 The repr version of a NumPy array.
61
61
62 Examples
62 Examples
63 --------
63 --------
64 >>> s = "array([ 0.3, inf, nan])"
64 >>> s = "array([ 0.3, inf, nan])"
65 >>> a = str_to_array(s)
65 >>> a = str_to_array(s)
66
66
67 """
67 """
68 import numpy as np
68 import numpy as np
69
69
70 # Need to make sure eval() knows about inf and nan.
70 # Need to make sure eval() knows about inf and nan.
71 # This also assumes default printoptions for NumPy.
71 # This also assumes default printoptions for NumPy.
72 from numpy import inf, nan
72 from numpy import inf, nan
73
73
74 if s.startswith(u'array'):
74 if s.startswith(u'array'):
75 # Remove array( and )
75 # Remove array( and )
76 s = s[6:-1]
76 s = s[6:-1]
77
77
78 if s.startswith(u'['):
78 if s.startswith(u'['):
79 a = np.array(eval(s), dtype=float)
79 a = np.array(eval(s), dtype=float)
80 else:
80 else:
81 # Assume its a regular float. Force 1D so we can index into it.
81 # Assume its a regular float. Force 1D so we can index into it.
82 a = np.atleast_1d(float(s))
82 a = np.atleast_1d(float(s))
83 return a
83 return a
84
84
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
86 """
86 """
87 Doctest which allow the submitted output to vary slightly from the input.
87 Doctest which allow the submitted output to vary slightly from the input.
88
88
89 Here is how it might appear in an rst file:
89 Here is how it might appear in an rst file:
90
90
91 .. code-block:: rst
91 .. code-block:: rst
92
92
93 .. ipython::
93 .. ipython::
94
94
95 @doctest float
95 @doctest float
96 In [1]: 0.1 + 0.2
96 In [1]: 0.1 + 0.2
97 Out[1]: 0.3
97 Out[1]: 0.3
98
98
99 """
99 """
100 import numpy as np
100 import numpy as np
101
101
102 if len(args) == 2:
102 if len(args) == 2:
103 rtol = 1e-05
103 rtol = 1e-05
104 atol = 1e-08
104 atol = 1e-08
105 else:
105 else:
106 # Both must be specified if any are specified.
106 # Both must be specified if any are specified.
107 try:
107 try:
108 rtol = float(args[2])
108 rtol = float(args[2])
109 atol = float(args[3])
109 atol = float(args[3])
110 except IndexError as e:
110 except IndexError:
111 e = ("Both `rtol` and `atol` must be specified "
111 e = ("Both `rtol` and `atol` must be specified "
112 "if either are specified: {0}".format(args))
112 "if either are specified: {0}".format(args))
113 raise IndexError(e) from e
113 raise IndexError(e) from e
114
114
115 try:
115 try:
116 submitted = str_to_array(submitted)
116 submitted = str_to_array(submitted)
117 found = str_to_array(found)
117 found = str_to_array(found)
118 except:
118 except:
119 # For example, if the array is huge and there are ellipsis in it.
119 # For example, if the array is huge and there are ellipsis in it.
120 error = True
120 error = True
121 else:
121 else:
122 found_isnan = np.isnan(found)
122 found_isnan = np.isnan(found)
123 submitted_isnan = np.isnan(submitted)
123 submitted_isnan = np.isnan(submitted)
124 error = not np.allclose(found_isnan, submitted_isnan)
124 error = not np.allclose(found_isnan, submitted_isnan)
125 error |= not np.allclose(found[~found_isnan],
125 error |= not np.allclose(found[~found_isnan],
126 submitted[~submitted_isnan],
126 submitted[~submitted_isnan],
127 rtol=rtol, atol=atol)
127 rtol=rtol, atol=atol)
128
128
129 TAB = ' ' * 4
129 TAB = ' ' * 4
130 directive = sphinx_shell.directive
130 directive = sphinx_shell.directive
131 if directive is None:
131 if directive is None:
132 source = 'Unavailable'
132 source = 'Unavailable'
133 content = 'Unavailable'
133 content = 'Unavailable'
134 else:
134 else:
135 source = directive.state.document.current_source
135 source = directive.state.document.current_source
136 # Add tabs and make into a single string.
136 # Add tabs and make into a single string.
137 content = '\n'.join([TAB + line for line in directive.content])
137 content = '\n'.join([TAB + line for line in directive.content])
138
138
139 if error:
139 if error:
140
140
141 e = ('doctest float comparison failure\n\n'
141 e = ('doctest float comparison failure\n\n'
142 'Document source: {0}\n\n'
142 'Document source: {0}\n\n'
143 'Raw content: \n{1}\n\n'
143 'Raw content: \n{1}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
146 'instead of the expected:\n{TAB}{4}\n\n')
146 'instead of the expected:\n{TAB}{4}\n\n')
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
148 repr(submitted), TAB=TAB)
148 repr(submitted), TAB=TAB)
149 raise RuntimeError(e)
149 raise RuntimeError(e)
150
150
151 # dict of allowable doctest handlers. The key represents the first argument
151 # dict of allowable doctest handlers. The key represents the first argument
152 # that must be given to @doctest in order to activate the handler.
152 # that must be given to @doctest in order to activate the handler.
153 doctests = {
153 doctests = {
154 'float': float_doctest,
154 'float': float_doctest,
155 }
155 }
@@ -1,1276 +1,1278
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 Sphinx directive to support embedded IPython code.
3 Sphinx directive to support embedded IPython code.
4
4
5 IPython provides an extension for `Sphinx <http://www.sphinx-doc.org/>`_ to
5 IPython provides an extension for `Sphinx <http://www.sphinx-doc.org/>`_ to
6 highlight and run code.
6 highlight and run code.
7
7
8 This directive allows pasting of entire interactive IPython sessions, prompts
8 This directive allows pasting of entire interactive IPython sessions, prompts
9 and all, and their code will actually get re-executed at doc build time, with
9 and all, and their code will actually get re-executed at doc build time, with
10 all prompts renumbered sequentially. It also allows you to input code as a pure
10 all prompts renumbered sequentially. It also allows you to input code as a pure
11 python input by giving the argument python to the directive. The output looks
11 python input by giving the argument python to the directive. The output looks
12 like an interactive ipython section.
12 like an interactive ipython section.
13
13
14 Here is an example of how the IPython directive can
14 Here is an example of how the IPython directive can
15 **run** python code, at build time.
15 **run** python code, at build time.
16
16
17 .. ipython::
17 .. ipython::
18
18
19 In [1]: 1+1
19 In [1]: 1+1
20
20
21 In [1]: import datetime
21 In [1]: import datetime
22 ...: datetime.date.fromisoformat('2022-02-22')
22 ...: datetime.date.fromisoformat('2022-02-22')
23
23
24 It supports IPython construct that plain
24 It supports IPython construct that plain
25 Python does not understand (like magics):
25 Python does not understand (like magics):
26
26
27 .. ipython::
27 .. ipython::
28
28
29 In [0]: import time
29 In [0]: import time
30
30
31 In [0]: %pdoc time.sleep
31 In [0]: %pdoc time.sleep
32
32
33 This will also support top-level async when using IPython 7.0+
33 This will also support top-level async when using IPython 7.0+
34
34
35 .. ipython::
35 .. ipython::
36
36
37 In [2]: import asyncio
37 In [2]: import asyncio
38 ...: print('before')
38 ...: print('before')
39 ...: await asyncio.sleep(1)
39 ...: await asyncio.sleep(1)
40 ...: print('after')
40 ...: print('after')
41
41
42
42
43 The namespace will persist across multiple code chucks, Let's define a variable:
43 The namespace will persist across multiple code chucks, Let's define a variable:
44
44
45 .. ipython::
45 .. ipython::
46
46
47 In [0]: who = "World"
47 In [0]: who = "World"
48
48
49 And now say hello:
49 And now say hello:
50
50
51 .. ipython::
51 .. ipython::
52
52
53 In [0]: print('Hello,', who)
53 In [0]: print('Hello,', who)
54
54
55 If the current section raises an exception, you can add the ``:okexcept:`` flag
55 If the current section raises an exception, you can add the ``:okexcept:`` flag
56 to the current block, otherwise the build will fail.
56 to the current block, otherwise the build will fail.
57
57
58 .. ipython::
58 .. ipython::
59 :okexcept:
59 :okexcept:
60
60
61 In [1]: 1/0
61 In [1]: 1/0
62
62
63 IPython Sphinx directive module
63 IPython Sphinx directive module
64 ===============================
64 ===============================
65
65
66 To enable this directive, simply list it in your Sphinx ``conf.py`` file
66 To enable this directive, simply list it in your Sphinx ``conf.py`` file
67 (making sure the directory where you placed it is visible to sphinx, as is
67 (making sure the directory where you placed it is visible to sphinx, as is
68 needed for all Sphinx directives). For example, to enable syntax highlighting
68 needed for all Sphinx directives). For example, to enable syntax highlighting
69 and the IPython directive::
69 and the IPython directive::
70
70
71 extensions = ['IPython.sphinxext.ipython_console_highlighting',
71 extensions = ['IPython.sphinxext.ipython_console_highlighting',
72 'IPython.sphinxext.ipython_directive']
72 'IPython.sphinxext.ipython_directive']
73
73
74 The IPython directive outputs code-blocks with the language 'ipython'. So
74 The IPython directive outputs code-blocks with the language 'ipython'. So
75 if you do not have the syntax highlighting extension enabled as well, then
75 if you do not have the syntax highlighting extension enabled as well, then
76 all rendered code-blocks will be uncolored. By default this directive assumes
76 all rendered code-blocks will be uncolored. By default this directive assumes
77 that your prompts are unchanged IPython ones, but this can be customized.
77 that your prompts are unchanged IPython ones, but this can be customized.
78 The configurable options that can be placed in conf.py are:
78 The configurable options that can be placed in conf.py are:
79
79
80 ipython_savefig_dir:
80 ipython_savefig_dir:
81 The directory in which to save the figures. This is relative to the
81 The directory in which to save the figures. This is relative to the
82 Sphinx source directory. The default is `html_static_path`.
82 Sphinx source directory. The default is `html_static_path`.
83 ipython_rgxin:
83 ipython_rgxin:
84 The compiled regular expression to denote the start of IPython input
84 The compiled regular expression to denote the start of IPython input
85 lines. The default is ``re.compile('In \\[(\\d+)\\]:\\s?(.*)\\s*')``. You
85 lines. The default is ``re.compile('In \\[(\\d+)\\]:\\s?(.*)\\s*')``. You
86 shouldn't need to change this.
86 shouldn't need to change this.
87 ipython_warning_is_error: [default to True]
87 ipython_warning_is_error: [default to True]
88 Fail the build if something unexpected happen, for example if a block raise
88 Fail the build if something unexpected happen, for example if a block raise
89 an exception but does not have the `:okexcept:` flag. The exact behavior of
89 an exception but does not have the `:okexcept:` flag. The exact behavior of
90 what is considered strict, may change between the sphinx directive version.
90 what is considered strict, may change between the sphinx directive version.
91 ipython_rgxout:
91 ipython_rgxout:
92 The compiled regular expression to denote the start of IPython output
92 The compiled regular expression to denote the start of IPython output
93 lines. The default is ``re.compile('Out\\[(\\d+)\\]:\\s?(.*)\\s*')``. You
93 lines. The default is ``re.compile('Out\\[(\\d+)\\]:\\s?(.*)\\s*')``. You
94 shouldn't need to change this.
94 shouldn't need to change this.
95 ipython_promptin:
95 ipython_promptin:
96 The string to represent the IPython input prompt in the generated ReST.
96 The string to represent the IPython input prompt in the generated ReST.
97 The default is ``'In [%d]:'``. This expects that the line numbers are used
97 The default is ``'In [%d]:'``. This expects that the line numbers are used
98 in the prompt.
98 in the prompt.
99 ipython_promptout:
99 ipython_promptout:
100 The string to represent the IPython prompt in the generated ReST. The
100 The string to represent the IPython prompt in the generated ReST. The
101 default is ``'Out [%d]:'``. This expects that the line numbers are used
101 default is ``'Out [%d]:'``. This expects that the line numbers are used
102 in the prompt.
102 in the prompt.
103 ipython_mplbackend:
103 ipython_mplbackend:
104 The string which specifies if the embedded Sphinx shell should import
104 The string which specifies if the embedded Sphinx shell should import
105 Matplotlib and set the backend. The value specifies a backend that is
105 Matplotlib and set the backend. The value specifies a backend that is
106 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
106 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
107 executed. If not specified in conf.py, then the default value of 'agg' is
107 executed. If not specified in conf.py, then the default value of 'agg' is
108 used. To use the IPython directive without matplotlib as a dependency, set
108 used. To use the IPython directive without matplotlib as a dependency, set
109 the value to `None`. It may end up that matplotlib is still imported
109 the value to `None`. It may end up that matplotlib is still imported
110 if the user specifies so in `ipython_execlines` or makes use of the
110 if the user specifies so in `ipython_execlines` or makes use of the
111 @savefig pseudo decorator.
111 @savefig pseudo decorator.
112 ipython_execlines:
112 ipython_execlines:
113 A list of strings to be exec'd in the embedded Sphinx shell. Typical
113 A list of strings to be exec'd in the embedded Sphinx shell. Typical
114 usage is to make certain packages always available. Set this to an empty
114 usage is to make certain packages always available. Set this to an empty
115 list if you wish to have no imports always available. If specified in
115 list if you wish to have no imports always available. If specified in
116 ``conf.py`` as `None`, then it has the effect of making no imports available.
116 ``conf.py`` as `None`, then it has the effect of making no imports available.
117 If omitted from conf.py altogether, then the default value of
117 If omitted from conf.py altogether, then the default value of
118 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
118 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
119 ipython_holdcount
119 ipython_holdcount
120 When the @suppress pseudo-decorator is used, the execution count can be
120 When the @suppress pseudo-decorator is used, the execution count can be
121 incremented or not. The default behavior is to hold the execution count,
121 incremented or not. The default behavior is to hold the execution count,
122 corresponding to a value of `True`. Set this to `False` to increment
122 corresponding to a value of `True`. Set this to `False` to increment
123 the execution count after each suppressed command.
123 the execution count after each suppressed command.
124
124
125 As an example, to use the IPython directive when `matplotlib` is not available,
125 As an example, to use the IPython directive when `matplotlib` is not available,
126 one sets the backend to `None`::
126 one sets the backend to `None`::
127
127
128 ipython_mplbackend = None
128 ipython_mplbackend = None
129
129
130 An example usage of the directive is:
130 An example usage of the directive is:
131
131
132 .. code-block:: rst
132 .. code-block:: rst
133
133
134 .. ipython::
134 .. ipython::
135
135
136 In [1]: x = 1
136 In [1]: x = 1
137
137
138 In [2]: y = x**2
138 In [2]: y = x**2
139
139
140 In [3]: print(y)
140 In [3]: print(y)
141
141
142 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
142 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
143 documentation.
143 documentation.
144
144
145 Pseudo-Decorators
145 Pseudo-Decorators
146 =================
146 =================
147
147
148 Note: Only one decorator is supported per input. If more than one decorator
148 Note: Only one decorator is supported per input. If more than one decorator
149 is specified, then only the last one is used.
149 is specified, then only the last one is used.
150
150
151 In addition to the Pseudo-Decorators/options described at the above link,
151 In addition to the Pseudo-Decorators/options described at the above link,
152 several enhancements have been made. The directive will emit a message to the
152 several enhancements have been made. The directive will emit a message to the
153 console at build-time if code-execution resulted in an exception or warning.
153 console at build-time if code-execution resulted in an exception or warning.
154 You can suppress these on a per-block basis by specifying the :okexcept:
154 You can suppress these on a per-block basis by specifying the :okexcept:
155 or :okwarning: options:
155 or :okwarning: options:
156
156
157 .. code-block:: rst
157 .. code-block:: rst
158
158
159 .. ipython::
159 .. ipython::
160 :okexcept:
160 :okexcept:
161 :okwarning:
161 :okwarning:
162
162
163 In [1]: 1/0
163 In [1]: 1/0
164 In [2]: # raise warning.
164 In [2]: # raise warning.
165
165
166 To Do
166 To Do
167 =====
167 =====
168
168
169 - Turn the ad-hoc test() function into a real test suite.
169 - Turn the ad-hoc test() function into a real test suite.
170 - Break up ipython-specific functionality from matplotlib stuff into better
170 - Break up ipython-specific functionality from matplotlib stuff into better
171 separated code.
171 separated code.
172
172
173 """
173 """
174
174
175 # Authors
175 # Authors
176 # =======
176 # =======
177 #
177 #
178 # - John D Hunter: original author.
178 # - John D Hunter: original author.
179 # - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
179 # - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
180 # - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
180 # - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
181 # - Skipper Seabold, refactoring, cleanups, pure python addition
181 # - Skipper Seabold, refactoring, cleanups, pure python addition
182
182
183 #-----------------------------------------------------------------------------
183 #-----------------------------------------------------------------------------
184 # Imports
184 # Imports
185 #-----------------------------------------------------------------------------
185 #-----------------------------------------------------------------------------
186
186
187 # Stdlib
187 # Stdlib
188 import atexit
188 import atexit
189 import errno
189 import errno
190 import os
190 import os
191 import pathlib
191 import pathlib
192 import re
192 import re
193 import sys
193 import sys
194 import tempfile
194 import tempfile
195 import ast
195 import ast
196 import warnings
196 import warnings
197 import shutil
197 import shutil
198 from io import StringIO
198 from io import StringIO
199 from typing import Any, Dict, Set
199
200
200 # Third-party
201 # Third-party
201 from docutils.parsers.rst import directives
202 from docutils.parsers.rst import directives
202 from docutils.parsers.rst import Directive
203 from docutils.parsers.rst import Directive
203 from sphinx.util import logging
204 from sphinx.util import logging
204
205
205 # Our own
206 # Our own
206 from traitlets.config import Config
207 from traitlets.config import Config
207 from IPython import InteractiveShell
208 from IPython import InteractiveShell
208 from IPython.core.profiledir import ProfileDir
209 from IPython.core.profiledir import ProfileDir
209
210
210 use_matplotlib = False
211 use_matplotlib = False
211 try:
212 try:
212 import matplotlib
213 import matplotlib
213 use_matplotlib = True
214 use_matplotlib = True
214 except Exception:
215 except Exception:
215 pass
216 pass
216
217
217 #-----------------------------------------------------------------------------
218 #-----------------------------------------------------------------------------
218 # Globals
219 # Globals
219 #-----------------------------------------------------------------------------
220 #-----------------------------------------------------------------------------
220 # for tokenizing blocks
221 # for tokenizing blocks
221 COMMENT, INPUT, OUTPUT = range(3)
222 COMMENT, INPUT, OUTPUT = range(3)
222
223
223 PSEUDO_DECORATORS = ["suppress", "verbatim", "savefig", "doctest"]
224 PSEUDO_DECORATORS = ["suppress", "verbatim", "savefig", "doctest"]
224
225
225 #-----------------------------------------------------------------------------
226 #-----------------------------------------------------------------------------
226 # Functions and class declarations
227 # Functions and class declarations
227 #-----------------------------------------------------------------------------
228 #-----------------------------------------------------------------------------
228
229
229 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
230 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
230 """
231 """
231 part is a string of ipython text, comprised of at most one
232 part is a string of ipython text, comprised of at most one
232 input, one output, comments, and blank lines. The block parser
233 input, one output, comments, and blank lines. The block parser
233 parses the text into a list of::
234 parses the text into a list of::
234
235
235 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
236 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
236
237
237 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
238 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
238 data is, depending on the type of token::
239 data is, depending on the type of token::
239
240
240 COMMENT : the comment string
241 COMMENT : the comment string
241
242
242 INPUT: the (DECORATOR, INPUT_LINE, REST) where
243 INPUT: the (DECORATOR, INPUT_LINE, REST) where
243 DECORATOR: the input decorator (or None)
244 DECORATOR: the input decorator (or None)
244 INPUT_LINE: the input as string (possibly multi-line)
245 INPUT_LINE: the input as string (possibly multi-line)
245 REST : any stdout generated by the input line (not OUTPUT)
246 REST : any stdout generated by the input line (not OUTPUT)
246
247
247 OUTPUT: the output string, possibly multi-line
248 OUTPUT: the output string, possibly multi-line
248
249
249 """
250 """
250 block = []
251 block = []
251 lines = part.split('\n')
252 lines = part.split('\n')
252 N = len(lines)
253 N = len(lines)
253 i = 0
254 i = 0
254 decorator = None
255 decorator = None
255 while 1:
256 while 1:
256
257
257 if i==N:
258 if i==N:
258 # nothing left to parse -- the last line
259 # nothing left to parse -- the last line
259 break
260 break
260
261
261 line = lines[i]
262 line = lines[i]
262 i += 1
263 i += 1
263 line_stripped = line.strip()
264 line_stripped = line.strip()
264 if line_stripped.startswith('#'):
265 if line_stripped.startswith('#'):
265 block.append((COMMENT, line))
266 block.append((COMMENT, line))
266 continue
267 continue
267
268
268 if any(
269 if any(
269 line_stripped.startswith("@" + pseudo_decorator)
270 line_stripped.startswith("@" + pseudo_decorator)
270 for pseudo_decorator in PSEUDO_DECORATORS
271 for pseudo_decorator in PSEUDO_DECORATORS
271 ):
272 ):
272 if decorator:
273 if decorator:
273 raise RuntimeError(
274 raise RuntimeError(
274 "Applying multiple pseudo-decorators on one line is not supported"
275 "Applying multiple pseudo-decorators on one line is not supported"
275 )
276 )
276 else:
277 else:
277 decorator = line_stripped
278 decorator = line_stripped
278 continue
279 continue
279
280
280 # does this look like an input line?
281 # does this look like an input line?
281 matchin = rgxin.match(line)
282 matchin = rgxin.match(line)
282 if matchin:
283 if matchin:
283 lineno, inputline = int(matchin.group(1)), matchin.group(2)
284 lineno, inputline = int(matchin.group(1)), matchin.group(2)
284
285
285 # the ....: continuation string
286 # the ....: continuation string
286 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
287 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
287 Nc = len(continuation)
288 Nc = len(continuation)
288 # input lines can continue on for more than one line, if
289 # input lines can continue on for more than one line, if
289 # we have a '\' line continuation char or a function call
290 # we have a '\' line continuation char or a function call
290 # echo line 'print'. The input line can only be
291 # echo line 'print'. The input line can only be
291 # terminated by the end of the block or an output line, so
292 # terminated by the end of the block or an output line, so
292 # we parse out the rest of the input line if it is
293 # we parse out the rest of the input line if it is
293 # multiline as well as any echo text
294 # multiline as well as any echo text
294
295
295 rest = []
296 rest = []
296 while i<N:
297 while i<N:
297
298
298 # look ahead; if the next line is blank, or a comment, or
299 # look ahead; if the next line is blank, or a comment, or
299 # an output line, we're done
300 # an output line, we're done
300
301
301 nextline = lines[i]
302 nextline = lines[i]
302 matchout = rgxout.match(nextline)
303 matchout = rgxout.match(nextline)
303 # print("nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation)))
304 # print("nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation)))
304 if matchout or nextline.startswith('#'):
305 if matchout or nextline.startswith('#'):
305 break
306 break
306 elif nextline.startswith(continuation):
307 elif nextline.startswith(continuation):
307 # The default ipython_rgx* treat the space following the colon as optional.
308 # The default ipython_rgx* treat the space following the colon as optional.
308 # However, If the space is there we must consume it or code
309 # However, If the space is there we must consume it or code
309 # employing the cython_magic extension will fail to execute.
310 # employing the cython_magic extension will fail to execute.
310 #
311 #
311 # This works with the default ipython_rgx* patterns,
312 # This works with the default ipython_rgx* patterns,
312 # If you modify them, YMMV.
313 # If you modify them, YMMV.
313 nextline = nextline[Nc:]
314 nextline = nextline[Nc:]
314 if nextline and nextline[0] == ' ':
315 if nextline and nextline[0] == ' ':
315 nextline = nextline[1:]
316 nextline = nextline[1:]
316
317
317 inputline += '\n' + nextline
318 inputline += '\n' + nextline
318 else:
319 else:
319 rest.append(nextline)
320 rest.append(nextline)
320 i+= 1
321 i+= 1
321
322
322 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
323 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
323 continue
324 continue
324
325
325 # if it looks like an output line grab all the text to the end
326 # if it looks like an output line grab all the text to the end
326 # of the block
327 # of the block
327 matchout = rgxout.match(line)
328 matchout = rgxout.match(line)
328 if matchout:
329 if matchout:
329 lineno, output = int(matchout.group(1)), matchout.group(2)
330 lineno, output = int(matchout.group(1)), matchout.group(2)
330 if i<N-1:
331 if i<N-1:
331 output = '\n'.join([output] + lines[i:])
332 output = '\n'.join([output] + lines[i:])
332
333
333 block.append((OUTPUT, output))
334 block.append((OUTPUT, output))
334 break
335 break
335
336
336 return block
337 return block
337
338
338
339
339 class EmbeddedSphinxShell(object):
340 class EmbeddedSphinxShell(object):
340 """An embedded IPython instance to run inside Sphinx"""
341 """An embedded IPython instance to run inside Sphinx"""
341
342
342 def __init__(self, exec_lines=None):
343 def __init__(self, exec_lines=None):
343
344
344 self.cout = StringIO()
345 self.cout = StringIO()
345
346
346 if exec_lines is None:
347 if exec_lines is None:
347 exec_lines = []
348 exec_lines = []
348
349
349 # Create config object for IPython
350 # Create config object for IPython
350 config = Config()
351 config = Config()
351 config.HistoryManager.hist_file = ':memory:'
352 config.HistoryManager.hist_file = ':memory:'
352 config.InteractiveShell.autocall = False
353 config.InteractiveShell.autocall = False
353 config.InteractiveShell.autoindent = False
354 config.InteractiveShell.autoindent = False
354 config.InteractiveShell.colors = 'NoColor'
355 config.InteractiveShell.colors = 'NoColor'
355
356
356 # create a profile so instance history isn't saved
357 # create a profile so instance history isn't saved
357 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
358 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
358 profname = 'auto_profile_sphinx_build'
359 profname = 'auto_profile_sphinx_build'
359 pdir = os.path.join(tmp_profile_dir,profname)
360 pdir = os.path.join(tmp_profile_dir,profname)
360 profile = ProfileDir.create_profile_dir(pdir)
361 profile = ProfileDir.create_profile_dir(pdir)
361
362
362 # Create and initialize global ipython, but don't start its mainloop.
363 # Create and initialize global ipython, but don't start its mainloop.
363 # This will persist across different EmbeddedSphinxShell instances.
364 # This will persist across different EmbeddedSphinxShell instances.
364 IP = InteractiveShell.instance(config=config, profile_dir=profile)
365 IP = InteractiveShell.instance(config=config, profile_dir=profile)
365 atexit.register(self.cleanup)
366 atexit.register(self.cleanup)
366
367
367 # Store a few parts of IPython we'll need.
368 # Store a few parts of IPython we'll need.
368 self.IP = IP
369 self.IP = IP
369 self.user_ns = self.IP.user_ns
370 self.user_ns = self.IP.user_ns
370 self.user_global_ns = self.IP.user_global_ns
371 self.user_global_ns = self.IP.user_global_ns
371
372
372 self.input = ''
373 self.input = ''
373 self.output = ''
374 self.output = ''
374 self.tmp_profile_dir = tmp_profile_dir
375 self.tmp_profile_dir = tmp_profile_dir
375
376
376 self.is_verbatim = False
377 self.is_verbatim = False
377 self.is_doctest = False
378 self.is_doctest = False
378 self.is_suppress = False
379 self.is_suppress = False
379
380
380 # Optionally, provide more detailed information to shell.
381 # Optionally, provide more detailed information to shell.
381 # this is assigned by the SetUp method of IPythonDirective
382 # this is assigned by the SetUp method of IPythonDirective
382 # to point at itself.
383 # to point at itself.
383 #
384 #
384 # So, you can access handy things at self.directive.state
385 # So, you can access handy things at self.directive.state
385 self.directive = None
386 self.directive = None
386
387
387 # on the first call to the savefig decorator, we'll import
388 # on the first call to the savefig decorator, we'll import
388 # pyplot as plt so we can make a call to the plt.gcf().savefig
389 # pyplot as plt so we can make a call to the plt.gcf().savefig
389 self._pyplot_imported = False
390 self._pyplot_imported = False
390
391
391 # Prepopulate the namespace.
392 # Prepopulate the namespace.
392 for line in exec_lines:
393 for line in exec_lines:
393 self.process_input_line(line, store_history=False)
394 self.process_input_line(line, store_history=False)
394
395
395 def cleanup(self):
396 def cleanup(self):
396 shutil.rmtree(self.tmp_profile_dir, ignore_errors=True)
397 shutil.rmtree(self.tmp_profile_dir, ignore_errors=True)
397
398
398 def clear_cout(self):
399 def clear_cout(self):
399 self.cout.seek(0)
400 self.cout.seek(0)
400 self.cout.truncate(0)
401 self.cout.truncate(0)
401
402
402 def process_input_line(self, line, store_history):
403 def process_input_line(self, line, store_history):
403 return self.process_input_lines([line], store_history=store_history)
404 return self.process_input_lines([line], store_history=store_history)
404
405
405 def process_input_lines(self, lines, store_history=True):
406 def process_input_lines(self, lines, store_history=True):
406 """process the input, capturing stdout"""
407 """process the input, capturing stdout"""
407 stdout = sys.stdout
408 stdout = sys.stdout
408 source_raw = '\n'.join(lines)
409 source_raw = '\n'.join(lines)
409 try:
410 try:
410 sys.stdout = self.cout
411 sys.stdout = self.cout
411 self.IP.run_cell(source_raw, store_history=store_history)
412 self.IP.run_cell(source_raw, store_history=store_history)
412 finally:
413 finally:
413 sys.stdout = stdout
414 sys.stdout = stdout
414
415
415 def process_image(self, decorator):
416 def process_image(self, decorator):
416 """
417 """
417 # build out an image directive like
418 # build out an image directive like
418 # .. image:: somefile.png
419 # .. image:: somefile.png
419 # :width 4in
420 # :width 4in
420 #
421 #
421 # from an input like
422 # from an input like
422 # savefig somefile.png width=4in
423 # savefig somefile.png width=4in
423 """
424 """
424 savefig_dir = self.savefig_dir
425 savefig_dir = self.savefig_dir
425 source_dir = self.source_dir
426 source_dir = self.source_dir
426 saveargs = decorator.split(' ')
427 saveargs = decorator.split(' ')
427 filename = saveargs[1]
428 filename = saveargs[1]
428 # insert relative path to image file in source
429 # insert relative path to image file in source
429 # as absolute path for Sphinx
430 # as absolute path for Sphinx
430 # sphinx expects a posix path, even on Windows
431 # sphinx expects a posix path, even on Windows
431 path = pathlib.Path(savefig_dir, filename)
432 path = pathlib.Path(savefig_dir, filename)
432 outfile = '/' + path.relative_to(source_dir).as_posix()
433 outfile = '/' + path.relative_to(source_dir).as_posix()
433
434
434 imagerows = ['.. image:: %s' % outfile]
435 imagerows = ['.. image:: %s' % outfile]
435
436
436 for kwarg in saveargs[2:]:
437 for kwarg in saveargs[2:]:
437 arg, val = kwarg.split('=')
438 arg, val = kwarg.split('=')
438 arg = arg.strip()
439 arg = arg.strip()
439 val = val.strip()
440 val = val.strip()
440 imagerows.append(' :%s: %s'%(arg, val))
441 imagerows.append(' :%s: %s'%(arg, val))
441
442
442 image_file = os.path.basename(outfile) # only return file name
443 image_file = os.path.basename(outfile) # only return file name
443 image_directive = '\n'.join(imagerows)
444 image_directive = '\n'.join(imagerows)
444 return image_file, image_directive
445 return image_file, image_directive
445
446
446 # Callbacks for each type of token
447 # Callbacks for each type of token
447 def process_input(self, data, input_prompt, lineno):
448 def process_input(self, data, input_prompt, lineno):
448 """
449 """
449 Process data block for INPUT token.
450 Process data block for INPUT token.
450
451
451 """
452 """
452 decorator, input, rest = data
453 decorator, input, rest = data
453 image_file = None
454 image_file = None
454 image_directive = None
455 image_directive = None
455
456
456 is_verbatim = decorator=='@verbatim' or self.is_verbatim
457 is_verbatim = decorator=='@verbatim' or self.is_verbatim
457 is_doctest = (decorator is not None and \
458 is_doctest = (decorator is not None and \
458 decorator.startswith('@doctest')) or self.is_doctest
459 decorator.startswith('@doctest')) or self.is_doctest
459 is_suppress = decorator=='@suppress' or self.is_suppress
460 is_suppress = decorator=='@suppress' or self.is_suppress
460 is_okexcept = decorator=='@okexcept' or self.is_okexcept
461 is_okexcept = decorator=='@okexcept' or self.is_okexcept
461 is_okwarning = decorator=='@okwarning' or self.is_okwarning
462 is_okwarning = decorator=='@okwarning' or self.is_okwarning
462 is_savefig = decorator is not None and \
463 is_savefig = decorator is not None and \
463 decorator.startswith('@savefig')
464 decorator.startswith('@savefig')
464
465
465 input_lines = input.split('\n')
466 input_lines = input.split('\n')
466 if len(input_lines) > 1:
467 if len(input_lines) > 1:
467 if input_lines[-1] != "":
468 if input_lines[-1] != "":
468 input_lines.append('') # make sure there's a blank line
469 input_lines.append('') # make sure there's a blank line
469 # so splitter buffer gets reset
470 # so splitter buffer gets reset
470
471
471 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
472 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
472
473
473 if is_savefig:
474 if is_savefig:
474 image_file, image_directive = self.process_image(decorator)
475 image_file, image_directive = self.process_image(decorator)
475
476
476 ret = []
477 ret = []
477 is_semicolon = False
478 is_semicolon = False
478
479
479 # Hold the execution count, if requested to do so.
480 # Hold the execution count, if requested to do so.
480 if is_suppress and self.hold_count:
481 if is_suppress and self.hold_count:
481 store_history = False
482 store_history = False
482 else:
483 else:
483 store_history = True
484 store_history = True
484
485
485 # Note: catch_warnings is not thread safe
486 # Note: catch_warnings is not thread safe
486 with warnings.catch_warnings(record=True) as ws:
487 with warnings.catch_warnings(record=True) as ws:
487 if input_lines[0].endswith(';'):
488 if input_lines[0].endswith(';'):
488 is_semicolon = True
489 is_semicolon = True
489 #for i, line in enumerate(input_lines):
490 #for i, line in enumerate(input_lines):
490
491
491 # process the first input line
492 # process the first input line
492 if is_verbatim:
493 if is_verbatim:
493 self.process_input_lines([''])
494 self.process_input_lines([''])
494 self.IP.execution_count += 1 # increment it anyway
495 self.IP.execution_count += 1 # increment it anyway
495 else:
496 else:
496 # only submit the line in non-verbatim mode
497 # only submit the line in non-verbatim mode
497 self.process_input_lines(input_lines, store_history=store_history)
498 self.process_input_lines(input_lines, store_history=store_history)
498
499
499 if not is_suppress:
500 if not is_suppress:
500 for i, line in enumerate(input_lines):
501 for i, line in enumerate(input_lines):
501 if i == 0:
502 if i == 0:
502 formatted_line = '%s %s'%(input_prompt, line)
503 formatted_line = '%s %s'%(input_prompt, line)
503 else:
504 else:
504 formatted_line = '%s %s'%(continuation, line)
505 formatted_line = '%s %s'%(continuation, line)
505 ret.append(formatted_line)
506 ret.append(formatted_line)
506
507
507 if not is_suppress and len(rest.strip()) and is_verbatim:
508 if not is_suppress and len(rest.strip()) and is_verbatim:
508 # The "rest" is the standard output of the input. This needs to be
509 # The "rest" is the standard output of the input. This needs to be
509 # added when in verbatim mode. If there is no "rest", then we don't
510 # added when in verbatim mode. If there is no "rest", then we don't
510 # add it, as the new line will be added by the processed output.
511 # add it, as the new line will be added by the processed output.
511 ret.append(rest)
512 ret.append(rest)
512
513
513 # Fetch the processed output. (This is not the submitted output.)
514 # Fetch the processed output. (This is not the submitted output.)
514 self.cout.seek(0)
515 self.cout.seek(0)
515 processed_output = self.cout.read()
516 processed_output = self.cout.read()
516 if not is_suppress and not is_semicolon:
517 if not is_suppress and not is_semicolon:
517 #
518 #
518 # In IPythonDirective.run, the elements of `ret` are eventually
519 # In IPythonDirective.run, the elements of `ret` are eventually
519 # combined such that '' entries correspond to newlines. So if
520 # combined such that '' entries correspond to newlines. So if
520 # `processed_output` is equal to '', then the adding it to `ret`
521 # `processed_output` is equal to '', then the adding it to `ret`
521 # ensures that there is a blank line between consecutive inputs
522 # ensures that there is a blank line between consecutive inputs
522 # that have no outputs, as in:
523 # that have no outputs, as in:
523 #
524 #
524 # In [1]: x = 4
525 # In [1]: x = 4
525 #
526 #
526 # In [2]: x = 5
527 # In [2]: x = 5
527 #
528 #
528 # When there is processed output, it has a '\n' at the tail end. So
529 # When there is processed output, it has a '\n' at the tail end. So
529 # adding the output to `ret` will provide the necessary spacing
530 # adding the output to `ret` will provide the necessary spacing
530 # between consecutive input/output blocks, as in:
531 # between consecutive input/output blocks, as in:
531 #
532 #
532 # In [1]: x
533 # In [1]: x
533 # Out[1]: 5
534 # Out[1]: 5
534 #
535 #
535 # In [2]: x
536 # In [2]: x
536 # Out[2]: 5
537 # Out[2]: 5
537 #
538 #
538 # When there is stdout from the input, it also has a '\n' at the
539 # When there is stdout from the input, it also has a '\n' at the
539 # tail end, and so this ensures proper spacing as well. E.g.:
540 # tail end, and so this ensures proper spacing as well. E.g.:
540 #
541 #
541 # In [1]: print(x)
542 # In [1]: print(x)
542 # 5
543 # 5
543 #
544 #
544 # In [2]: x = 5
545 # In [2]: x = 5
545 #
546 #
546 # When in verbatim mode, `processed_output` is empty (because
547 # When in verbatim mode, `processed_output` is empty (because
547 # nothing was passed to IP. Sometimes the submitted code block has
548 # nothing was passed to IP. Sometimes the submitted code block has
548 # an Out[] portion and sometimes it does not. When it does not, we
549 # an Out[] portion and sometimes it does not. When it does not, we
549 # need to ensure proper spacing, so we have to add '' to `ret`.
550 # need to ensure proper spacing, so we have to add '' to `ret`.
550 # However, if there is an Out[] in the submitted code, then we do
551 # However, if there is an Out[] in the submitted code, then we do
551 # not want to add a newline as `process_output` has stuff to add.
552 # not want to add a newline as `process_output` has stuff to add.
552 # The difficulty is that `process_input` doesn't know if
553 # The difficulty is that `process_input` doesn't know if
553 # `process_output` will be called---so it doesn't know if there is
554 # `process_output` will be called---so it doesn't know if there is
554 # Out[] in the code block. The requires that we include a hack in
555 # Out[] in the code block. The requires that we include a hack in
555 # `process_block`. See the comments there.
556 # `process_block`. See the comments there.
556 #
557 #
557 ret.append(processed_output)
558 ret.append(processed_output)
558 elif is_semicolon:
559 elif is_semicolon:
559 # Make sure there is a newline after the semicolon.
560 # Make sure there is a newline after the semicolon.
560 ret.append('')
561 ret.append('')
561
562
562 # context information
563 # context information
563 filename = "Unknown"
564 filename = "Unknown"
564 lineno = 0
565 lineno = 0
565 if self.directive.state:
566 if self.directive.state:
566 filename = self.directive.state.document.current_source
567 filename = self.directive.state.document.current_source
567 lineno = self.directive.state.document.current_line
568 lineno = self.directive.state.document.current_line
568
569
569 # Use sphinx logger for warnings
570 # Use sphinx logger for warnings
570 logger = logging.getLogger(__name__)
571 logger = logging.getLogger(__name__)
571
572
572 # output any exceptions raised during execution to stdout
573 # output any exceptions raised during execution to stdout
573 # unless :okexcept: has been specified.
574 # unless :okexcept: has been specified.
574 if not is_okexcept and (
575 if not is_okexcept and (
575 ("Traceback" in processed_output) or ("SyntaxError" in processed_output)
576 ("Traceback" in processed_output) or ("SyntaxError" in processed_output)
576 ):
577 ):
577 s = "\n>>>" + ("-" * 73) + "\n"
578 s = "\n>>>" + ("-" * 73) + "\n"
578 s += "Exception in %s at block ending on line %s\n" % (filename, lineno)
579 s += "Exception in %s at block ending on line %s\n" % (filename, lineno)
579 s += "Specify :okexcept: as an option in the ipython:: block to suppress this message\n"
580 s += "Specify :okexcept: as an option in the ipython:: block to suppress this message\n"
580 s += processed_output + "\n"
581 s += processed_output + "\n"
581 s += "<<<" + ("-" * 73)
582 s += "<<<" + ("-" * 73)
582 logger.warning(s)
583 logger.warning(s)
583 if self.warning_is_error:
584 if self.warning_is_error:
584 raise RuntimeError(
585 raise RuntimeError(
585 "Unexpected exception in `{}` line {}".format(filename, lineno)
586 "Unexpected exception in `{}` line {}".format(filename, lineno)
586 )
587 )
587
588
588 # output any warning raised during execution to stdout
589 # output any warning raised during execution to stdout
589 # unless :okwarning: has been specified.
590 # unless :okwarning: has been specified.
590 if not is_okwarning:
591 if not is_okwarning:
591 for w in ws:
592 for w in ws:
592 s = "\n>>>" + ("-" * 73) + "\n"
593 s = "\n>>>" + ("-" * 73) + "\n"
593 s += "Warning in %s at block ending on line %s\n" % (filename, lineno)
594 s += "Warning in %s at block ending on line %s\n" % (filename, lineno)
594 s += "Specify :okwarning: as an option in the ipython:: block to suppress this message\n"
595 s += "Specify :okwarning: as an option in the ipython:: block to suppress this message\n"
595 s += ("-" * 76) + "\n"
596 s += ("-" * 76) + "\n"
596 s += warnings.formatwarning(
597 s += warnings.formatwarning(
597 w.message, w.category, w.filename, w.lineno, w.line
598 w.message, w.category, w.filename, w.lineno, w.line
598 )
599 )
599 s += "<<<" + ("-" * 73)
600 s += "<<<" + ("-" * 73)
600 logger.warning(s)
601 logger.warning(s)
601 if self.warning_is_error:
602 if self.warning_is_error:
602 raise RuntimeError(
603 raise RuntimeError(
603 "Unexpected warning in `{}` line {}".format(filename, lineno)
604 "Unexpected warning in `{}` line {}".format(filename, lineno)
604 )
605 )
605
606
606 self.clear_cout()
607 self.clear_cout()
607 return (ret, input_lines, processed_output,
608 return (ret, input_lines, processed_output,
608 is_doctest, decorator, image_file, image_directive)
609 is_doctest, decorator, image_file, image_directive)
609
610
610
611
611 def process_output(self, data, output_prompt, input_lines, output,
612 def process_output(self, data, output_prompt, input_lines, output,
612 is_doctest, decorator, image_file):
613 is_doctest, decorator, image_file):
613 """
614 """
614 Process data block for OUTPUT token.
615 Process data block for OUTPUT token.
615
616
616 """
617 """
617 # Recall: `data` is the submitted output, and `output` is the processed
618 # Recall: `data` is the submitted output, and `output` is the processed
618 # output from `input_lines`.
619 # output from `input_lines`.
619
620
620 TAB = ' ' * 4
621 TAB = ' ' * 4
621
622
622 if is_doctest and output is not None:
623 if is_doctest and output is not None:
623
624
624 found = output # This is the processed output
625 found = output # This is the processed output
625 found = found.strip()
626 found = found.strip()
626 submitted = data.strip()
627 submitted = data.strip()
627
628
628 if self.directive is None:
629 if self.directive is None:
629 source = 'Unavailable'
630 source = 'Unavailable'
630 content = 'Unavailable'
631 content = 'Unavailable'
631 else:
632 else:
632 source = self.directive.state.document.current_source
633 source = self.directive.state.document.current_source
633 content = self.directive.content
634 content = self.directive.content
634 # Add tabs and join into a single string.
635 # Add tabs and join into a single string.
635 content = '\n'.join([TAB + line for line in content])
636 content = '\n'.join([TAB + line for line in content])
636
637
637 # Make sure the output contains the output prompt.
638 # Make sure the output contains the output prompt.
638 ind = found.find(output_prompt)
639 ind = found.find(output_prompt)
639 if ind < 0:
640 if ind < 0:
640 e = ('output does not contain output prompt\n\n'
641 e = ('output does not contain output prompt\n\n'
641 'Document source: {0}\n\n'
642 'Document source: {0}\n\n'
642 'Raw content: \n{1}\n\n'
643 'Raw content: \n{1}\n\n'
643 'Input line(s):\n{TAB}{2}\n\n'
644 'Input line(s):\n{TAB}{2}\n\n'
644 'Output line(s):\n{TAB}{3}\n\n')
645 'Output line(s):\n{TAB}{3}\n\n')
645 e = e.format(source, content, '\n'.join(input_lines),
646 e = e.format(source, content, '\n'.join(input_lines),
646 repr(found), TAB=TAB)
647 repr(found), TAB=TAB)
647 raise RuntimeError(e)
648 raise RuntimeError(e)
648 found = found[len(output_prompt):].strip()
649 found = found[len(output_prompt):].strip()
649
650
650 # Handle the actual doctest comparison.
651 # Handle the actual doctest comparison.
651 if decorator.strip() == '@doctest':
652 if decorator.strip() == '@doctest':
652 # Standard doctest
653 # Standard doctest
653 if found != submitted:
654 if found != submitted:
654 e = ('doctest failure\n\n'
655 e = ('doctest failure\n\n'
655 'Document source: {0}\n\n'
656 'Document source: {0}\n\n'
656 'Raw content: \n{1}\n\n'
657 'Raw content: \n{1}\n\n'
657 'On input line(s):\n{TAB}{2}\n\n'
658 'On input line(s):\n{TAB}{2}\n\n'
658 'we found output:\n{TAB}{3}\n\n'
659 'we found output:\n{TAB}{3}\n\n'
659 'instead of the expected:\n{TAB}{4}\n\n')
660 'instead of the expected:\n{TAB}{4}\n\n')
660 e = e.format(source, content, '\n'.join(input_lines),
661 e = e.format(source, content, '\n'.join(input_lines),
661 repr(found), repr(submitted), TAB=TAB)
662 repr(found), repr(submitted), TAB=TAB)
662 raise RuntimeError(e)
663 raise RuntimeError(e)
663 else:
664 else:
664 self.custom_doctest(decorator, input_lines, found, submitted)
665 self.custom_doctest(decorator, input_lines, found, submitted)
665
666
666 # When in verbatim mode, this holds additional submitted output
667 # When in verbatim mode, this holds additional submitted output
667 # to be written in the final Sphinx output.
668 # to be written in the final Sphinx output.
668 # https://github.com/ipython/ipython/issues/5776
669 # https://github.com/ipython/ipython/issues/5776
669 out_data = []
670 out_data = []
670
671
671 is_verbatim = decorator=='@verbatim' or self.is_verbatim
672 is_verbatim = decorator=='@verbatim' or self.is_verbatim
672 if is_verbatim and data.strip():
673 if is_verbatim and data.strip():
673 # Note that `ret` in `process_block` has '' as its last element if
674 # Note that `ret` in `process_block` has '' as its last element if
674 # the code block was in verbatim mode. So if there is no submitted
675 # the code block was in verbatim mode. So if there is no submitted
675 # output, then we will have proper spacing only if we do not add
676 # output, then we will have proper spacing only if we do not add
676 # an additional '' to `out_data`. This is why we condition on
677 # an additional '' to `out_data`. This is why we condition on
677 # `and data.strip()`.
678 # `and data.strip()`.
678
679
679 # The submitted output has no output prompt. If we want the
680 # The submitted output has no output prompt. If we want the
680 # prompt and the code to appear, we need to join them now
681 # prompt and the code to appear, we need to join them now
681 # instead of adding them separately---as this would create an
682 # instead of adding them separately---as this would create an
682 # undesired newline. How we do this ultimately depends on the
683 # undesired newline. How we do this ultimately depends on the
683 # format of the output regex. I'll do what works for the default
684 # format of the output regex. I'll do what works for the default
684 # prompt for now, and we might have to adjust if it doesn't work
685 # prompt for now, and we might have to adjust if it doesn't work
685 # in other cases. Finally, the submitted output does not have
686 # in other cases. Finally, the submitted output does not have
686 # a trailing newline, so we must add it manually.
687 # a trailing newline, so we must add it manually.
687 out_data.append("{0} {1}\n".format(output_prompt, data))
688 out_data.append("{0} {1}\n".format(output_prompt, data))
688
689
689 return out_data
690 return out_data
690
691
691 def process_comment(self, data):
692 def process_comment(self, data):
692 """Process data fPblock for COMMENT token."""
693 """Process data fPblock for COMMENT token."""
693 if not self.is_suppress:
694 if not self.is_suppress:
694 return [data]
695 return [data]
695
696
696 def save_image(self, image_file):
697 def save_image(self, image_file):
697 """
698 """
698 Saves the image file to disk.
699 Saves the image file to disk.
699 """
700 """
700 self.ensure_pyplot()
701 self.ensure_pyplot()
701 command = 'plt.gcf().savefig("%s")'%image_file
702 command = 'plt.gcf().savefig("%s")'%image_file
702 # print('SAVEFIG', command) # dbg
703 # print('SAVEFIG', command) # dbg
703 self.process_input_line('bookmark ipy_thisdir', store_history=False)
704 self.process_input_line('bookmark ipy_thisdir', store_history=False)
704 self.process_input_line('cd -b ipy_savedir', store_history=False)
705 self.process_input_line('cd -b ipy_savedir', store_history=False)
705 self.process_input_line(command, store_history=False)
706 self.process_input_line(command, store_history=False)
706 self.process_input_line('cd -b ipy_thisdir', store_history=False)
707 self.process_input_line('cd -b ipy_thisdir', store_history=False)
707 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
708 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
708 self.clear_cout()
709 self.clear_cout()
709
710
710 def process_block(self, block):
711 def process_block(self, block):
711 """
712 """
712 process block from the block_parser and return a list of processed lines
713 process block from the block_parser and return a list of processed lines
713 """
714 """
714 ret = []
715 ret = []
715 output = None
716 output = None
716 input_lines = None
717 input_lines = None
717 lineno = self.IP.execution_count
718 lineno = self.IP.execution_count
718
719
719 input_prompt = self.promptin % lineno
720 input_prompt = self.promptin % lineno
720 output_prompt = self.promptout % lineno
721 output_prompt = self.promptout % lineno
721 image_file = None
722 image_file = None
722 image_directive = None
723 image_directive = None
723
724
724 found_input = False
725 found_input = False
725 for token, data in block:
726 for token, data in block:
726 if token == COMMENT:
727 if token == COMMENT:
727 out_data = self.process_comment(data)
728 out_data = self.process_comment(data)
728 elif token == INPUT:
729 elif token == INPUT:
729 found_input = True
730 found_input = True
730 (out_data, input_lines, output, is_doctest,
731 (out_data, input_lines, output, is_doctest,
731 decorator, image_file, image_directive) = \
732 decorator, image_file, image_directive) = \
732 self.process_input(data, input_prompt, lineno)
733 self.process_input(data, input_prompt, lineno)
733 elif token == OUTPUT:
734 elif token == OUTPUT:
734 if not found_input:
735 if not found_input:
735
736
736 TAB = ' ' * 4
737 TAB = ' ' * 4
737 linenumber = 0
738 linenumber = 0
738 source = 'Unavailable'
739 source = 'Unavailable'
739 content = 'Unavailable'
740 content = 'Unavailable'
740 if self.directive:
741 if self.directive:
741 linenumber = self.directive.state.document.current_line
742 linenumber = self.directive.state.document.current_line
742 source = self.directive.state.document.current_source
743 source = self.directive.state.document.current_source
743 content = self.directive.content
744 content = self.directive.content
744 # Add tabs and join into a single string.
745 # Add tabs and join into a single string.
745 content = '\n'.join([TAB + line for line in content])
746 content = '\n'.join([TAB + line for line in content])
746
747
747 e = ('\n\nInvalid block: Block contains an output prompt '
748 e = ('\n\nInvalid block: Block contains an output prompt '
748 'without an input prompt.\n\n'
749 'without an input prompt.\n\n'
749 'Document source: {0}\n\n'
750 'Document source: {0}\n\n'
750 'Content begins at line {1}: \n\n{2}\n\n'
751 'Content begins at line {1}: \n\n{2}\n\n'
751 'Problematic block within content: \n\n{TAB}{3}\n\n')
752 'Problematic block within content: \n\n{TAB}{3}\n\n')
752 e = e.format(source, linenumber, content, block, TAB=TAB)
753 e = e.format(source, linenumber, content, block, TAB=TAB)
753
754
754 # Write, rather than include in exception, since Sphinx
755 # Write, rather than include in exception, since Sphinx
755 # will truncate tracebacks.
756 # will truncate tracebacks.
756 sys.stdout.write(e)
757 sys.stdout.write(e)
757 raise RuntimeError('An invalid block was detected.')
758 raise RuntimeError('An invalid block was detected.')
758 out_data = \
759 out_data = \
759 self.process_output(data, output_prompt, input_lines,
760 self.process_output(data, output_prompt, input_lines,
760 output, is_doctest, decorator,
761 output, is_doctest, decorator,
761 image_file)
762 image_file)
762 if out_data:
763 if out_data:
763 # Then there was user submitted output in verbatim mode.
764 # Then there was user submitted output in verbatim mode.
764 # We need to remove the last element of `ret` that was
765 # We need to remove the last element of `ret` that was
765 # added in `process_input`, as it is '' and would introduce
766 # added in `process_input`, as it is '' and would introduce
766 # an undesirable newline.
767 # an undesirable newline.
767 assert(ret[-1] == '')
768 assert(ret[-1] == '')
768 del ret[-1]
769 del ret[-1]
769
770
770 if out_data:
771 if out_data:
771 ret.extend(out_data)
772 ret.extend(out_data)
772
773
773 # save the image files
774 # save the image files
774 if image_file is not None:
775 if image_file is not None:
775 self.save_image(image_file)
776 self.save_image(image_file)
776
777
777 return ret, image_directive
778 return ret, image_directive
778
779
779 def ensure_pyplot(self):
780 def ensure_pyplot(self):
780 """
781 """
781 Ensures that pyplot has been imported into the embedded IPython shell.
782 Ensures that pyplot has been imported into the embedded IPython shell.
782
783
783 Also, makes sure to set the backend appropriately if not set already.
784 Also, makes sure to set the backend appropriately if not set already.
784
785
785 """
786 """
786 # We are here if the @figure pseudo decorator was used. Thus, it's
787 # We are here if the @figure pseudo decorator was used. Thus, it's
787 # possible that we could be here even if python_mplbackend were set to
788 # possible that we could be here even if python_mplbackend were set to
788 # `None`. That's also strange and perhaps worthy of raising an
789 # `None`. That's also strange and perhaps worthy of raising an
789 # exception, but for now, we just set the backend to 'agg'.
790 # exception, but for now, we just set the backend to 'agg'.
790
791
791 if not self._pyplot_imported:
792 if not self._pyplot_imported:
792 if 'matplotlib.backends' not in sys.modules:
793 if 'matplotlib.backends' not in sys.modules:
793 # Then ipython_matplotlib was set to None but there was a
794 # Then ipython_matplotlib was set to None but there was a
794 # call to the @figure decorator (and ipython_execlines did
795 # call to the @figure decorator (and ipython_execlines did
795 # not set a backend).
796 # not set a backend).
796 #raise Exception("No backend was set, but @figure was used!")
797 #raise Exception("No backend was set, but @figure was used!")
797 import matplotlib
798 import matplotlib
798 matplotlib.use('agg')
799 matplotlib.use('agg')
799
800
800 # Always import pyplot into embedded shell.
801 # Always import pyplot into embedded shell.
801 self.process_input_line('import matplotlib.pyplot as plt',
802 self.process_input_line('import matplotlib.pyplot as plt',
802 store_history=False)
803 store_history=False)
803 self._pyplot_imported = True
804 self._pyplot_imported = True
804
805
805 def process_pure_python(self, content):
806 def process_pure_python(self, content):
806 """
807 """
807 content is a list of strings. it is unedited directive content
808 content is a list of strings. it is unedited directive content
808
809
809 This runs it line by line in the InteractiveShell, prepends
810 This runs it line by line in the InteractiveShell, prepends
810 prompts as needed capturing stderr and stdout, then returns
811 prompts as needed capturing stderr and stdout, then returns
811 the content as a list as if it were ipython code
812 the content as a list as if it were ipython code
812 """
813 """
813 output = []
814 output = []
814 savefig = False # keep up with this to clear figure
815 savefig = False # keep up with this to clear figure
815 multiline = False # to handle line continuation
816 multiline = False # to handle line continuation
816 multiline_start = None
817 multiline_start = None
817 fmtin = self.promptin
818 fmtin = self.promptin
818
819
819 ct = 0
820 ct = 0
820
821
821 for lineno, line in enumerate(content):
822 for lineno, line in enumerate(content):
822
823
823 line_stripped = line.strip()
824 line_stripped = line.strip()
824 if not len(line):
825 if not len(line):
825 output.append(line)
826 output.append(line)
826 continue
827 continue
827
828
828 # handle pseudo-decorators, whilst ensuring real python decorators are treated as input
829 # handle pseudo-decorators, whilst ensuring real python decorators are treated as input
829 if any(
830 if any(
830 line_stripped.startswith("@" + pseudo_decorator)
831 line_stripped.startswith("@" + pseudo_decorator)
831 for pseudo_decorator in PSEUDO_DECORATORS
832 for pseudo_decorator in PSEUDO_DECORATORS
832 ):
833 ):
833 output.extend([line])
834 output.extend([line])
834 if 'savefig' in line:
835 if 'savefig' in line:
835 savefig = True # and need to clear figure
836 savefig = True # and need to clear figure
836 continue
837 continue
837
838
838 # handle comments
839 # handle comments
839 if line_stripped.startswith('#'):
840 if line_stripped.startswith('#'):
840 output.extend([line])
841 output.extend([line])
841 continue
842 continue
842
843
843 # deal with lines checking for multiline
844 # deal with lines checking for multiline
844 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
845 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
845 if not multiline:
846 if not multiline:
846 modified = u"%s %s" % (fmtin % ct, line_stripped)
847 modified = u"%s %s" % (fmtin % ct, line_stripped)
847 output.append(modified)
848 output.append(modified)
848 ct += 1
849 ct += 1
849 try:
850 try:
850 ast.parse(line_stripped)
851 ast.parse(line_stripped)
851 output.append(u'')
852 output.append(u'')
852 except Exception: # on a multiline
853 except Exception: # on a multiline
853 multiline = True
854 multiline = True
854 multiline_start = lineno
855 multiline_start = lineno
855 else: # still on a multiline
856 else: # still on a multiline
856 modified = u'%s %s' % (continuation, line)
857 modified = u'%s %s' % (continuation, line)
857 output.append(modified)
858 output.append(modified)
858
859
859 # if the next line is indented, it should be part of multiline
860 # if the next line is indented, it should be part of multiline
860 if len(content) > lineno + 1:
861 if len(content) > lineno + 1:
861 nextline = content[lineno + 1]
862 nextline = content[lineno + 1]
862 if len(nextline) - len(nextline.lstrip()) > 3:
863 if len(nextline) - len(nextline.lstrip()) > 3:
863 continue
864 continue
864 try:
865 try:
865 mod = ast.parse(
866 mod = ast.parse(
866 '\n'.join(content[multiline_start:lineno+1]))
867 '\n'.join(content[multiline_start:lineno+1]))
867 if isinstance(mod.body[0], ast.FunctionDef):
868 if isinstance(mod.body[0], ast.FunctionDef):
868 # check to see if we have the whole function
869 # check to see if we have the whole function
869 for element in mod.body[0].body:
870 for element in mod.body[0].body:
870 if isinstance(element, ast.Return):
871 if isinstance(element, ast.Return):
871 multiline = False
872 multiline = False
872 else:
873 else:
873 output.append(u'')
874 output.append(u'')
874 multiline = False
875 multiline = False
875 except Exception:
876 except Exception:
876 pass
877 pass
877
878
878 if savefig: # clear figure if plotted
879 if savefig: # clear figure if plotted
879 self.ensure_pyplot()
880 self.ensure_pyplot()
880 self.process_input_line('plt.clf()', store_history=False)
881 self.process_input_line('plt.clf()', store_history=False)
881 self.clear_cout()
882 self.clear_cout()
882 savefig = False
883 savefig = False
883
884
884 return output
885 return output
885
886
886 def custom_doctest(self, decorator, input_lines, found, submitted):
887 def custom_doctest(self, decorator, input_lines, found, submitted):
887 """
888 """
888 Perform a specialized doctest.
889 Perform a specialized doctest.
889
890
890 """
891 """
891 from .custom_doctests import doctests
892 from .custom_doctests import doctests
892
893
893 args = decorator.split()
894 args = decorator.split()
894 doctest_type = args[1]
895 doctest_type = args[1]
895 if doctest_type in doctests:
896 if doctest_type in doctests:
896 doctests[doctest_type](self, args, input_lines, found, submitted)
897 doctests[doctest_type](self, args, input_lines, found, submitted)
897 else:
898 else:
898 e = "Invalid option to @doctest: {0}".format(doctest_type)
899 e = "Invalid option to @doctest: {0}".format(doctest_type)
899 raise Exception(e)
900 raise Exception(e)
900
901
901
902
902 class IPythonDirective(Directive):
903 class IPythonDirective(Directive):
903
904
904 has_content = True
905 has_content: bool = True
905 required_arguments = 0
906 required_arguments: int = 0
906 optional_arguments = 4 # python, suppress, verbatim, doctest
907 optional_arguments: int = 4 # python, suppress, verbatim, doctest
907 final_argumuent_whitespace = True
908 final_argumuent_whitespace: bool = True
908 option_spec = { 'python': directives.unchanged,
909 option_spec: Dict[str, Any] = {
909 'suppress' : directives.flag,
910 "python": directives.unchanged,
910 'verbatim' : directives.flag,
911 "suppress": directives.flag,
911 'doctest' : directives.flag,
912 "verbatim": directives.flag,
912 'okexcept': directives.flag,
913 "doctest": directives.flag,
913 'okwarning': directives.flag
914 "okexcept": directives.flag,
915 "okwarning": directives.flag,
914 }
916 }
915
917
916 shell = None
918 shell = None
917
919
918 seen_docs = set()
920 seen_docs: Set = set()
919
921
920 def get_config_options(self):
922 def get_config_options(self):
921 # contains sphinx configuration variables
923 # contains sphinx configuration variables
922 config = self.state.document.settings.env.config
924 config = self.state.document.settings.env.config
923
925
924 # get config variables to set figure output directory
926 # get config variables to set figure output directory
925 savefig_dir = config.ipython_savefig_dir
927 savefig_dir = config.ipython_savefig_dir
926 source_dir = self.state.document.settings.env.srcdir
928 source_dir = self.state.document.settings.env.srcdir
927 savefig_dir = os.path.join(source_dir, savefig_dir)
929 savefig_dir = os.path.join(source_dir, savefig_dir)
928
930
929 # get regex and prompt stuff
931 # get regex and prompt stuff
930 rgxin = config.ipython_rgxin
932 rgxin = config.ipython_rgxin
931 rgxout = config.ipython_rgxout
933 rgxout = config.ipython_rgxout
932 warning_is_error= config.ipython_warning_is_error
934 warning_is_error= config.ipython_warning_is_error
933 promptin = config.ipython_promptin
935 promptin = config.ipython_promptin
934 promptout = config.ipython_promptout
936 promptout = config.ipython_promptout
935 mplbackend = config.ipython_mplbackend
937 mplbackend = config.ipython_mplbackend
936 exec_lines = config.ipython_execlines
938 exec_lines = config.ipython_execlines
937 hold_count = config.ipython_holdcount
939 hold_count = config.ipython_holdcount
938
940
939 return (savefig_dir, source_dir, rgxin, rgxout,
941 return (savefig_dir, source_dir, rgxin, rgxout,
940 promptin, promptout, mplbackend, exec_lines, hold_count, warning_is_error)
942 promptin, promptout, mplbackend, exec_lines, hold_count, warning_is_error)
941
943
942 def setup(self):
944 def setup(self):
943 # Get configuration values.
945 # Get configuration values.
944 (savefig_dir, source_dir, rgxin, rgxout, promptin, promptout,
946 (savefig_dir, source_dir, rgxin, rgxout, promptin, promptout,
945 mplbackend, exec_lines, hold_count, warning_is_error) = self.get_config_options()
947 mplbackend, exec_lines, hold_count, warning_is_error) = self.get_config_options()
946
948
947 try:
949 try:
948 os.makedirs(savefig_dir)
950 os.makedirs(savefig_dir)
949 except OSError as e:
951 except OSError as e:
950 if e.errno != errno.EEXIST:
952 if e.errno != errno.EEXIST:
951 raise
953 raise
952
954
953 if self.shell is None:
955 if self.shell is None:
954 # We will be here many times. However, when the
956 # We will be here many times. However, when the
955 # EmbeddedSphinxShell is created, its interactive shell member
957 # EmbeddedSphinxShell is created, its interactive shell member
956 # is the same for each instance.
958 # is the same for each instance.
957
959
958 if mplbackend and 'matplotlib.backends' not in sys.modules and use_matplotlib:
960 if mplbackend and 'matplotlib.backends' not in sys.modules and use_matplotlib:
959 import matplotlib
961 import matplotlib
960 matplotlib.use(mplbackend)
962 matplotlib.use(mplbackend)
961
963
962 # Must be called after (potentially) importing matplotlib and
964 # Must be called after (potentially) importing matplotlib and
963 # setting its backend since exec_lines might import pylab.
965 # setting its backend since exec_lines might import pylab.
964 self.shell = EmbeddedSphinxShell(exec_lines)
966 self.shell = EmbeddedSphinxShell(exec_lines)
965
967
966 # Store IPython directive to enable better error messages
968 # Store IPython directive to enable better error messages
967 self.shell.directive = self
969 self.shell.directive = self
968
970
969 # reset the execution count if we haven't processed this doc
971 # reset the execution count if we haven't processed this doc
970 #NOTE: this may be borked if there are multiple seen_doc tmp files
972 #NOTE: this may be borked if there are multiple seen_doc tmp files
971 #check time stamp?
973 #check time stamp?
972 if not self.state.document.current_source in self.seen_docs:
974 if not self.state.document.current_source in self.seen_docs:
973 self.shell.IP.history_manager.reset()
975 self.shell.IP.history_manager.reset()
974 self.shell.IP.execution_count = 1
976 self.shell.IP.execution_count = 1
975 self.seen_docs.add(self.state.document.current_source)
977 self.seen_docs.add(self.state.document.current_source)
976
978
977 # and attach to shell so we don't have to pass them around
979 # and attach to shell so we don't have to pass them around
978 self.shell.rgxin = rgxin
980 self.shell.rgxin = rgxin
979 self.shell.rgxout = rgxout
981 self.shell.rgxout = rgxout
980 self.shell.promptin = promptin
982 self.shell.promptin = promptin
981 self.shell.promptout = promptout
983 self.shell.promptout = promptout
982 self.shell.savefig_dir = savefig_dir
984 self.shell.savefig_dir = savefig_dir
983 self.shell.source_dir = source_dir
985 self.shell.source_dir = source_dir
984 self.shell.hold_count = hold_count
986 self.shell.hold_count = hold_count
985 self.shell.warning_is_error = warning_is_error
987 self.shell.warning_is_error = warning_is_error
986
988
987 # setup bookmark for saving figures directory
989 # setup bookmark for saving figures directory
988 self.shell.process_input_line(
990 self.shell.process_input_line(
989 'bookmark ipy_savedir "%s"' % savefig_dir, store_history=False
991 'bookmark ipy_savedir "%s"' % savefig_dir, store_history=False
990 )
992 )
991 self.shell.clear_cout()
993 self.shell.clear_cout()
992
994
993 return rgxin, rgxout, promptin, promptout
995 return rgxin, rgxout, promptin, promptout
994
996
995 def teardown(self):
997 def teardown(self):
996 # delete last bookmark
998 # delete last bookmark
997 self.shell.process_input_line('bookmark -d ipy_savedir',
999 self.shell.process_input_line('bookmark -d ipy_savedir',
998 store_history=False)
1000 store_history=False)
999 self.shell.clear_cout()
1001 self.shell.clear_cout()
1000
1002
1001 def run(self):
1003 def run(self):
1002 debug = False
1004 debug = False
1003
1005
1004 #TODO, any reason block_parser can't be a method of embeddable shell
1006 #TODO, any reason block_parser can't be a method of embeddable shell
1005 # then we wouldn't have to carry these around
1007 # then we wouldn't have to carry these around
1006 rgxin, rgxout, promptin, promptout = self.setup()
1008 rgxin, rgxout, promptin, promptout = self.setup()
1007
1009
1008 options = self.options
1010 options = self.options
1009 self.shell.is_suppress = 'suppress' in options
1011 self.shell.is_suppress = 'suppress' in options
1010 self.shell.is_doctest = 'doctest' in options
1012 self.shell.is_doctest = 'doctest' in options
1011 self.shell.is_verbatim = 'verbatim' in options
1013 self.shell.is_verbatim = 'verbatim' in options
1012 self.shell.is_okexcept = 'okexcept' in options
1014 self.shell.is_okexcept = 'okexcept' in options
1013 self.shell.is_okwarning = 'okwarning' in options
1015 self.shell.is_okwarning = 'okwarning' in options
1014
1016
1015 # handle pure python code
1017 # handle pure python code
1016 if 'python' in self.arguments:
1018 if 'python' in self.arguments:
1017 content = self.content
1019 content = self.content
1018 self.content = self.shell.process_pure_python(content)
1020 self.content = self.shell.process_pure_python(content)
1019
1021
1020 # parts consists of all text within the ipython-block.
1022 # parts consists of all text within the ipython-block.
1021 # Each part is an input/output block.
1023 # Each part is an input/output block.
1022 parts = '\n'.join(self.content).split('\n\n')
1024 parts = '\n'.join(self.content).split('\n\n')
1023
1025
1024 lines = ['.. code-block:: ipython', '']
1026 lines = ['.. code-block:: ipython', '']
1025 figures = []
1027 figures = []
1026
1028
1027 # Use sphinx logger for warnings
1029 # Use sphinx logger for warnings
1028 logger = logging.getLogger(__name__)
1030 logger = logging.getLogger(__name__)
1029
1031
1030 for part in parts:
1032 for part in parts:
1031 block = block_parser(part, rgxin, rgxout, promptin, promptout)
1033 block = block_parser(part, rgxin, rgxout, promptin, promptout)
1032 if len(block):
1034 if len(block):
1033 rows, figure = self.shell.process_block(block)
1035 rows, figure = self.shell.process_block(block)
1034 for row in rows:
1036 for row in rows:
1035 lines.extend([' {0}'.format(line)
1037 lines.extend([' {0}'.format(line)
1036 for line in row.split('\n')])
1038 for line in row.split('\n')])
1037
1039
1038 if figure is not None:
1040 if figure is not None:
1039 figures.append(figure)
1041 figures.append(figure)
1040 else:
1042 else:
1041 message = 'Code input with no code at {}, line {}'\
1043 message = 'Code input with no code at {}, line {}'\
1042 .format(
1044 .format(
1043 self.state.document.current_source,
1045 self.state.document.current_source,
1044 self.state.document.current_line)
1046 self.state.document.current_line)
1045 if self.shell.warning_is_error:
1047 if self.shell.warning_is_error:
1046 raise RuntimeError(message)
1048 raise RuntimeError(message)
1047 else:
1049 else:
1048 logger.warning(message)
1050 logger.warning(message)
1049
1051
1050 for figure in figures:
1052 for figure in figures:
1051 lines.append('')
1053 lines.append('')
1052 lines.extend(figure.split('\n'))
1054 lines.extend(figure.split('\n'))
1053 lines.append('')
1055 lines.append('')
1054
1056
1055 if len(lines) > 2:
1057 if len(lines) > 2:
1056 if debug:
1058 if debug:
1057 print('\n'.join(lines))
1059 print('\n'.join(lines))
1058 else:
1060 else:
1059 # This has to do with input, not output. But if we comment
1061 # This has to do with input, not output. But if we comment
1060 # these lines out, then no IPython code will appear in the
1062 # these lines out, then no IPython code will appear in the
1061 # final output.
1063 # final output.
1062 self.state_machine.insert_input(
1064 self.state_machine.insert_input(
1063 lines, self.state_machine.input_lines.source(0))
1065 lines, self.state_machine.input_lines.source(0))
1064
1066
1065 # cleanup
1067 # cleanup
1066 self.teardown()
1068 self.teardown()
1067
1069
1068 return []
1070 return []
1069
1071
1070 # Enable as a proper Sphinx directive
1072 # Enable as a proper Sphinx directive
1071 def setup(app):
1073 def setup(app):
1072 setup.app = app
1074 setup.app = app
1073
1075
1074 app.add_directive('ipython', IPythonDirective)
1076 app.add_directive('ipython', IPythonDirective)
1075 app.add_config_value('ipython_savefig_dir', 'savefig', 'env')
1077 app.add_config_value('ipython_savefig_dir', 'savefig', 'env')
1076 app.add_config_value('ipython_warning_is_error', True, 'env')
1078 app.add_config_value('ipython_warning_is_error', True, 'env')
1077 app.add_config_value('ipython_rgxin',
1079 app.add_config_value('ipython_rgxin',
1078 re.compile(r'In \[(\d+)\]:\s?(.*)\s*'), 'env')
1080 re.compile(r'In \[(\d+)\]:\s?(.*)\s*'), 'env')
1079 app.add_config_value('ipython_rgxout',
1081 app.add_config_value('ipython_rgxout',
1080 re.compile(r'Out\[(\d+)\]:\s?(.*)\s*'), 'env')
1082 re.compile(r'Out\[(\d+)\]:\s?(.*)\s*'), 'env')
1081 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
1083 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
1082 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
1084 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
1083
1085
1084 # We could just let matplotlib pick whatever is specified as the default
1086 # We could just let matplotlib pick whatever is specified as the default
1085 # backend in the matplotlibrc file, but this would cause issues if the
1087 # backend in the matplotlibrc file, but this would cause issues if the
1086 # backend didn't work in headless environments. For this reason, 'agg'
1088 # backend didn't work in headless environments. For this reason, 'agg'
1087 # is a good default backend choice.
1089 # is a good default backend choice.
1088 app.add_config_value('ipython_mplbackend', 'agg', 'env')
1090 app.add_config_value('ipython_mplbackend', 'agg', 'env')
1089
1091
1090 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
1092 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
1091 # __init__ method will treat it as [].
1093 # __init__ method will treat it as [].
1092 execlines = ['import numpy as np']
1094 execlines = ['import numpy as np']
1093 if use_matplotlib:
1095 if use_matplotlib:
1094 execlines.append('import matplotlib.pyplot as plt')
1096 execlines.append('import matplotlib.pyplot as plt')
1095 app.add_config_value('ipython_execlines', execlines, 'env')
1097 app.add_config_value('ipython_execlines', execlines, 'env')
1096
1098
1097 app.add_config_value('ipython_holdcount', True, 'env')
1099 app.add_config_value('ipython_holdcount', True, 'env')
1098
1100
1099 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
1101 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
1100 return metadata
1102 return metadata
1101
1103
1102 # Simple smoke test, needs to be converted to a proper automatic test.
1104 # Simple smoke test, needs to be converted to a proper automatic test.
1103 def test():
1105 def test():
1104
1106
1105 examples = [
1107 examples = [
1106 r"""
1108 r"""
1107 In [9]: pwd
1109 In [9]: pwd
1108 Out[9]: '/home/jdhunter/py4science/book'
1110 Out[9]: '/home/jdhunter/py4science/book'
1109
1111
1110 In [10]: cd bookdata/
1112 In [10]: cd bookdata/
1111 /home/jdhunter/py4science/book/bookdata
1113 /home/jdhunter/py4science/book/bookdata
1112
1114
1113 In [2]: from pylab import *
1115 In [2]: from pylab import *
1114
1116
1115 In [2]: ion()
1117 In [2]: ion()
1116
1118
1117 In [3]: im = imread('stinkbug.png')
1119 In [3]: im = imread('stinkbug.png')
1118
1120
1119 @savefig mystinkbug.png width=4in
1121 @savefig mystinkbug.png width=4in
1120 In [4]: imshow(im)
1122 In [4]: imshow(im)
1121 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
1123 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
1122
1124
1123 """,
1125 """,
1124 r"""
1126 r"""
1125
1127
1126 In [1]: x = 'hello world'
1128 In [1]: x = 'hello world'
1127
1129
1128 # string methods can be
1130 # string methods can be
1129 # used to alter the string
1131 # used to alter the string
1130 @doctest
1132 @doctest
1131 In [2]: x.upper()
1133 In [2]: x.upper()
1132 Out[2]: 'HELLO WORLD'
1134 Out[2]: 'HELLO WORLD'
1133
1135
1134 @verbatim
1136 @verbatim
1135 In [3]: x.st<TAB>
1137 In [3]: x.st<TAB>
1136 x.startswith x.strip
1138 x.startswith x.strip
1137 """,
1139 """,
1138 r"""
1140 r"""
1139
1141
1140 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
1142 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
1141 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
1143 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
1142
1144
1143 In [131]: print url.split('&')
1145 In [131]: print url.split('&')
1144 ['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv']
1146 ['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv']
1145
1147
1146 In [60]: import urllib
1148 In [60]: import urllib
1147
1149
1148 """,
1150 """,
1149 r"""\
1151 r"""\
1150
1152
1151 In [133]: import numpy.random
1153 In [133]: import numpy.random
1152
1154
1153 @suppress
1155 @suppress
1154 In [134]: numpy.random.seed(2358)
1156 In [134]: numpy.random.seed(2358)
1155
1157
1156 @doctest
1158 @doctest
1157 In [135]: numpy.random.rand(10,2)
1159 In [135]: numpy.random.rand(10,2)
1158 Out[135]:
1160 Out[135]:
1159 array([[ 0.64524308, 0.59943846],
1161 array([[ 0.64524308, 0.59943846],
1160 [ 0.47102322, 0.8715456 ],
1162 [ 0.47102322, 0.8715456 ],
1161 [ 0.29370834, 0.74776844],
1163 [ 0.29370834, 0.74776844],
1162 [ 0.99539577, 0.1313423 ],
1164 [ 0.99539577, 0.1313423 ],
1163 [ 0.16250302, 0.21103583],
1165 [ 0.16250302, 0.21103583],
1164 [ 0.81626524, 0.1312433 ],
1166 [ 0.81626524, 0.1312433 ],
1165 [ 0.67338089, 0.72302393],
1167 [ 0.67338089, 0.72302393],
1166 [ 0.7566368 , 0.07033696],
1168 [ 0.7566368 , 0.07033696],
1167 [ 0.22591016, 0.77731835],
1169 [ 0.22591016, 0.77731835],
1168 [ 0.0072729 , 0.34273127]])
1170 [ 0.0072729 , 0.34273127]])
1169
1171
1170 """,
1172 """,
1171
1173
1172 r"""
1174 r"""
1173 In [106]: print x
1175 In [106]: print x
1174 jdh
1176 jdh
1175
1177
1176 In [109]: for i in range(10):
1178 In [109]: for i in range(10):
1177 .....: print i
1179 .....: print i
1178 .....:
1180 .....:
1179 .....:
1181 .....:
1180 0
1182 0
1181 1
1183 1
1182 2
1184 2
1183 3
1185 3
1184 4
1186 4
1185 5
1187 5
1186 6
1188 6
1187 7
1189 7
1188 8
1190 8
1189 9
1191 9
1190 """,
1192 """,
1191
1193
1192 r"""
1194 r"""
1193
1195
1194 In [144]: from pylab import *
1196 In [144]: from pylab import *
1195
1197
1196 In [145]: ion()
1198 In [145]: ion()
1197
1199
1198 # use a semicolon to suppress the output
1200 # use a semicolon to suppress the output
1199 @savefig test_hist.png width=4in
1201 @savefig test_hist.png width=4in
1200 In [151]: hist(np.random.randn(10000), 100);
1202 In [151]: hist(np.random.randn(10000), 100);
1201
1203
1202
1204
1203 @savefig test_plot.png width=4in
1205 @savefig test_plot.png width=4in
1204 In [151]: plot(np.random.randn(10000), 'o');
1206 In [151]: plot(np.random.randn(10000), 'o');
1205 """,
1207 """,
1206
1208
1207 r"""
1209 r"""
1208 # use a semicolon to suppress the output
1210 # use a semicolon to suppress the output
1209 In [151]: plt.clf()
1211 In [151]: plt.clf()
1210
1212
1211 @savefig plot_simple.png width=4in
1213 @savefig plot_simple.png width=4in
1212 In [151]: plot([1,2,3])
1214 In [151]: plot([1,2,3])
1213
1215
1214 @savefig hist_simple.png width=4in
1216 @savefig hist_simple.png width=4in
1215 In [151]: hist(np.random.randn(10000), 100);
1217 In [151]: hist(np.random.randn(10000), 100);
1216
1218
1217 """,
1219 """,
1218 r"""
1220 r"""
1219 # update the current fig
1221 # update the current fig
1220 In [151]: ylabel('number')
1222 In [151]: ylabel('number')
1221
1223
1222 In [152]: title('normal distribution')
1224 In [152]: title('normal distribution')
1223
1225
1224
1226
1225 @savefig hist_with_text.png
1227 @savefig hist_with_text.png
1226 In [153]: grid(True)
1228 In [153]: grid(True)
1227
1229
1228 @doctest float
1230 @doctest float
1229 In [154]: 0.1 + 0.2
1231 In [154]: 0.1 + 0.2
1230 Out[154]: 0.3
1232 Out[154]: 0.3
1231
1233
1232 @doctest float
1234 @doctest float
1233 In [155]: np.arange(16).reshape(4,4)
1235 In [155]: np.arange(16).reshape(4,4)
1234 Out[155]:
1236 Out[155]:
1235 array([[ 0, 1, 2, 3],
1237 array([[ 0, 1, 2, 3],
1236 [ 4, 5, 6, 7],
1238 [ 4, 5, 6, 7],
1237 [ 8, 9, 10, 11],
1239 [ 8, 9, 10, 11],
1238 [12, 13, 14, 15]])
1240 [12, 13, 14, 15]])
1239
1241
1240 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
1242 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
1241
1243
1242 In [2]: x[0,0] = np.inf
1244 In [2]: x[0,0] = np.inf
1243
1245
1244 In [3]: x[0,1] = np.nan
1246 In [3]: x[0,1] = np.nan
1245
1247
1246 @doctest float
1248 @doctest float
1247 In [4]: x
1249 In [4]: x
1248 Out[4]:
1250 Out[4]:
1249 array([[ inf, nan, 2., 3.],
1251 array([[ inf, nan, 2., 3.],
1250 [ 4., 5., 6., 7.],
1252 [ 4., 5., 6., 7.],
1251 [ 8., 9., 10., 11.],
1253 [ 8., 9., 10., 11.],
1252 [ 12., 13., 14., 15.]])
1254 [ 12., 13., 14., 15.]])
1253
1255
1254
1256
1255 """,
1257 """,
1256 ]
1258 ]
1257 # skip local-file depending first example:
1259 # skip local-file depending first example:
1258 examples = examples[1:]
1260 examples = examples[1:]
1259
1261
1260 #ipython_directive.DEBUG = True # dbg
1262 #ipython_directive.DEBUG = True # dbg
1261 #options = dict(suppress=True) # dbg
1263 #options = dict(suppress=True) # dbg
1262 options = {}
1264 options = {}
1263 for example in examples:
1265 for example in examples:
1264 content = example.split('\n')
1266 content = example.split('\n')
1265 IPythonDirective('debug', arguments=None, options=options,
1267 IPythonDirective('debug', arguments=None, options=options,
1266 content=content, lineno=0,
1268 content=content, lineno=0,
1267 content_offset=None, block_text=None,
1269 content_offset=None, block_text=None,
1268 state=None, state_machine=None,
1270 state=None, state_machine=None,
1269 )
1271 )
1270
1272
1271 # Run test suite as a script
1273 # Run test suite as a script
1272 if __name__=='__main__':
1274 if __name__=='__main__':
1273 if not os.path.isdir('_static'):
1275 if not os.path.isdir('_static'):
1274 os.mkdir('_static')
1276 os.mkdir('_static')
1275 test()
1277 test()
1276 print('All OK? Check figures in _static/')
1278 print('All OK? Check figures in _static/')
@@ -1,104 +1,105
1 """
1 """
2 Utilities function for keybinding with prompt toolkit.
2 Utilities function for keybinding with prompt toolkit.
3
3
4 This will be bound to specific key press and filter modes,
4 This will be bound to specific key press and filter modes,
5 like whether we are in edit mode, and whether the completer is open.
5 like whether we are in edit mode, and whether the completer is open.
6 """
6 """
7
7 import re
8 import re
8 from prompt_toolkit.key_binding import KeyPressEvent
9 from prompt_toolkit.key_binding import KeyPressEvent
9
10
10
11
11 def parenthesis(event: KeyPressEvent):
12 def parenthesis(event: KeyPressEvent):
12 """Auto-close parenthesis"""
13 """Auto-close parenthesis"""
13 event.current_buffer.insert_text("()")
14 event.current_buffer.insert_text("()")
14 event.current_buffer.cursor_left()
15 event.current_buffer.cursor_left()
15
16
16
17
17 def brackets(event: KeyPressEvent):
18 def brackets(event: KeyPressEvent):
18 """Auto-close brackets"""
19 """Auto-close brackets"""
19 event.current_buffer.insert_text("[]")
20 event.current_buffer.insert_text("[]")
20 event.current_buffer.cursor_left()
21 event.current_buffer.cursor_left()
21
22
22
23
23 def braces(event: KeyPressEvent):
24 def braces(event: KeyPressEvent):
24 """Auto-close braces"""
25 """Auto-close braces"""
25 event.current_buffer.insert_text("{}")
26 event.current_buffer.insert_text("{}")
26 event.current_buffer.cursor_left()
27 event.current_buffer.cursor_left()
27
28
28
29
29 def double_quote(event: KeyPressEvent):
30 def double_quote(event: KeyPressEvent):
30 """Auto-close double quotes"""
31 """Auto-close double quotes"""
31 event.current_buffer.insert_text('""')
32 event.current_buffer.insert_text('""')
32 event.current_buffer.cursor_left()
33 event.current_buffer.cursor_left()
33
34
34
35
35 def single_quote(event: KeyPressEvent):
36 def single_quote(event: KeyPressEvent):
36 """Auto-close single quotes"""
37 """Auto-close single quotes"""
37 event.current_buffer.insert_text("''")
38 event.current_buffer.insert_text("''")
38 event.current_buffer.cursor_left()
39 event.current_buffer.cursor_left()
39
40
40
41
41 def docstring_double_quotes(event: KeyPressEvent):
42 def docstring_double_quotes(event: KeyPressEvent):
42 """Auto-close docstring (double quotes)"""
43 """Auto-close docstring (double quotes)"""
43 event.current_buffer.insert_text('""""')
44 event.current_buffer.insert_text('""""')
44 event.current_buffer.cursor_left(3)
45 event.current_buffer.cursor_left(3)
45
46
46
47
47 def docstring_single_quotes(event: KeyPressEvent):
48 def docstring_single_quotes(event: KeyPressEvent):
48 """Auto-close docstring (single quotes)"""
49 """Auto-close docstring (single quotes)"""
49 event.current_buffer.insert_text("''''")
50 event.current_buffer.insert_text("''''")
50 event.current_buffer.cursor_left(3)
51 event.current_buffer.cursor_left(3)
51
52
52
53
53 def raw_string_parenthesis(event: KeyPressEvent):
54 def raw_string_parenthesis(event: KeyPressEvent):
54 """Auto-close parenthesis in raw strings"""
55 """Auto-close parenthesis in raw strings"""
55 matches = re.match(
56 matches = re.match(
56 r".*(r|R)[\"'](-*)",
57 r".*(r|R)[\"'](-*)",
57 event.current_buffer.document.current_line_before_cursor,
58 event.current_buffer.document.current_line_before_cursor,
58 )
59 )
59 dashes = matches.group(2) if matches else ""
60 dashes = matches.group(2) if matches else ""
60 event.current_buffer.insert_text("()" + dashes)
61 event.current_buffer.insert_text("()" + dashes)
61 event.current_buffer.cursor_left(len(dashes) + 1)
62 event.current_buffer.cursor_left(len(dashes) + 1)
62
63
63
64
64 def raw_string_bracket(event: KeyPressEvent):
65 def raw_string_bracket(event: KeyPressEvent):
65 """Auto-close bracker in raw strings"""
66 """Auto-close bracker in raw strings"""
66 matches = re.match(
67 matches = re.match(
67 r".*(r|R)[\"'](-*)",
68 r".*(r|R)[\"'](-*)",
68 event.current_buffer.document.current_line_before_cursor,
69 event.current_buffer.document.current_line_before_cursor,
69 )
70 )
70 dashes = matches.group(2) if matches else ""
71 dashes = matches.group(2) if matches else ""
71 event.current_buffer.insert_text("[]" + dashes)
72 event.current_buffer.insert_text("[]" + dashes)
72 event.current_buffer.cursor_left(len(dashes) + 1)
73 event.current_buffer.cursor_left(len(dashes) + 1)
73
74
74
75
75 def raw_string_braces(event: KeyPressEvent):
76 def raw_string_braces(event: KeyPressEvent):
76 """Auto-close braces in raw strings"""
77 """Auto-close braces in raw strings"""
77 matches = re.match(
78 matches = re.match(
78 r".*(r|R)[\"'](-*)",
79 r".*(r|R)[\"'](-*)",
79 event.current_buffer.document.current_line_before_cursor,
80 event.current_buffer.document.current_line_before_cursor,
80 )
81 )
81 dashes = matches.group(2) if matches else ""
82 dashes = matches.group(2) if matches else ""
82 event.current_buffer.insert_text("{}" + dashes)
83 event.current_buffer.insert_text("{}" + dashes)
83 event.current_buffer.cursor_left(len(dashes) + 1)
84 event.current_buffer.cursor_left(len(dashes) + 1)
84
85
85
86
86 def skip_over(event: KeyPressEvent):
87 def skip_over(event: KeyPressEvent):
87 """Skip over automatically added parenthesis/quote.
88 """Skip over automatically added parenthesis/quote.
88
89
89 (rather than adding another parenthesis/quote)"""
90 (rather than adding another parenthesis/quote)"""
90 event.current_buffer.cursor_right()
91 event.current_buffer.cursor_right()
91
92
92
93
93 def delete_pair(event: KeyPressEvent):
94 def delete_pair(event: KeyPressEvent):
94 """Delete auto-closed parenthesis"""
95 """Delete auto-closed parenthesis"""
95 event.current_buffer.delete()
96 event.current_buffer.delete()
96 event.current_buffer.delete_before_cursor()
97 event.current_buffer.delete_before_cursor()
97
98
98
99
99 auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
100 auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
100 auto_match_parens_raw_string = {
101 auto_match_parens_raw_string = {
101 "(": raw_string_parenthesis,
102 "(": raw_string_parenthesis,
102 "[": raw_string_bracket,
103 "[": raw_string_bracket,
103 "{": raw_string_braces,
104 "{": raw_string_braces,
104 }
105 }
General Comments 0
You need to be logged in to leave comments. Login now