Show More
@@ -1,372 +1,371 b'' | |||||
1 | """ |
|
1 | """ | |
2 | Module to define and register Terminal IPython shortcuts with |
|
2 | Module to define and register Terminal IPython shortcuts with | |
3 | :mod:`prompt_toolkit` |
|
3 | :mod:`prompt_toolkit` | |
4 | """ |
|
4 | """ | |
5 |
|
5 | |||
6 | # Copyright (c) IPython Development Team. |
|
6 | # Copyright (c) IPython Development Team. | |
7 | # Distributed under the terms of the Modified BSD License. |
|
7 | # Distributed under the terms of the Modified BSD License. | |
8 |
|
8 | |||
9 | import warnings |
|
9 | import warnings | |
10 | import signal |
|
10 | import signal | |
11 | import sys |
|
11 | import sys | |
12 | import re |
|
12 | import re | |
13 | from typing import Callable |
|
13 | from typing import Callable | |
14 |
|
14 | |||
15 |
|
15 | |||
16 | from prompt_toolkit.application.current import get_app |
|
16 | from prompt_toolkit.application.current import get_app | |
17 | from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER |
|
17 | from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER | |
18 | from prompt_toolkit.filters import (has_focus, has_selection, Condition, |
|
18 | from prompt_toolkit.filters import (has_focus, has_selection, Condition, | |
19 | vi_insert_mode, emacs_insert_mode, has_completions, vi_mode) |
|
19 | vi_insert_mode, emacs_insert_mode, has_completions, vi_mode) | |
20 | from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline |
|
20 | from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline | |
21 | from prompt_toolkit.key_binding import KeyBindings |
|
21 | from prompt_toolkit.key_binding import KeyBindings | |
22 | from prompt_toolkit.key_binding.bindings import named_commands as nc |
|
22 | from prompt_toolkit.key_binding.bindings import named_commands as nc | |
23 | from prompt_toolkit.key_binding.vi_state import InputMode, ViState |
|
23 | from prompt_toolkit.key_binding.vi_state import InputMode, ViState | |
24 |
|
24 | |||
25 | from IPython.utils.decorators import undoc |
|
25 | from IPython.utils.decorators import undoc | |
26 |
|
26 | |||
27 | @undoc |
|
27 | @undoc | |
28 | @Condition |
|
28 | @Condition | |
29 | def cursor_in_leading_ws(): |
|
29 | def cursor_in_leading_ws(): | |
30 | before = get_app().current_buffer.document.current_line_before_cursor |
|
30 | before = get_app().current_buffer.document.current_line_before_cursor | |
31 | return (not before) or before.isspace() |
|
31 | return (not before) or before.isspace() | |
32 |
|
32 | |||
33 |
|
33 | |||
34 | def create_ipython_shortcuts(shell): |
|
34 | def create_ipython_shortcuts(shell): | |
35 | """Set up the prompt_toolkit keyboard shortcuts for IPython""" |
|
35 | """Set up the prompt_toolkit keyboard shortcuts for IPython""" | |
36 |
|
36 | |||
37 | kb = KeyBindings() |
|
37 | kb = KeyBindings() | |
38 | insert_mode = vi_insert_mode | emacs_insert_mode |
|
38 | insert_mode = vi_insert_mode | emacs_insert_mode | |
39 |
|
39 | |||
40 | if getattr(shell, 'handle_return', None): |
|
40 | if getattr(shell, 'handle_return', None): | |
41 | return_handler = shell.handle_return(shell) |
|
41 | return_handler = shell.handle_return(shell) | |
42 | else: |
|
42 | else: | |
43 | return_handler = newline_or_execute_outer(shell) |
|
43 | return_handler = newline_or_execute_outer(shell) | |
44 |
|
44 | |||
45 | kb.add('enter', filter=(has_focus(DEFAULT_BUFFER) |
|
45 | kb.add('enter', filter=(has_focus(DEFAULT_BUFFER) | |
46 | & ~has_selection |
|
46 | & ~has_selection | |
47 | & insert_mode |
|
47 | & insert_mode | |
48 | ))(return_handler) |
|
48 | ))(return_handler) | |
49 |
|
49 | |||
50 | def reformat_and_execute(event): |
|
50 | def reformat_and_execute(event): | |
51 | reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell) |
|
51 | reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell) | |
52 | event.current_buffer.validate_and_handle() |
|
52 | event.current_buffer.validate_and_handle() | |
53 |
|
53 | |||
54 | kb.add('escape', 'enter', filter=(has_focus(DEFAULT_BUFFER) |
|
54 | kb.add('escape', 'enter', filter=(has_focus(DEFAULT_BUFFER) | |
55 | & ~has_selection |
|
55 | & ~has_selection | |
56 | & insert_mode |
|
56 | & insert_mode | |
57 | ))(reformat_and_execute) |
|
57 | ))(reformat_and_execute) | |
58 |
|
58 | |||
59 | kb.add('c-\\')(force_exit) |
|
59 | kb.add('c-\\')(force_exit) | |
60 |
|
60 | |||
61 | kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) |
|
61 | kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) | |
62 | )(previous_history_or_previous_completion) |
|
62 | )(previous_history_or_previous_completion) | |
63 |
|
63 | |||
64 | kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) |
|
64 | kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)) | |
65 | )(next_history_or_next_completion) |
|
65 | )(next_history_or_next_completion) | |
66 |
|
66 | |||
67 | kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions) |
|
67 | kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions) | |
68 | )(dismiss_completion) |
|
68 | )(dismiss_completion) | |
69 |
|
69 | |||
70 | kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer) |
|
70 | kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer) | |
71 |
|
71 | |||
72 | kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) |
|
72 | kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) | |
73 |
|
73 | |||
74 | supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP')) |
|
74 | supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP')) | |
75 | kb.add('c-z', filter=supports_suspend)(suspend_to_bg) |
|
75 | kb.add('c-z', filter=supports_suspend)(suspend_to_bg) | |
76 |
|
76 | |||
77 | # Ctrl+I == Tab |
|
77 | # Ctrl+I == Tab | |
78 | kb.add('tab', filter=(has_focus(DEFAULT_BUFFER) |
|
78 | kb.add('tab', filter=(has_focus(DEFAULT_BUFFER) | |
79 | & ~has_selection |
|
79 | & ~has_selection | |
80 | & insert_mode |
|
80 | & insert_mode | |
81 | & cursor_in_leading_ws |
|
81 | & cursor_in_leading_ws | |
82 | ))(indent_buffer) |
|
82 | ))(indent_buffer) | |
83 | kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode) |
|
83 | kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode) | |
84 | )(newline_autoindent_outer(shell.input_transformer_manager)) |
|
84 | )(newline_autoindent_outer(shell.input_transformer_manager)) | |
85 |
|
85 | |||
86 | kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) |
|
86 | kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) | |
87 |
|
87 | |||
88 | if shell.display_completions == 'readlinelike': |
|
88 | if shell.display_completions == 'readlinelike': | |
89 | kb.add('c-i', filter=(has_focus(DEFAULT_BUFFER) |
|
89 | kb.add('c-i', filter=(has_focus(DEFAULT_BUFFER) | |
90 | & ~has_selection |
|
90 | & ~has_selection | |
91 | & insert_mode |
|
91 | & insert_mode | |
92 | & ~cursor_in_leading_ws |
|
92 | & ~cursor_in_leading_ws | |
93 | ))(display_completions_like_readline) |
|
93 | ))(display_completions_like_readline) | |
94 |
|
94 | |||
95 | if sys.platform == "win32": |
|
95 | if sys.platform == "win32": | |
96 | kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) |
|
96 | kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) | |
97 |
|
97 | |||
98 | @Condition |
|
98 | @Condition | |
99 | def ebivim(): |
|
99 | def ebivim(): | |
100 | return shell.emacs_bindings_in_vi_insert_mode |
|
100 | return shell.emacs_bindings_in_vi_insert_mode | |
101 |
|
101 | |||
102 | focused_insert = has_focus(DEFAULT_BUFFER) & vi_insert_mode |
|
102 | focused_insert = has_focus(DEFAULT_BUFFER) & vi_insert_mode | |
103 |
|
103 | |||
104 | # Needed for to accept autosuggestions in vi insert mode |
|
104 | # Needed for to accept autosuggestions in vi insert mode | |
105 | @kb.add("c-e", filter=focused_insert & ebivim) |
|
105 | @kb.add("c-e", filter=focused_insert & ebivim) | |
106 | def _(event): |
|
106 | def _(event): | |
107 | b = event.current_buffer |
|
107 | b = event.current_buffer | |
108 | suggestion = b.suggestion |
|
108 | suggestion = b.suggestion | |
109 | if suggestion: |
|
109 | if suggestion: | |
110 | b.insert_text(suggestion.text) |
|
110 | b.insert_text(suggestion.text) | |
111 | else: |
|
111 | else: | |
112 | nc.end_of_line(event) |
|
112 | nc.end_of_line(event) | |
113 |
|
113 | |||
114 | @kb.add("c-f", filter=focused_insert & ebivim) |
|
114 | @kb.add("c-f", filter=focused_insert & ebivim) | |
115 | def _(event): |
|
115 | def _(event): | |
116 | b = event.current_buffer |
|
116 | b = event.current_buffer | |
117 | suggestion = b.suggestion |
|
117 | suggestion = b.suggestion | |
118 | if suggestion: |
|
118 | if suggestion: | |
119 | b.insert_text(suggestion.text) |
|
119 | b.insert_text(suggestion.text) | |
120 | else: |
|
120 | else: | |
121 | nc.forward_char(event) |
|
121 | nc.forward_char(event) | |
122 |
|
122 | |||
123 | @kb.add("escape", "f", filter=focused_insert & ebivim) |
|
123 | @kb.add("escape", "f", filter=focused_insert & ebivim) | |
124 | def _(event): |
|
124 | def _(event): | |
125 | b = event.current_buffer |
|
125 | b = event.current_buffer | |
126 | suggestion = b.suggestion |
|
126 | suggestion = b.suggestion | |
127 | if suggestion: |
|
127 | if suggestion: | |
128 | t = re.split(r"(\S+\s+)", suggestion.text) |
|
128 | t = re.split(r"(\S+\s+)", suggestion.text) | |
129 | b.insert_text(next((x for x in t if x), "")) |
|
129 | b.insert_text(next((x for x in t if x), "")) | |
130 | else: |
|
130 | else: | |
131 | nc.forward_word(event) |
|
131 | nc.forward_word(event) | |
132 |
|
132 | |||
133 | # Simple Control keybindings |
|
133 | # Simple Control keybindings | |
134 | key_cmd_dict = { |
|
134 | key_cmd_dict = { | |
135 | "c-a": nc.beginning_of_line, |
|
135 | "c-a": nc.beginning_of_line, | |
136 | "c-b": nc.backward_char, |
|
136 | "c-b": nc.backward_char, | |
137 | "c-k": nc.kill_line, |
|
137 | "c-k": nc.kill_line, | |
138 | "c-w": nc.backward_kill_word, |
|
138 | "c-w": nc.backward_kill_word, | |
139 | "c-y": nc.yank, |
|
139 | "c-y": nc.yank, | |
140 | "c-_": nc.undo, |
|
140 | "c-_": nc.undo, | |
141 | } |
|
141 | } | |
142 |
|
142 | |||
143 | for key, cmd in key_cmd_dict.items(): |
|
143 | for key, cmd in key_cmd_dict.items(): | |
144 | kb.add(key, filter=focused_insert & ebivim)(cmd) |
|
144 | kb.add(key, filter=focused_insert & ebivim)(cmd) | |
145 |
|
145 | |||
146 | # Alt and Combo Control keybindings |
|
146 | # Alt and Combo Control keybindings | |
147 | keys_cmd_dict = { |
|
147 | keys_cmd_dict = { | |
148 | # Control Combos |
|
148 | # Control Combos | |
149 | ("c-x", "c-e"): nc.edit_and_execute, |
|
149 | ("c-x", "c-e"): nc.edit_and_execute, | |
150 | ("c-x", "e"): nc.edit_and_execute, |
|
150 | ("c-x", "e"): nc.edit_and_execute, | |
151 | # Alt |
|
151 | # Alt | |
152 | ("escape", "b"): nc.backward_word, |
|
152 | ("escape", "b"): nc.backward_word, | |
153 | ("escape", "c"): nc.capitalize_word, |
|
153 | ("escape", "c"): nc.capitalize_word, | |
154 | ("escape", "d"): nc.kill_word, |
|
154 | ("escape", "d"): nc.kill_word, | |
155 | ("escape", "h"): nc.backward_kill_word, |
|
155 | ("escape", "h"): nc.backward_kill_word, | |
156 | ("escape", "l"): nc.downcase_word, |
|
156 | ("escape", "l"): nc.downcase_word, | |
157 | ("escape", "u"): nc.uppercase_word, |
|
157 | ("escape", "u"): nc.uppercase_word, | |
158 | ("escape", "y"): nc.yank_pop, |
|
158 | ("escape", "y"): nc.yank_pop, | |
159 | ("escape", "."): nc.yank_last_arg, |
|
159 | ("escape", "."): nc.yank_last_arg, | |
160 | } |
|
160 | } | |
161 |
|
161 | |||
162 | for keys, cmd in keys_cmd_dict.items(): |
|
162 | for keys, cmd in keys_cmd_dict.items(): | |
163 | kb.add(*keys, filter=focused_insert & ebivim)(cmd) |
|
163 | kb.add(*keys, filter=focused_insert & ebivim)(cmd) | |
164 |
|
164 | |||
165 | def get_input_mode(self): |
|
165 | def get_input_mode(self): | |
166 | if sys.version_info[0] == 3: |
|
166 | app = get_app() | |
167 | app = get_app() |
|
167 | app.ttimeoutlen = shell.ttimeoutlen | |
168 |
|
|
168 | app.timeoutlen = shell.timeoutlen | |
169 | app.timeoutlen = shell.timeoutlen |
|
|||
170 |
|
169 | |||
171 | return self._input_mode |
|
170 | return self._input_mode | |
172 |
|
171 | |||
173 | def set_input_mode(self, mode): |
|
172 | def set_input_mode(self, mode): | |
174 | shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6) |
|
173 | shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6) | |
175 | cursor = "\x1b[{} q".format(shape) |
|
174 | cursor = "\x1b[{} q".format(shape) | |
176 |
|
175 | |||
177 | if hasattr(sys.stdout, "_cli"): |
|
176 | if hasattr(sys.stdout, "_cli"): | |
178 | write = sys.stdout._cli.output.write_raw |
|
177 | write = sys.stdout._cli.output.write_raw | |
179 | else: |
|
178 | else: | |
180 | write = sys.stdout.write |
|
179 | write = sys.stdout.write | |
181 |
|
180 | |||
182 | write(cursor) |
|
181 | write(cursor) | |
183 | sys.stdout.flush() |
|
182 | sys.stdout.flush() | |
184 |
|
183 | |||
185 | self._input_mode = mode |
|
184 | self._input_mode = mode | |
186 |
|
185 | |||
187 | if shell.editing_mode == "vi" and shell.modal_cursor: |
|
186 | if shell.editing_mode == "vi" and shell.modal_cursor: | |
188 | ViState._input_mode = InputMode.INSERT |
|
187 | ViState._input_mode = InputMode.INSERT | |
189 | ViState.input_mode = property(get_input_mode, set_input_mode) |
|
188 | ViState.input_mode = property(get_input_mode, set_input_mode) | |
190 |
|
189 | |||
191 | return kb |
|
190 | return kb | |
192 |
|
191 | |||
193 |
|
192 | |||
194 | def reformat_text_before_cursor(buffer, document, shell): |
|
193 | def reformat_text_before_cursor(buffer, document, shell): | |
195 | text = buffer.delete_before_cursor(len(document.text[:document.cursor_position])) |
|
194 | text = buffer.delete_before_cursor(len(document.text[:document.cursor_position])) | |
196 | try: |
|
195 | try: | |
197 | formatted_text = shell.reformat_handler(text) |
|
196 | formatted_text = shell.reformat_handler(text) | |
198 | buffer.insert_text(formatted_text) |
|
197 | buffer.insert_text(formatted_text) | |
199 | except Exception as e: |
|
198 | except Exception as e: | |
200 | buffer.insert_text(text) |
|
199 | buffer.insert_text(text) | |
201 |
|
200 | |||
202 |
|
201 | |||
203 | def newline_or_execute_outer(shell): |
|
202 | def newline_or_execute_outer(shell): | |
204 |
|
203 | |||
205 | def newline_or_execute(event): |
|
204 | def newline_or_execute(event): | |
206 | """When the user presses return, insert a newline or execute the code.""" |
|
205 | """When the user presses return, insert a newline or execute the code.""" | |
207 | b = event.current_buffer |
|
206 | b = event.current_buffer | |
208 | d = b.document |
|
207 | d = b.document | |
209 |
|
208 | |||
210 | if b.complete_state: |
|
209 | if b.complete_state: | |
211 | cc = b.complete_state.current_completion |
|
210 | cc = b.complete_state.current_completion | |
212 | if cc: |
|
211 | if cc: | |
213 | b.apply_completion(cc) |
|
212 | b.apply_completion(cc) | |
214 | else: |
|
213 | else: | |
215 | b.cancel_completion() |
|
214 | b.cancel_completion() | |
216 | return |
|
215 | return | |
217 |
|
216 | |||
218 | # If there's only one line, treat it as if the cursor is at the end. |
|
217 | # If there's only one line, treat it as if the cursor is at the end. | |
219 | # See https://github.com/ipython/ipython/issues/10425 |
|
218 | # See https://github.com/ipython/ipython/issues/10425 | |
220 | if d.line_count == 1: |
|
219 | if d.line_count == 1: | |
221 | check_text = d.text |
|
220 | check_text = d.text | |
222 | else: |
|
221 | else: | |
223 | check_text = d.text[:d.cursor_position] |
|
222 | check_text = d.text[:d.cursor_position] | |
224 | status, indent = shell.check_complete(check_text) |
|
223 | status, indent = shell.check_complete(check_text) | |
225 |
|
224 | |||
226 | # if all we have after the cursor is whitespace: reformat current text |
|
225 | # if all we have after the cursor is whitespace: reformat current text | |
227 | # before cursor |
|
226 | # before cursor | |
228 | after_cursor = d.text[d.cursor_position:] |
|
227 | after_cursor = d.text[d.cursor_position:] | |
229 | reformatted = False |
|
228 | reformatted = False | |
230 | if not after_cursor.strip(): |
|
229 | if not after_cursor.strip(): | |
231 | reformat_text_before_cursor(b, d, shell) |
|
230 | reformat_text_before_cursor(b, d, shell) | |
232 | reformatted = True |
|
231 | reformatted = True | |
233 | if not (d.on_last_line or |
|
232 | if not (d.on_last_line or | |
234 | d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() |
|
233 | d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() | |
235 | ): |
|
234 | ): | |
236 | if shell.autoindent: |
|
235 | if shell.autoindent: | |
237 | b.insert_text('\n' + indent) |
|
236 | b.insert_text('\n' + indent) | |
238 | else: |
|
237 | else: | |
239 | b.insert_text('\n') |
|
238 | b.insert_text('\n') | |
240 | return |
|
239 | return | |
241 |
|
240 | |||
242 | if (status != 'incomplete') and b.accept_handler: |
|
241 | if (status != 'incomplete') and b.accept_handler: | |
243 | if not reformatted: |
|
242 | if not reformatted: | |
244 | reformat_text_before_cursor(b, d, shell) |
|
243 | reformat_text_before_cursor(b, d, shell) | |
245 | b.validate_and_handle() |
|
244 | b.validate_and_handle() | |
246 | else: |
|
245 | else: | |
247 | if shell.autoindent: |
|
246 | if shell.autoindent: | |
248 | b.insert_text('\n' + indent) |
|
247 | b.insert_text('\n' + indent) | |
249 | else: |
|
248 | else: | |
250 | b.insert_text('\n') |
|
249 | b.insert_text('\n') | |
251 | return newline_or_execute |
|
250 | return newline_or_execute | |
252 |
|
251 | |||
253 |
|
252 | |||
254 | def previous_history_or_previous_completion(event): |
|
253 | def previous_history_or_previous_completion(event): | |
255 | """ |
|
254 | """ | |
256 | Control-P in vi edit mode on readline is history next, unlike default prompt toolkit. |
|
255 | Control-P in vi edit mode on readline is history next, unlike default prompt toolkit. | |
257 |
|
256 | |||
258 | If completer is open this still select previous completion. |
|
257 | If completer is open this still select previous completion. | |
259 | """ |
|
258 | """ | |
260 | event.current_buffer.auto_up() |
|
259 | event.current_buffer.auto_up() | |
261 |
|
260 | |||
262 |
|
261 | |||
263 | def next_history_or_next_completion(event): |
|
262 | def next_history_or_next_completion(event): | |
264 | """ |
|
263 | """ | |
265 | Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit. |
|
264 | Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit. | |
266 |
|
265 | |||
267 | If completer is open this still select next completion. |
|
266 | If completer is open this still select next completion. | |
268 | """ |
|
267 | """ | |
269 | event.current_buffer.auto_down() |
|
268 | event.current_buffer.auto_down() | |
270 |
|
269 | |||
271 |
|
270 | |||
272 | def dismiss_completion(event): |
|
271 | def dismiss_completion(event): | |
273 | b = event.current_buffer |
|
272 | b = event.current_buffer | |
274 | if b.complete_state: |
|
273 | if b.complete_state: | |
275 | b.cancel_completion() |
|
274 | b.cancel_completion() | |
276 |
|
275 | |||
277 |
|
276 | |||
278 | def reset_buffer(event): |
|
277 | def reset_buffer(event): | |
279 | b = event.current_buffer |
|
278 | b = event.current_buffer | |
280 | if b.complete_state: |
|
279 | if b.complete_state: | |
281 | b.cancel_completion() |
|
280 | b.cancel_completion() | |
282 | else: |
|
281 | else: | |
283 | b.reset() |
|
282 | b.reset() | |
284 |
|
283 | |||
285 |
|
284 | |||
286 | def reset_search_buffer(event): |
|
285 | def reset_search_buffer(event): | |
287 | if event.current_buffer.document.text: |
|
286 | if event.current_buffer.document.text: | |
288 | event.current_buffer.reset() |
|
287 | event.current_buffer.reset() | |
289 | else: |
|
288 | else: | |
290 | event.app.layout.focus(DEFAULT_BUFFER) |
|
289 | event.app.layout.focus(DEFAULT_BUFFER) | |
291 |
|
290 | |||
292 | def suspend_to_bg(event): |
|
291 | def suspend_to_bg(event): | |
293 | event.app.suspend_to_background() |
|
292 | event.app.suspend_to_background() | |
294 |
|
293 | |||
295 | def force_exit(event): |
|
294 | def force_exit(event): | |
296 | """ |
|
295 | """ | |
297 | Force exit (with a non-zero return value) |
|
296 | Force exit (with a non-zero return value) | |
298 | """ |
|
297 | """ | |
299 | sys.exit("Quit") |
|
298 | sys.exit("Quit") | |
300 |
|
299 | |||
301 | def indent_buffer(event): |
|
300 | def indent_buffer(event): | |
302 | event.current_buffer.insert_text(' ' * 4) |
|
301 | event.current_buffer.insert_text(' ' * 4) | |
303 |
|
302 | |||
304 | @undoc |
|
303 | @undoc | |
305 | def newline_with_copy_margin(event): |
|
304 | def newline_with_copy_margin(event): | |
306 | """ |
|
305 | """ | |
307 | DEPRECATED since IPython 6.0 |
|
306 | DEPRECATED since IPython 6.0 | |
308 |
|
307 | |||
309 | See :any:`newline_autoindent_outer` for a replacement. |
|
308 | See :any:`newline_autoindent_outer` for a replacement. | |
310 |
|
309 | |||
311 | Preserve margin and cursor position when using |
|
310 | Preserve margin and cursor position when using | |
312 | Control-O to insert a newline in EMACS mode |
|
311 | Control-O to insert a newline in EMACS mode | |
313 | """ |
|
312 | """ | |
314 | warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " |
|
313 | warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " | |
315 | "see `newline_autoindent_outer(shell)(event)` for a replacement.", |
|
314 | "see `newline_autoindent_outer(shell)(event)` for a replacement.", | |
316 | DeprecationWarning, stacklevel=2) |
|
315 | DeprecationWarning, stacklevel=2) | |
317 |
|
316 | |||
318 | b = event.current_buffer |
|
317 | b = event.current_buffer | |
319 | cursor_start_pos = b.document.cursor_position_col |
|
318 | cursor_start_pos = b.document.cursor_position_col | |
320 | b.newline(copy_margin=True) |
|
319 | b.newline(copy_margin=True) | |
321 | b.cursor_up(count=1) |
|
320 | b.cursor_up(count=1) | |
322 | cursor_end_pos = b.document.cursor_position_col |
|
321 | cursor_end_pos = b.document.cursor_position_col | |
323 | if cursor_start_pos != cursor_end_pos: |
|
322 | if cursor_start_pos != cursor_end_pos: | |
324 | pos_diff = cursor_start_pos - cursor_end_pos |
|
323 | pos_diff = cursor_start_pos - cursor_end_pos | |
325 | b.cursor_right(count=pos_diff) |
|
324 | b.cursor_right(count=pos_diff) | |
326 |
|
325 | |||
327 | def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: |
|
326 | def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: | |
328 | """ |
|
327 | """ | |
329 | Return a function suitable for inserting a indented newline after the cursor. |
|
328 | Return a function suitable for inserting a indented newline after the cursor. | |
330 |
|
329 | |||
331 | Fancier version of deprecated ``newline_with_copy_margin`` which should |
|
330 | Fancier version of deprecated ``newline_with_copy_margin`` which should | |
332 | compute the correct indentation of the inserted line. That is to say, indent |
|
331 | compute the correct indentation of the inserted line. That is to say, indent | |
333 | by 4 extra space after a function definition, class definition, context |
|
332 | by 4 extra space after a function definition, class definition, context | |
334 | manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``. |
|
333 | manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``. | |
335 | """ |
|
334 | """ | |
336 |
|
335 | |||
337 | def newline_autoindent(event): |
|
336 | def newline_autoindent(event): | |
338 | """insert a newline after the cursor indented appropriately.""" |
|
337 | """insert a newline after the cursor indented appropriately.""" | |
339 | b = event.current_buffer |
|
338 | b = event.current_buffer | |
340 | d = b.document |
|
339 | d = b.document | |
341 |
|
340 | |||
342 | if b.complete_state: |
|
341 | if b.complete_state: | |
343 | b.cancel_completion() |
|
342 | b.cancel_completion() | |
344 | text = d.text[:d.cursor_position] + '\n' |
|
343 | text = d.text[:d.cursor_position] + '\n' | |
345 | _, indent = inputsplitter.check_complete(text) |
|
344 | _, indent = inputsplitter.check_complete(text) | |
346 | b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False) |
|
345 | b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False) | |
347 |
|
346 | |||
348 | return newline_autoindent |
|
347 | return newline_autoindent | |
349 |
|
348 | |||
350 |
|
349 | |||
351 | def open_input_in_editor(event): |
|
350 | def open_input_in_editor(event): | |
352 | event.app.current_buffer.open_in_editor() |
|
351 | event.app.current_buffer.open_in_editor() | |
353 |
|
352 | |||
354 |
|
353 | |||
355 | if sys.platform == 'win32': |
|
354 | if sys.platform == 'win32': | |
356 | from IPython.core.error import TryNext |
|
355 | from IPython.core.error import TryNext | |
357 | from IPython.lib.clipboard import (ClipboardEmpty, |
|
356 | from IPython.lib.clipboard import (ClipboardEmpty, | |
358 | win32_clipboard_get, |
|
357 | win32_clipboard_get, | |
359 | tkinter_clipboard_get) |
|
358 | tkinter_clipboard_get) | |
360 |
|
359 | |||
361 | @undoc |
|
360 | @undoc | |
362 | def win_paste(event): |
|
361 | def win_paste(event): | |
363 | try: |
|
362 | try: | |
364 | text = win32_clipboard_get() |
|
363 | text = win32_clipboard_get() | |
365 | except TryNext: |
|
364 | except TryNext: | |
366 | try: |
|
365 | try: | |
367 | text = tkinter_clipboard_get() |
|
366 | text = tkinter_clipboard_get() | |
368 | except (TryNext, ClipboardEmpty): |
|
367 | except (TryNext, ClipboardEmpty): | |
369 | return |
|
368 | return | |
370 | except ClipboardEmpty: |
|
369 | except ClipboardEmpty: | |
371 | return |
|
370 | return | |
372 | event.current_buffer.insert_text(text.replace("\t", " " * 4)) |
|
371 | event.current_buffer.insert_text(text.replace("\t", " " * 4)) |
@@ -1,212 +1,207 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | """Decorators for labeling test objects. |
|
2 | """Decorators for labeling test objects. | |
3 |
|
3 | |||
4 | Decorators that merely return a modified version of the original function |
|
4 | Decorators that merely return a modified version of the original function | |
5 | object are straightforward. Decorators that return a new function object need |
|
5 | object are straightforward. Decorators that return a new function object need | |
6 | to use nose.tools.make_decorator(original_function)(decorator) in returning the |
|
6 | to use nose.tools.make_decorator(original_function)(decorator) in returning the | |
7 | decorator, in order to preserve metadata such as function name, setup and |
|
7 | decorator, in order to preserve metadata such as function name, setup and | |
8 | teardown functions and so on - see nose.tools for more information. |
|
8 | teardown functions and so on - see nose.tools for more information. | |
9 |
|
9 | |||
10 | This module provides a set of useful decorators meant to be ready to use in |
|
10 | This module provides a set of useful decorators meant to be ready to use in | |
11 | your own tests. See the bottom of the file for the ready-made ones, and if you |
|
11 | your own tests. See the bottom of the file for the ready-made ones, and if you | |
12 | find yourself writing a new one that may be of generic use, add it here. |
|
12 | find yourself writing a new one that may be of generic use, add it here. | |
13 |
|
13 | |||
14 | Included decorators: |
|
14 | Included decorators: | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | Lightweight testing that remains unittest-compatible. |
|
17 | Lightweight testing that remains unittest-compatible. | |
18 |
|
18 | |||
19 | - An @as_unittest decorator can be used to tag any normal parameter-less |
|
19 | - An @as_unittest decorator can be used to tag any normal parameter-less | |
20 | function as a unittest TestCase. Then, both nose and normal unittest will |
|
20 | function as a unittest TestCase. Then, both nose and normal unittest will | |
21 | recognize it as such. This will make it easier to migrate away from Nose if |
|
21 | recognize it as such. This will make it easier to migrate away from Nose if | |
22 | we ever need/want to while maintaining very lightweight tests. |
|
22 | we ever need/want to while maintaining very lightweight tests. | |
23 |
|
23 | |||
24 | NOTE: This file contains IPython-specific decorators. Using the machinery in |
|
24 | NOTE: This file contains IPython-specific decorators. Using the machinery in | |
25 | IPython.external.decorators, we import either numpy.testing.decorators if numpy is |
|
25 | IPython.external.decorators, we import either numpy.testing.decorators if numpy is | |
26 | available, OR use equivalent code in IPython.external._decorators, which |
|
26 | available, OR use equivalent code in IPython.external._decorators, which | |
27 | we've copied verbatim from numpy. |
|
27 | we've copied verbatim from numpy. | |
28 |
|
28 | |||
29 | """ |
|
29 | """ | |
30 |
|
30 | |||
31 | # Copyright (c) IPython Development Team. |
|
31 | # Copyright (c) IPython Development Team. | |
32 | # Distributed under the terms of the Modified BSD License. |
|
32 | # Distributed under the terms of the Modified BSD License. | |
33 |
|
33 | |||
34 | import os |
|
34 | import os | |
35 | import shutil |
|
35 | import shutil | |
36 | import sys |
|
36 | import sys | |
37 | import tempfile |
|
37 | import tempfile | |
38 | import unittest |
|
38 | import unittest | |
39 | import warnings |
|
39 | import warnings | |
40 | from importlib import import_module |
|
40 | from importlib import import_module | |
41 |
|
41 | |||
42 | from decorator import decorator |
|
42 | from decorator import decorator | |
43 |
|
43 | |||
44 | # Expose the unittest-driven decorators |
|
44 | # Expose the unittest-driven decorators | |
45 | from .ipunittest import ipdoctest, ipdocstring |
|
45 | from .ipunittest import ipdoctest, ipdocstring | |
46 |
|
46 | |||
47 | #----------------------------------------------------------------------------- |
|
47 | #----------------------------------------------------------------------------- | |
48 | # Classes and functions |
|
48 | # Classes and functions | |
49 | #----------------------------------------------------------------------------- |
|
49 | #----------------------------------------------------------------------------- | |
50 |
|
50 | |||
51 | # Simple example of the basic idea |
|
51 | # Simple example of the basic idea | |
52 | def as_unittest(func): |
|
52 | def as_unittest(func): | |
53 | """Decorator to make a simple function into a normal test via unittest.""" |
|
53 | """Decorator to make a simple function into a normal test via unittest.""" | |
54 | class Tester(unittest.TestCase): |
|
54 | class Tester(unittest.TestCase): | |
55 | def test(self): |
|
55 | def test(self): | |
56 | func() |
|
56 | func() | |
57 |
|
57 | |||
58 | Tester.__name__ = func.__name__ |
|
58 | Tester.__name__ = func.__name__ | |
59 |
|
59 | |||
60 | return Tester |
|
60 | return Tester | |
61 |
|
61 | |||
62 | # Utility functions |
|
62 | # Utility functions | |
63 |
|
63 | |||
64 |
|
64 | |||
65 | def skipif(skip_condition, msg=None): |
|
65 | def skipif(skip_condition, msg=None): | |
66 | """Make function raise SkipTest exception if skip_condition is true |
|
66 | """Make function raise SkipTest exception if skip_condition is true | |
67 |
|
67 | |||
68 | Parameters |
|
68 | Parameters | |
69 | ---------- |
|
69 | ---------- | |
70 |
|
70 | |||
71 | skip_condition : bool or callable |
|
71 | skip_condition : bool or callable | |
72 | Flag to determine whether to skip test. If the condition is a |
|
72 | Flag to determine whether to skip test. If the condition is a | |
73 | callable, it is used at runtime to dynamically make the decision. This |
|
73 | callable, it is used at runtime to dynamically make the decision. This | |
74 | is useful for tests that may require costly imports, to delay the cost |
|
74 | is useful for tests that may require costly imports, to delay the cost | |
75 | until the test suite is actually executed. |
|
75 | until the test suite is actually executed. | |
76 | msg : string |
|
76 | msg : string | |
77 | Message to give on raising a SkipTest exception. |
|
77 | Message to give on raising a SkipTest exception. | |
78 |
|
78 | |||
79 | Returns |
|
79 | Returns | |
80 | ------- |
|
80 | ------- | |
81 | decorator : function |
|
81 | decorator : function | |
82 | Decorator, which, when applied to a function, causes SkipTest |
|
82 | Decorator, which, when applied to a function, causes SkipTest | |
83 | to be raised when the skip_condition was True, and the function |
|
83 | to be raised when the skip_condition was True, and the function | |
84 | to be called normally otherwise. |
|
84 | to be called normally otherwise. | |
85 | """ |
|
85 | """ | |
86 | if msg is None: |
|
86 | if msg is None: | |
87 | msg = "Test skipped due to test condition." |
|
87 | msg = "Test skipped due to test condition." | |
88 |
|
88 | |||
89 | import pytest |
|
89 | import pytest | |
90 |
|
90 | |||
91 | assert isinstance(skip_condition, bool) |
|
91 | assert isinstance(skip_condition, bool) | |
92 | return pytest.mark.skipif(skip_condition, reason=msg) |
|
92 | return pytest.mark.skipif(skip_condition, reason=msg) | |
93 |
|
93 | |||
94 |
|
94 | |||
95 | # A version with the condition set to true, common case just to attach a message |
|
95 | # A version with the condition set to true, common case just to attach a message | |
96 | # to a skip decorator |
|
96 | # to a skip decorator | |
97 | def skip(msg=None): |
|
97 | def skip(msg=None): | |
98 | """Decorator factory - mark a test function for skipping from test suite. |
|
98 | """Decorator factory - mark a test function for skipping from test suite. | |
99 |
|
99 | |||
100 | Parameters |
|
100 | Parameters | |
101 | ---------- |
|
101 | ---------- | |
102 | msg : string |
|
102 | msg : string | |
103 | Optional message to be added. |
|
103 | Optional message to be added. | |
104 |
|
104 | |||
105 | Returns |
|
105 | Returns | |
106 | ------- |
|
106 | ------- | |
107 | decorator : function |
|
107 | decorator : function | |
108 | Decorator, which, when applied to a function, causes SkipTest |
|
108 | Decorator, which, when applied to a function, causes SkipTest | |
109 | to be raised, with the optional message added. |
|
109 | to be raised, with the optional message added. | |
110 | """ |
|
110 | """ | |
111 | if msg and not isinstance(msg, str): |
|
111 | if msg and not isinstance(msg, str): | |
112 | raise ValueError('invalid object passed to `@skip` decorator, did you ' |
|
112 | raise ValueError('invalid object passed to `@skip` decorator, did you ' | |
113 | 'meant `@skip()` with brackets ?') |
|
113 | 'meant `@skip()` with brackets ?') | |
114 | return skipif(True, msg) |
|
114 | return skipif(True, msg) | |
115 |
|
115 | |||
116 |
|
116 | |||
117 | def onlyif(condition, msg): |
|
117 | def onlyif(condition, msg): | |
118 | """The reverse from skipif, see skipif for details.""" |
|
118 | """The reverse from skipif, see skipif for details.""" | |
119 |
|
119 | |||
120 | return skipif(not condition, msg) |
|
120 | return skipif(not condition, msg) | |
121 |
|
121 | |||
122 | #----------------------------------------------------------------------------- |
|
122 | #----------------------------------------------------------------------------- | |
123 | # Utility functions for decorators |
|
123 | # Utility functions for decorators | |
124 | def module_not_available(module): |
|
124 | def module_not_available(module): | |
125 | """Can module be imported? Returns true if module does NOT import. |
|
125 | """Can module be imported? Returns true if module does NOT import. | |
126 |
|
126 | |||
127 | This is used to make a decorator to skip tests that require module to be |
|
127 | This is used to make a decorator to skip tests that require module to be | |
128 | available, but delay the 'import numpy' to test execution time. |
|
128 | available, but delay the 'import numpy' to test execution time. | |
129 | """ |
|
129 | """ | |
130 | try: |
|
130 | try: | |
131 | mod = import_module(module) |
|
131 | mod = import_module(module) | |
132 | mod_not_avail = False |
|
132 | mod_not_avail = False | |
133 | except ImportError: |
|
133 | except ImportError: | |
134 | mod_not_avail = True |
|
134 | mod_not_avail = True | |
135 |
|
135 | |||
136 | return mod_not_avail |
|
136 | return mod_not_avail | |
137 |
|
137 | |||
138 |
|
138 | |||
139 | #----------------------------------------------------------------------------- |
|
139 | #----------------------------------------------------------------------------- | |
140 | # Decorators for public use |
|
140 | # Decorators for public use | |
141 |
|
141 | |||
142 | # Decorators to skip certain tests on specific platforms. |
|
142 | # Decorators to skip certain tests on specific platforms. | |
143 | skip_win32 = skipif(sys.platform == 'win32', |
|
143 | skip_win32 = skipif(sys.platform == 'win32', | |
144 | "This test does not run under Windows") |
|
144 | "This test does not run under Windows") | |
145 | skip_linux = skipif(sys.platform.startswith('linux'), |
|
145 | skip_linux = skipif(sys.platform.startswith('linux'), | |
146 | "This test does not run under Linux") |
|
146 | "This test does not run under Linux") | |
147 | skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X") |
|
147 | skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X") | |
148 |
|
148 | |||
149 |
|
149 | |||
150 | # Decorators to skip tests if not on specific platforms. |
|
150 | # Decorators to skip tests if not on specific platforms. | |
151 | skip_if_not_win32 = skipif(sys.platform != 'win32', |
|
151 | skip_if_not_win32 = skipif(sys.platform != 'win32', | |
152 | "This test only runs under Windows") |
|
152 | "This test only runs under Windows") | |
153 | skip_if_not_linux = skipif(not sys.platform.startswith('linux'), |
|
153 | skip_if_not_linux = skipif(not sys.platform.startswith('linux'), | |
154 | "This test only runs under Linux") |
|
154 | "This test only runs under Linux") | |
155 | skip_if_not_osx = skipif(sys.platform != 'darwin', |
|
155 | skip_if_not_osx = skipif(sys.platform != 'darwin', | |
156 | "This test only runs under OSX") |
|
156 | "This test only runs under OSX") | |
157 |
|
157 | |||
158 |
|
158 | |||
159 | _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and |
|
159 | _x11_skip_cond = (sys.platform not in ('darwin', 'win32') and | |
160 | os.environ.get('DISPLAY', '') == '') |
|
160 | os.environ.get('DISPLAY', '') == '') | |
161 | _x11_skip_msg = "Skipped under *nix when X11/XOrg not available" |
|
161 | _x11_skip_msg = "Skipped under *nix when X11/XOrg not available" | |
162 |
|
162 | |||
163 | skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg) |
|
163 | skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg) | |
164 |
|
164 | |||
165 |
|
||||
166 | # Decorators to skip certain tests on specific platform/python combinations |
|
|||
167 | skip_win32_py38 = skipif(sys.version_info > (3,8) and os.name == 'nt') |
|
|||
168 |
|
||||
169 |
|
||||
170 | # Other skip decorators |
|
165 | # Other skip decorators | |
171 |
|
166 | |||
172 | # generic skip without module |
|
167 | # generic skip without module | |
173 | skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod) |
|
168 | skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod) | |
174 |
|
169 | |||
175 | skipif_not_numpy = skip_without('numpy') |
|
170 | skipif_not_numpy = skip_without('numpy') | |
176 |
|
171 | |||
177 | skipif_not_matplotlib = skip_without('matplotlib') |
|
172 | skipif_not_matplotlib = skip_without('matplotlib') | |
178 |
|
173 | |||
179 | skipif_not_sympy = skip_without('sympy') |
|
174 | skipif_not_sympy = skip_without('sympy') | |
180 |
|
175 | |||
181 | # A null 'decorator', useful to make more readable code that needs to pick |
|
176 | # A null 'decorator', useful to make more readable code that needs to pick | |
182 | # between different decorators based on OS or other conditions |
|
177 | # between different decorators based on OS or other conditions | |
183 | null_deco = lambda f: f |
|
178 | null_deco = lambda f: f | |
184 |
|
179 | |||
185 | # Some tests only run where we can use unicode paths. Note that we can't just |
|
180 | # Some tests only run where we can use unicode paths. Note that we can't just | |
186 | # check os.path.supports_unicode_filenames, which is always False on Linux. |
|
181 | # check os.path.supports_unicode_filenames, which is always False on Linux. | |
187 | try: |
|
182 | try: | |
188 | f = tempfile.NamedTemporaryFile(prefix=u"tmpβ¬") |
|
183 | f = tempfile.NamedTemporaryFile(prefix=u"tmpβ¬") | |
189 | except UnicodeEncodeError: |
|
184 | except UnicodeEncodeError: | |
190 | unicode_paths = False |
|
185 | unicode_paths = False | |
191 | else: |
|
186 | else: | |
192 | unicode_paths = True |
|
187 | unicode_paths = True | |
193 | f.close() |
|
188 | f.close() | |
194 |
|
189 | |||
195 | onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable " |
|
190 | onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable " | |
196 | "where we can use unicode in filenames.")) |
|
191 | "where we can use unicode in filenames.")) | |
197 |
|
192 | |||
198 |
|
193 | |||
199 | def onlyif_cmds_exist(*commands): |
|
194 | def onlyif_cmds_exist(*commands): | |
200 | """ |
|
195 | """ | |
201 | Decorator to skip test when at least one of `commands` is not found. |
|
196 | Decorator to skip test when at least one of `commands` is not found. | |
202 | """ |
|
197 | """ | |
203 | for cmd in commands: |
|
198 | for cmd in commands: | |
204 | reason = f"This test runs only if command '{cmd}' is installed" |
|
199 | reason = f"This test runs only if command '{cmd}' is installed" | |
205 | if not shutil.which(cmd): |
|
200 | if not shutil.which(cmd): | |
206 | if os.environ.get("IPTEST_WORKING_DIR", None) is not None: |
|
201 | if os.environ.get("IPTEST_WORKING_DIR", None) is not None: | |
207 | return skip(reason) |
|
202 | return skip(reason) | |
208 | else: |
|
203 | else: | |
209 | import pytest |
|
204 | import pytest | |
210 |
|
205 | |||
211 | return pytest.mark.skip(reason=reason) |
|
206 | return pytest.mark.skip(reason=reason) | |
212 | return null_deco |
|
207 | return null_deco |
@@ -1,466 +1,463 b'' | |||||
1 | """Generic testing tools. |
|
1 | """Generic testing tools. | |
2 |
|
2 | |||
3 | Authors |
|
3 | Authors | |
4 | ------- |
|
4 | ------- | |
5 | - Fernando Perez <Fernando.Perez@berkeley.edu> |
|
5 | - Fernando Perez <Fernando.Perez@berkeley.edu> | |
6 | """ |
|
6 | """ | |
7 |
|
7 | |||
8 |
|
8 | |||
9 | # Copyright (c) IPython Development Team. |
|
9 | # Copyright (c) IPython Development Team. | |
10 | # Distributed under the terms of the Modified BSD License. |
|
10 | # Distributed under the terms of the Modified BSD License. | |
11 |
|
11 | |||
12 | import os |
|
12 | import os | |
13 | from pathlib import Path |
|
13 | from pathlib import Path | |
14 | import re |
|
14 | import re | |
15 | import sys |
|
15 | import sys | |
16 | import tempfile |
|
16 | import tempfile | |
17 | import unittest |
|
17 | import unittest | |
18 |
|
18 | |||
19 | from contextlib import contextmanager |
|
19 | from contextlib import contextmanager | |
20 | from io import StringIO |
|
20 | from io import StringIO | |
21 | from subprocess import Popen, PIPE |
|
21 | from subprocess import Popen, PIPE | |
22 | from unittest.mock import patch |
|
22 | from unittest.mock import patch | |
23 |
|
23 | |||
24 | from traitlets.config.loader import Config |
|
24 | from traitlets.config.loader import Config | |
25 | from IPython.utils.process import get_output_error_code |
|
25 | from IPython.utils.process import get_output_error_code | |
26 | from IPython.utils.text import list_strings |
|
26 | from IPython.utils.text import list_strings | |
27 | from IPython.utils.io import temp_pyfile, Tee |
|
27 | from IPython.utils.io import temp_pyfile, Tee | |
28 | from IPython.utils import py3compat |
|
28 | from IPython.utils import py3compat | |
29 |
|
29 | |||
30 | from . import decorators as dec |
|
30 | from . import decorators as dec | |
31 | from . import skipdoctest |
|
31 | from . import skipdoctest | |
32 |
|
32 | |||
33 |
|
33 | |||
34 | # The docstring for full_path doctests differently on win32 (different path |
|
34 | # The docstring for full_path doctests differently on win32 (different path | |
35 | # separator) so just skip the doctest there. The example remains informative. |
|
35 | # separator) so just skip the doctest there. The example remains informative. | |
36 | doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco |
|
36 | doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco | |
37 |
|
37 | |||
38 | @doctest_deco |
|
38 | @doctest_deco | |
39 | def full_path(startPath,files): |
|
39 | def full_path(startPath,files): | |
40 | """Make full paths for all the listed files, based on startPath. |
|
40 | """Make full paths for all the listed files, based on startPath. | |
41 |
|
41 | |||
42 | Only the base part of startPath is kept, since this routine is typically |
|
42 | Only the base part of startPath is kept, since this routine is typically | |
43 | used with a script's ``__file__`` variable as startPath. The base of startPath |
|
43 | used with a script's ``__file__`` variable as startPath. The base of startPath | |
44 | is then prepended to all the listed files, forming the output list. |
|
44 | is then prepended to all the listed files, forming the output list. | |
45 |
|
45 | |||
46 | Parameters |
|
46 | Parameters | |
47 | ---------- |
|
47 | ---------- | |
48 | startPath : string |
|
48 | startPath : string | |
49 | Initial path to use as the base for the results. This path is split |
|
49 | Initial path to use as the base for the results. This path is split | |
50 | using os.path.split() and only its first component is kept. |
|
50 | using os.path.split() and only its first component is kept. | |
51 |
|
51 | |||
52 | files : string or list |
|
52 | files : string or list | |
53 | One or more files. |
|
53 | One or more files. | |
54 |
|
54 | |||
55 | Examples |
|
55 | Examples | |
56 | -------- |
|
56 | -------- | |
57 |
|
57 | |||
58 | >>> full_path('/foo/bar.py',['a.txt','b.txt']) |
|
58 | >>> full_path('/foo/bar.py',['a.txt','b.txt']) | |
59 | ['/foo/a.txt', '/foo/b.txt'] |
|
59 | ['/foo/a.txt', '/foo/b.txt'] | |
60 |
|
60 | |||
61 | >>> full_path('/foo',['a.txt','b.txt']) |
|
61 | >>> full_path('/foo',['a.txt','b.txt']) | |
62 | ['/a.txt', '/b.txt'] |
|
62 | ['/a.txt', '/b.txt'] | |
63 |
|
63 | |||
64 | If a single file is given, the output is still a list:: |
|
64 | If a single file is given, the output is still a list:: | |
65 |
|
65 | |||
66 | >>> full_path('/foo','a.txt') |
|
66 | >>> full_path('/foo','a.txt') | |
67 | ['/a.txt'] |
|
67 | ['/a.txt'] | |
68 | """ |
|
68 | """ | |
69 |
|
69 | |||
70 | files = list_strings(files) |
|
70 | files = list_strings(files) | |
71 | base = os.path.split(startPath)[0] |
|
71 | base = os.path.split(startPath)[0] | |
72 | return [ os.path.join(base,f) for f in files ] |
|
72 | return [ os.path.join(base,f) for f in files ] | |
73 |
|
73 | |||
74 |
|
74 | |||
75 | def parse_test_output(txt): |
|
75 | def parse_test_output(txt): | |
76 | """Parse the output of a test run and return errors, failures. |
|
76 | """Parse the output of a test run and return errors, failures. | |
77 |
|
77 | |||
78 | Parameters |
|
78 | Parameters | |
79 | ---------- |
|
79 | ---------- | |
80 | txt : str |
|
80 | txt : str | |
81 | Text output of a test run, assumed to contain a line of one of the |
|
81 | Text output of a test run, assumed to contain a line of one of the | |
82 | following forms:: |
|
82 | following forms:: | |
83 |
|
83 | |||
84 | 'FAILED (errors=1)' |
|
84 | 'FAILED (errors=1)' | |
85 | 'FAILED (failures=1)' |
|
85 | 'FAILED (failures=1)' | |
86 | 'FAILED (errors=1, failures=1)' |
|
86 | 'FAILED (errors=1, failures=1)' | |
87 |
|
87 | |||
88 | Returns |
|
88 | Returns | |
89 | ------- |
|
89 | ------- | |
90 | nerr, nfail |
|
90 | nerr, nfail | |
91 | number of errors and failures. |
|
91 | number of errors and failures. | |
92 | """ |
|
92 | """ | |
93 |
|
93 | |||
94 | err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE) |
|
94 | err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE) | |
95 | if err_m: |
|
95 | if err_m: | |
96 | nerr = int(err_m.group(1)) |
|
96 | nerr = int(err_m.group(1)) | |
97 | nfail = 0 |
|
97 | nfail = 0 | |
98 | return nerr, nfail |
|
98 | return nerr, nfail | |
99 |
|
99 | |||
100 | fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE) |
|
100 | fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE) | |
101 | if fail_m: |
|
101 | if fail_m: | |
102 | nerr = 0 |
|
102 | nerr = 0 | |
103 | nfail = int(fail_m.group(1)) |
|
103 | nfail = int(fail_m.group(1)) | |
104 | return nerr, nfail |
|
104 | return nerr, nfail | |
105 |
|
105 | |||
106 | both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt, |
|
106 | both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt, | |
107 | re.MULTILINE) |
|
107 | re.MULTILINE) | |
108 | if both_m: |
|
108 | if both_m: | |
109 | nerr = int(both_m.group(1)) |
|
109 | nerr = int(both_m.group(1)) | |
110 | nfail = int(both_m.group(2)) |
|
110 | nfail = int(both_m.group(2)) | |
111 | return nerr, nfail |
|
111 | return nerr, nfail | |
112 |
|
112 | |||
113 | # If the input didn't match any of these forms, assume no error/failures |
|
113 | # If the input didn't match any of these forms, assume no error/failures | |
114 | return 0, 0 |
|
114 | return 0, 0 | |
115 |
|
115 | |||
116 |
|
116 | |||
117 | # So nose doesn't think this is a test |
|
117 | # So nose doesn't think this is a test | |
118 | parse_test_output.__test__ = False |
|
118 | parse_test_output.__test__ = False | |
119 |
|
119 | |||
120 |
|
120 | |||
121 | def default_argv(): |
|
121 | def default_argv(): | |
122 | """Return a valid default argv for creating testing instances of ipython""" |
|
122 | """Return a valid default argv for creating testing instances of ipython""" | |
123 |
|
123 | |||
124 | return ['--quick', # so no config file is loaded |
|
124 | return ['--quick', # so no config file is loaded | |
125 | # Other defaults to minimize side effects on stdout |
|
125 | # Other defaults to minimize side effects on stdout | |
126 | '--colors=NoColor', '--no-term-title','--no-banner', |
|
126 | '--colors=NoColor', '--no-term-title','--no-banner', | |
127 | '--autocall=0'] |
|
127 | '--autocall=0'] | |
128 |
|
128 | |||
129 |
|
129 | |||
130 | def default_config(): |
|
130 | def default_config(): | |
131 | """Return a config object with good defaults for testing.""" |
|
131 | """Return a config object with good defaults for testing.""" | |
132 | config = Config() |
|
132 | config = Config() | |
133 | config.TerminalInteractiveShell.colors = 'NoColor' |
|
133 | config.TerminalInteractiveShell.colors = 'NoColor' | |
134 | config.TerminalTerminalInteractiveShell.term_title = False, |
|
134 | config.TerminalTerminalInteractiveShell.term_title = False, | |
135 | config.TerminalInteractiveShell.autocall = 0 |
|
135 | config.TerminalInteractiveShell.autocall = 0 | |
136 | f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False) |
|
136 | f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False) | |
137 | config.HistoryManager.hist_file = Path(f.name) |
|
137 | config.HistoryManager.hist_file = Path(f.name) | |
138 | f.close() |
|
138 | f.close() | |
139 | config.HistoryManager.db_cache_size = 10000 |
|
139 | config.HistoryManager.db_cache_size = 10000 | |
140 | return config |
|
140 | return config | |
141 |
|
141 | |||
142 |
|
142 | |||
143 | def get_ipython_cmd(as_string=False): |
|
143 | def get_ipython_cmd(as_string=False): | |
144 | """ |
|
144 | """ | |
145 | Return appropriate IPython command line name. By default, this will return |
|
145 | Return appropriate IPython command line name. By default, this will return | |
146 | a list that can be used with subprocess.Popen, for example, but passing |
|
146 | a list that can be used with subprocess.Popen, for example, but passing | |
147 | `as_string=True` allows for returning the IPython command as a string. |
|
147 | `as_string=True` allows for returning the IPython command as a string. | |
148 |
|
148 | |||
149 | Parameters |
|
149 | Parameters | |
150 | ---------- |
|
150 | ---------- | |
151 | as_string: bool |
|
151 | as_string: bool | |
152 | Flag to allow to return the command as a string. |
|
152 | Flag to allow to return the command as a string. | |
153 | """ |
|
153 | """ | |
154 | ipython_cmd = [sys.executable, "-m", "IPython"] |
|
154 | ipython_cmd = [sys.executable, "-m", "IPython"] | |
155 |
|
155 | |||
156 | if as_string: |
|
156 | if as_string: | |
157 | ipython_cmd = " ".join(ipython_cmd) |
|
157 | ipython_cmd = " ".join(ipython_cmd) | |
158 |
|
158 | |||
159 | return ipython_cmd |
|
159 | return ipython_cmd | |
160 |
|
160 | |||
161 | def ipexec(fname, options=None, commands=()): |
|
161 | def ipexec(fname, options=None, commands=()): | |
162 | """Utility to call 'ipython filename'. |
|
162 | """Utility to call 'ipython filename'. | |
163 |
|
163 | |||
164 | Starts IPython with a minimal and safe configuration to make startup as fast |
|
164 | Starts IPython with a minimal and safe configuration to make startup as fast | |
165 | as possible. |
|
165 | as possible. | |
166 |
|
166 | |||
167 | Note that this starts IPython in a subprocess! |
|
167 | Note that this starts IPython in a subprocess! | |
168 |
|
168 | |||
169 | Parameters |
|
169 | Parameters | |
170 | ---------- |
|
170 | ---------- | |
171 | fname : str, Path |
|
171 | fname : str, Path | |
172 | Name of file to be executed (should have .py or .ipy extension). |
|
172 | Name of file to be executed (should have .py or .ipy extension). | |
173 |
|
173 | |||
174 | options : optional, list |
|
174 | options : optional, list | |
175 | Extra command-line flags to be passed to IPython. |
|
175 | Extra command-line flags to be passed to IPython. | |
176 |
|
176 | |||
177 | commands : optional, list |
|
177 | commands : optional, list | |
178 | Commands to send in on stdin |
|
178 | Commands to send in on stdin | |
179 |
|
179 | |||
180 | Returns |
|
180 | Returns | |
181 | ------- |
|
181 | ------- | |
182 | ``(stdout, stderr)`` of ipython subprocess. |
|
182 | ``(stdout, stderr)`` of ipython subprocess. | |
183 | """ |
|
183 | """ | |
184 | if options is None: options = [] |
|
184 | if options is None: options = [] | |
185 |
|
185 | |||
186 | cmdargs = default_argv() + options |
|
186 | cmdargs = default_argv() + options | |
187 |
|
187 | |||
188 | test_dir = os.path.dirname(__file__) |
|
188 | test_dir = os.path.dirname(__file__) | |
189 |
|
189 | |||
190 | ipython_cmd = get_ipython_cmd() |
|
190 | ipython_cmd = get_ipython_cmd() | |
191 | # Absolute path for filename |
|
191 | # Absolute path for filename | |
192 | full_fname = os.path.join(test_dir, fname) |
|
192 | full_fname = os.path.join(test_dir, fname) | |
193 | full_cmd = ipython_cmd + cmdargs + ['--', full_fname] |
|
193 | full_cmd = ipython_cmd + cmdargs + ['--', full_fname] | |
194 | if sys.platform == "win32" and sys.version_info < (3, 8): |
|
|||
195 | # subprocess.Popen does not support Path objects yet |
|
|||
196 | full_cmd = list(map(str, full_cmd)) |
|
|||
197 | env = os.environ.copy() |
|
194 | env = os.environ.copy() | |
198 | # FIXME: ignore all warnings in ipexec while we have shims |
|
195 | # FIXME: ignore all warnings in ipexec while we have shims | |
199 | # should we keep suppressing warnings here, even after removing shims? |
|
196 | # should we keep suppressing warnings here, even after removing shims? | |
200 | env['PYTHONWARNINGS'] = 'ignore' |
|
197 | env['PYTHONWARNINGS'] = 'ignore' | |
201 | # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr |
|
198 | # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr | |
202 | # Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout) |
|
199 | # Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout) | |
203 | env.pop("PYCHARM_HOSTED", None) |
|
200 | env.pop("PYCHARM_HOSTED", None) | |
204 | for k, v in env.items(): |
|
201 | for k, v in env.items(): | |
205 | # Debug a bizarre failure we've seen on Windows: |
|
202 | # Debug a bizarre failure we've seen on Windows: | |
206 | # TypeError: environment can only contain strings |
|
203 | # TypeError: environment can only contain strings | |
207 | if not isinstance(v, str): |
|
204 | if not isinstance(v, str): | |
208 | print(k, v) |
|
205 | print(k, v) | |
209 | p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env) |
|
206 | p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env) | |
210 | out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None) |
|
207 | out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None) | |
211 | out, err = py3compat.decode(out), py3compat.decode(err) |
|
208 | out, err = py3compat.decode(out), py3compat.decode(err) | |
212 | # `import readline` causes 'ESC[?1034h' to be output sometimes, |
|
209 | # `import readline` causes 'ESC[?1034h' to be output sometimes, | |
213 | # so strip that out before doing comparisons |
|
210 | # so strip that out before doing comparisons | |
214 | if out: |
|
211 | if out: | |
215 | out = re.sub(r'\x1b\[[^h]+h', '', out) |
|
212 | out = re.sub(r'\x1b\[[^h]+h', '', out) | |
216 | return out, err |
|
213 | return out, err | |
217 |
|
214 | |||
218 |
|
215 | |||
219 | def ipexec_validate(fname, expected_out, expected_err='', |
|
216 | def ipexec_validate(fname, expected_out, expected_err='', | |
220 | options=None, commands=()): |
|
217 | options=None, commands=()): | |
221 | """Utility to call 'ipython filename' and validate output/error. |
|
218 | """Utility to call 'ipython filename' and validate output/error. | |
222 |
|
219 | |||
223 | This function raises an AssertionError if the validation fails. |
|
220 | This function raises an AssertionError if the validation fails. | |
224 |
|
221 | |||
225 | Note that this starts IPython in a subprocess! |
|
222 | Note that this starts IPython in a subprocess! | |
226 |
|
223 | |||
227 | Parameters |
|
224 | Parameters | |
228 | ---------- |
|
225 | ---------- | |
229 | fname : str, Path |
|
226 | fname : str, Path | |
230 | Name of the file to be executed (should have .py or .ipy extension). |
|
227 | Name of the file to be executed (should have .py or .ipy extension). | |
231 |
|
228 | |||
232 | expected_out : str |
|
229 | expected_out : str | |
233 | Expected stdout of the process. |
|
230 | Expected stdout of the process. | |
234 |
|
231 | |||
235 | expected_err : optional, str |
|
232 | expected_err : optional, str | |
236 | Expected stderr of the process. |
|
233 | Expected stderr of the process. | |
237 |
|
234 | |||
238 | options : optional, list |
|
235 | options : optional, list | |
239 | Extra command-line flags to be passed to IPython. |
|
236 | Extra command-line flags to be passed to IPython. | |
240 |
|
237 | |||
241 | Returns |
|
238 | Returns | |
242 | ------- |
|
239 | ------- | |
243 | None |
|
240 | None | |
244 | """ |
|
241 | """ | |
245 |
|
242 | |||
246 | out, err = ipexec(fname, options, commands) |
|
243 | out, err = ipexec(fname, options, commands) | |
247 | #print 'OUT', out # dbg |
|
244 | #print 'OUT', out # dbg | |
248 | #print 'ERR', err # dbg |
|
245 | #print 'ERR', err # dbg | |
249 | # If there are any errors, we must check those before stdout, as they may be |
|
246 | # If there are any errors, we must check those before stdout, as they may be | |
250 | # more informative than simply having an empty stdout. |
|
247 | # more informative than simply having an empty stdout. | |
251 | if err: |
|
248 | if err: | |
252 | if expected_err: |
|
249 | if expected_err: | |
253 | assert err.strip().splitlines() == expected_err.strip().splitlines() |
|
250 | assert err.strip().splitlines() == expected_err.strip().splitlines() | |
254 | else: |
|
251 | else: | |
255 | raise ValueError('Running file %r produced error: %r' % |
|
252 | raise ValueError('Running file %r produced error: %r' % | |
256 | (fname, err)) |
|
253 | (fname, err)) | |
257 | # If no errors or output on stderr was expected, match stdout |
|
254 | # If no errors or output on stderr was expected, match stdout | |
258 | assert out.strip().splitlines() == expected_out.strip().splitlines() |
|
255 | assert out.strip().splitlines() == expected_out.strip().splitlines() | |
259 |
|
256 | |||
260 |
|
257 | |||
261 | class TempFileMixin(unittest.TestCase): |
|
258 | class TempFileMixin(unittest.TestCase): | |
262 | """Utility class to create temporary Python/IPython files. |
|
259 | """Utility class to create temporary Python/IPython files. | |
263 |
|
260 | |||
264 | Meant as a mixin class for test cases.""" |
|
261 | Meant as a mixin class for test cases.""" | |
265 |
|
262 | |||
266 | def mktmp(self, src, ext='.py'): |
|
263 | def mktmp(self, src, ext='.py'): | |
267 | """Make a valid python temp file.""" |
|
264 | """Make a valid python temp file.""" | |
268 | fname = temp_pyfile(src, ext) |
|
265 | fname = temp_pyfile(src, ext) | |
269 | if not hasattr(self, 'tmps'): |
|
266 | if not hasattr(self, 'tmps'): | |
270 | self.tmps=[] |
|
267 | self.tmps=[] | |
271 | self.tmps.append(fname) |
|
268 | self.tmps.append(fname) | |
272 | self.fname = fname |
|
269 | self.fname = fname | |
273 |
|
270 | |||
274 | def tearDown(self): |
|
271 | def tearDown(self): | |
275 | # If the tmpfile wasn't made because of skipped tests, like in |
|
272 | # If the tmpfile wasn't made because of skipped tests, like in | |
276 | # win32, there's nothing to cleanup. |
|
273 | # win32, there's nothing to cleanup. | |
277 | if hasattr(self, 'tmps'): |
|
274 | if hasattr(self, 'tmps'): | |
278 | for fname in self.tmps: |
|
275 | for fname in self.tmps: | |
279 | # If the tmpfile wasn't made because of skipped tests, like in |
|
276 | # If the tmpfile wasn't made because of skipped tests, like in | |
280 | # win32, there's nothing to cleanup. |
|
277 | # win32, there's nothing to cleanup. | |
281 | try: |
|
278 | try: | |
282 | os.unlink(fname) |
|
279 | os.unlink(fname) | |
283 | except: |
|
280 | except: | |
284 | # On Windows, even though we close the file, we still can't |
|
281 | # On Windows, even though we close the file, we still can't | |
285 | # delete it. I have no clue why |
|
282 | # delete it. I have no clue why | |
286 | pass |
|
283 | pass | |
287 |
|
284 | |||
288 | def __enter__(self): |
|
285 | def __enter__(self): | |
289 | return self |
|
286 | return self | |
290 |
|
287 | |||
291 | def __exit__(self, exc_type, exc_value, traceback): |
|
288 | def __exit__(self, exc_type, exc_value, traceback): | |
292 | self.tearDown() |
|
289 | self.tearDown() | |
293 |
|
290 | |||
294 |
|
291 | |||
295 | pair_fail_msg = ("Testing {0}\n\n" |
|
292 | pair_fail_msg = ("Testing {0}\n\n" | |
296 | "In:\n" |
|
293 | "In:\n" | |
297 | " {1!r}\n" |
|
294 | " {1!r}\n" | |
298 | "Expected:\n" |
|
295 | "Expected:\n" | |
299 | " {2!r}\n" |
|
296 | " {2!r}\n" | |
300 | "Got:\n" |
|
297 | "Got:\n" | |
301 | " {3!r}\n") |
|
298 | " {3!r}\n") | |
302 | def check_pairs(func, pairs): |
|
299 | def check_pairs(func, pairs): | |
303 | """Utility function for the common case of checking a function with a |
|
300 | """Utility function for the common case of checking a function with a | |
304 | sequence of input/output pairs. |
|
301 | sequence of input/output pairs. | |
305 |
|
302 | |||
306 | Parameters |
|
303 | Parameters | |
307 | ---------- |
|
304 | ---------- | |
308 | func : callable |
|
305 | func : callable | |
309 | The function to be tested. Should accept a single argument. |
|
306 | The function to be tested. Should accept a single argument. | |
310 | pairs : iterable |
|
307 | pairs : iterable | |
311 | A list of (input, expected_output) tuples. |
|
308 | A list of (input, expected_output) tuples. | |
312 |
|
309 | |||
313 | Returns |
|
310 | Returns | |
314 | ------- |
|
311 | ------- | |
315 | None. Raises an AssertionError if any output does not match the expected |
|
312 | None. Raises an AssertionError if any output does not match the expected | |
316 | value. |
|
313 | value. | |
317 | """ |
|
314 | """ | |
318 | name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>")) |
|
315 | name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>")) | |
319 | for inp, expected in pairs: |
|
316 | for inp, expected in pairs: | |
320 | out = func(inp) |
|
317 | out = func(inp) | |
321 | assert out == expected, pair_fail_msg.format(name, inp, expected, out) |
|
318 | assert out == expected, pair_fail_msg.format(name, inp, expected, out) | |
322 |
|
319 | |||
323 |
|
320 | |||
324 | MyStringIO = StringIO |
|
321 | MyStringIO = StringIO | |
325 |
|
322 | |||
326 | _re_type = type(re.compile(r'')) |
|
323 | _re_type = type(re.compile(r'')) | |
327 |
|
324 | |||
328 | notprinted_msg = """Did not find {0!r} in printed output (on {1}): |
|
325 | notprinted_msg = """Did not find {0!r} in printed output (on {1}): | |
329 | ------- |
|
326 | ------- | |
330 | {2!s} |
|
327 | {2!s} | |
331 | ------- |
|
328 | ------- | |
332 | """ |
|
329 | """ | |
333 |
|
330 | |||
334 | class AssertPrints(object): |
|
331 | class AssertPrints(object): | |
335 | """Context manager for testing that code prints certain text. |
|
332 | """Context manager for testing that code prints certain text. | |
336 |
|
333 | |||
337 | Examples |
|
334 | Examples | |
338 | -------- |
|
335 | -------- | |
339 | >>> with AssertPrints("abc", suppress=False): |
|
336 | >>> with AssertPrints("abc", suppress=False): | |
340 | ... print("abcd") |
|
337 | ... print("abcd") | |
341 | ... print("def") |
|
338 | ... print("def") | |
342 | ... |
|
339 | ... | |
343 | abcd |
|
340 | abcd | |
344 | def |
|
341 | def | |
345 | """ |
|
342 | """ | |
346 | def __init__(self, s, channel='stdout', suppress=True): |
|
343 | def __init__(self, s, channel='stdout', suppress=True): | |
347 | self.s = s |
|
344 | self.s = s | |
348 | if isinstance(self.s, (str, _re_type)): |
|
345 | if isinstance(self.s, (str, _re_type)): | |
349 | self.s = [self.s] |
|
346 | self.s = [self.s] | |
350 | self.channel = channel |
|
347 | self.channel = channel | |
351 | self.suppress = suppress |
|
348 | self.suppress = suppress | |
352 |
|
349 | |||
353 | def __enter__(self): |
|
350 | def __enter__(self): | |
354 | self.orig_stream = getattr(sys, self.channel) |
|
351 | self.orig_stream = getattr(sys, self.channel) | |
355 | self.buffer = MyStringIO() |
|
352 | self.buffer = MyStringIO() | |
356 | self.tee = Tee(self.buffer, channel=self.channel) |
|
353 | self.tee = Tee(self.buffer, channel=self.channel) | |
357 | setattr(sys, self.channel, self.buffer if self.suppress else self.tee) |
|
354 | setattr(sys, self.channel, self.buffer if self.suppress else self.tee) | |
358 |
|
355 | |||
359 | def __exit__(self, etype, value, traceback): |
|
356 | def __exit__(self, etype, value, traceback): | |
360 | try: |
|
357 | try: | |
361 | if value is not None: |
|
358 | if value is not None: | |
362 | # If an error was raised, don't check anything else |
|
359 | # If an error was raised, don't check anything else | |
363 | return False |
|
360 | return False | |
364 | self.tee.flush() |
|
361 | self.tee.flush() | |
365 | setattr(sys, self.channel, self.orig_stream) |
|
362 | setattr(sys, self.channel, self.orig_stream) | |
366 | printed = self.buffer.getvalue() |
|
363 | printed = self.buffer.getvalue() | |
367 | for s in self.s: |
|
364 | for s in self.s: | |
368 | if isinstance(s, _re_type): |
|
365 | if isinstance(s, _re_type): | |
369 | assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed) |
|
366 | assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed) | |
370 | else: |
|
367 | else: | |
371 | assert s in printed, notprinted_msg.format(s, self.channel, printed) |
|
368 | assert s in printed, notprinted_msg.format(s, self.channel, printed) | |
372 | return False |
|
369 | return False | |
373 | finally: |
|
370 | finally: | |
374 | self.tee.close() |
|
371 | self.tee.close() | |
375 |
|
372 | |||
376 | printed_msg = """Found {0!r} in printed output (on {1}): |
|
373 | printed_msg = """Found {0!r} in printed output (on {1}): | |
377 | ------- |
|
374 | ------- | |
378 | {2!s} |
|
375 | {2!s} | |
379 | ------- |
|
376 | ------- | |
380 | """ |
|
377 | """ | |
381 |
|
378 | |||
382 | class AssertNotPrints(AssertPrints): |
|
379 | class AssertNotPrints(AssertPrints): | |
383 | """Context manager for checking that certain output *isn't* produced. |
|
380 | """Context manager for checking that certain output *isn't* produced. | |
384 |
|
381 | |||
385 | Counterpart of AssertPrints""" |
|
382 | Counterpart of AssertPrints""" | |
386 | def __exit__(self, etype, value, traceback): |
|
383 | def __exit__(self, etype, value, traceback): | |
387 | try: |
|
384 | try: | |
388 | if value is not None: |
|
385 | if value is not None: | |
389 | # If an error was raised, don't check anything else |
|
386 | # If an error was raised, don't check anything else | |
390 | self.tee.close() |
|
387 | self.tee.close() | |
391 | return False |
|
388 | return False | |
392 | self.tee.flush() |
|
389 | self.tee.flush() | |
393 | setattr(sys, self.channel, self.orig_stream) |
|
390 | setattr(sys, self.channel, self.orig_stream) | |
394 | printed = self.buffer.getvalue() |
|
391 | printed = self.buffer.getvalue() | |
395 | for s in self.s: |
|
392 | for s in self.s: | |
396 | if isinstance(s, _re_type): |
|
393 | if isinstance(s, _re_type): | |
397 | assert not s.search(printed),printed_msg.format( |
|
394 | assert not s.search(printed),printed_msg.format( | |
398 | s.pattern, self.channel, printed) |
|
395 | s.pattern, self.channel, printed) | |
399 | else: |
|
396 | else: | |
400 | assert s not in printed, printed_msg.format( |
|
397 | assert s not in printed, printed_msg.format( | |
401 | s, self.channel, printed) |
|
398 | s, self.channel, printed) | |
402 | return False |
|
399 | return False | |
403 | finally: |
|
400 | finally: | |
404 | self.tee.close() |
|
401 | self.tee.close() | |
405 |
|
402 | |||
406 | @contextmanager |
|
403 | @contextmanager | |
407 | def mute_warn(): |
|
404 | def mute_warn(): | |
408 | from IPython.utils import warn |
|
405 | from IPython.utils import warn | |
409 | save_warn = warn.warn |
|
406 | save_warn = warn.warn | |
410 | warn.warn = lambda *a, **kw: None |
|
407 | warn.warn = lambda *a, **kw: None | |
411 | try: |
|
408 | try: | |
412 | yield |
|
409 | yield | |
413 | finally: |
|
410 | finally: | |
414 | warn.warn = save_warn |
|
411 | warn.warn = save_warn | |
415 |
|
412 | |||
416 | @contextmanager |
|
413 | @contextmanager | |
417 | def make_tempfile(name): |
|
414 | def make_tempfile(name): | |
418 | """ Create an empty, named, temporary file for the duration of the context. |
|
415 | """ Create an empty, named, temporary file for the duration of the context. | |
419 | """ |
|
416 | """ | |
420 | open(name, 'w').close() |
|
417 | open(name, 'w').close() | |
421 | try: |
|
418 | try: | |
422 | yield |
|
419 | yield | |
423 | finally: |
|
420 | finally: | |
424 | os.unlink(name) |
|
421 | os.unlink(name) | |
425 |
|
422 | |||
426 | def fake_input(inputs): |
|
423 | def fake_input(inputs): | |
427 | """Temporarily replace the input() function to return the given values |
|
424 | """Temporarily replace the input() function to return the given values | |
428 |
|
425 | |||
429 | Use as a context manager: |
|
426 | Use as a context manager: | |
430 |
|
427 | |||
431 | with fake_input(['result1', 'result2']): |
|
428 | with fake_input(['result1', 'result2']): | |
432 | ... |
|
429 | ... | |
433 |
|
430 | |||
434 | Values are returned in order. If input() is called again after the last value |
|
431 | Values are returned in order. If input() is called again after the last value | |
435 | was used, EOFError is raised. |
|
432 | was used, EOFError is raised. | |
436 | """ |
|
433 | """ | |
437 | it = iter(inputs) |
|
434 | it = iter(inputs) | |
438 | def mock_input(prompt=''): |
|
435 | def mock_input(prompt=''): | |
439 | try: |
|
436 | try: | |
440 | return next(it) |
|
437 | return next(it) | |
441 | except StopIteration as e: |
|
438 | except StopIteration as e: | |
442 | raise EOFError('No more inputs given') from e |
|
439 | raise EOFError('No more inputs given') from e | |
443 |
|
440 | |||
444 | return patch('builtins.input', mock_input) |
|
441 | return patch('builtins.input', mock_input) | |
445 |
|
442 | |||
446 | def help_output_test(subcommand=''): |
|
443 | def help_output_test(subcommand=''): | |
447 | """test that `ipython [subcommand] -h` works""" |
|
444 | """test that `ipython [subcommand] -h` works""" | |
448 | cmd = get_ipython_cmd() + [subcommand, '-h'] |
|
445 | cmd = get_ipython_cmd() + [subcommand, '-h'] | |
449 | out, err, rc = get_output_error_code(cmd) |
|
446 | out, err, rc = get_output_error_code(cmd) | |
450 | assert rc == 0, err |
|
447 | assert rc == 0, err | |
451 | assert "Traceback" not in err |
|
448 | assert "Traceback" not in err | |
452 | assert "Options" in out |
|
449 | assert "Options" in out | |
453 | assert "--help-all" in out |
|
450 | assert "--help-all" in out | |
454 | return out, err |
|
451 | return out, err | |
455 |
|
452 | |||
456 |
|
453 | |||
457 | def help_all_output_test(subcommand=''): |
|
454 | def help_all_output_test(subcommand=''): | |
458 | """test that `ipython [subcommand] --help-all` works""" |
|
455 | """test that `ipython [subcommand] --help-all` works""" | |
459 | cmd = get_ipython_cmd() + [subcommand, '--help-all'] |
|
456 | cmd = get_ipython_cmd() + [subcommand, '--help-all'] | |
460 | out, err, rc = get_output_error_code(cmd) |
|
457 | out, err, rc = get_output_error_code(cmd) | |
461 | assert rc == 0, err |
|
458 | assert rc == 0, err | |
462 | assert "Traceback" not in err |
|
459 | assert "Traceback" not in err | |
463 | assert "Options" in out |
|
460 | assert "Options" in out | |
464 | assert "Class" in out |
|
461 | assert "Class" in out | |
465 | return out, err |
|
462 | return out, err | |
466 |
|
463 |
@@ -1,502 +1,504 b'' | |||||
1 | # encoding: utf-8 |
|
1 | # encoding: utf-8 | |
2 | """Tests for IPython.utils.path.py""" |
|
2 | """Tests for IPython.utils.path.py""" | |
3 |
|
3 | |||
4 | # Copyright (c) IPython Development Team. |
|
4 | # Copyright (c) IPython Development Team. | |
5 | # Distributed under the terms of the Modified BSD License. |
|
5 | # Distributed under the terms of the Modified BSD License. | |
6 |
|
6 | |||
7 | import os |
|
7 | import os | |
8 | import shutil |
|
8 | import shutil | |
9 | import sys |
|
9 | import sys | |
10 | import tempfile |
|
10 | import tempfile | |
11 | import unittest |
|
11 | import unittest | |
12 | from contextlib import contextmanager |
|
12 | from contextlib import contextmanager | |
13 | from unittest.mock import patch |
|
13 | from unittest.mock import patch | |
14 | from os.path import join, abspath |
|
14 | from os.path import join, abspath | |
15 | from importlib import reload |
|
15 | from importlib import reload | |
16 |
|
16 | |||
17 | import pytest |
|
17 | import pytest | |
18 |
|
18 | |||
19 | import IPython |
|
19 | import IPython | |
20 | from IPython import paths |
|
20 | from IPython import paths | |
21 | from IPython.testing import decorators as dec |
|
21 | from IPython.testing import decorators as dec | |
22 |
from IPython.testing.decorators import ( |
|
22 | from IPython.testing.decorators import ( | |
23 | onlyif_unicode_paths, |
|
23 | skip_if_not_win32, | |
24 | skip_win32_py38,) |
|
24 | skip_win32, | |
|
25 | onlyif_unicode_paths, | |||
|
26 | ) | |||
25 | from IPython.testing.tools import make_tempfile |
|
27 | from IPython.testing.tools import make_tempfile | |
26 | from IPython.utils import path |
|
28 | from IPython.utils import path | |
27 | from IPython.utils.tempdir import TemporaryDirectory |
|
29 | from IPython.utils.tempdir import TemporaryDirectory | |
28 |
|
30 | |||
29 |
|
31 | |||
30 | # Platform-dependent imports |
|
32 | # Platform-dependent imports | |
31 | try: |
|
33 | try: | |
32 | import winreg as wreg |
|
34 | import winreg as wreg | |
33 | except ImportError: |
|
35 | except ImportError: | |
34 | #Fake _winreg module on non-windows platforms |
|
36 | #Fake _winreg module on non-windows platforms | |
35 | import types |
|
37 | import types | |
36 | wr_name = "winreg" |
|
38 | wr_name = "winreg" | |
37 | sys.modules[wr_name] = types.ModuleType(wr_name) |
|
39 | sys.modules[wr_name] = types.ModuleType(wr_name) | |
38 | try: |
|
40 | try: | |
39 | import winreg as wreg |
|
41 | import winreg as wreg | |
40 | except ImportError: |
|
42 | except ImportError: | |
41 | import _winreg as wreg |
|
43 | import _winreg as wreg | |
42 | #Add entries that needs to be stubbed by the testing code |
|
44 | #Add entries that needs to be stubbed by the testing code | |
43 | (wreg.OpenKey, wreg.QueryValueEx,) = (None, None) |
|
45 | (wreg.OpenKey, wreg.QueryValueEx,) = (None, None) | |
44 |
|
46 | |||
45 | #----------------------------------------------------------------------------- |
|
47 | #----------------------------------------------------------------------------- | |
46 | # Globals |
|
48 | # Globals | |
47 | #----------------------------------------------------------------------------- |
|
49 | #----------------------------------------------------------------------------- | |
48 | env = os.environ |
|
50 | env = os.environ | |
49 | TMP_TEST_DIR = tempfile.mkdtemp() |
|
51 | TMP_TEST_DIR = tempfile.mkdtemp() | |
50 | HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir") |
|
52 | HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir") | |
51 | # |
|
53 | # | |
52 | # Setup/teardown functions/decorators |
|
54 | # Setup/teardown functions/decorators | |
53 | # |
|
55 | # | |
54 |
|
56 | |||
55 | def setup_module(): |
|
57 | def setup_module(): | |
56 | """Setup testenvironment for the module: |
|
58 | """Setup testenvironment for the module: | |
57 |
|
59 | |||
58 | - Adds dummy home dir tree |
|
60 | - Adds dummy home dir tree | |
59 | """ |
|
61 | """ | |
60 | # Do not mask exceptions here. In particular, catching WindowsError is a |
|
62 | # Do not mask exceptions here. In particular, catching WindowsError is a | |
61 | # problem because that exception is only defined on Windows... |
|
63 | # problem because that exception is only defined on Windows... | |
62 | os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython')) |
|
64 | os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython')) | |
63 |
|
65 | |||
64 |
|
66 | |||
65 | def teardown_module(): |
|
67 | def teardown_module(): | |
66 | """Teardown testenvironment for the module: |
|
68 | """Teardown testenvironment for the module: | |
67 |
|
69 | |||
68 | - Remove dummy home dir tree |
|
70 | - Remove dummy home dir tree | |
69 | """ |
|
71 | """ | |
70 | # Note: we remove the parent test dir, which is the root of all test |
|
72 | # Note: we remove the parent test dir, which is the root of all test | |
71 | # subdirs we may have created. Use shutil instead of os.removedirs, so |
|
73 | # subdirs we may have created. Use shutil instead of os.removedirs, so | |
72 | # that non-empty directories are all recursively removed. |
|
74 | # that non-empty directories are all recursively removed. | |
73 | shutil.rmtree(TMP_TEST_DIR) |
|
75 | shutil.rmtree(TMP_TEST_DIR) | |
74 |
|
76 | |||
75 |
|
77 | |||
76 | def setup_environment(): |
|
78 | def setup_environment(): | |
77 | """Setup testenvironment for some functions that are tested |
|
79 | """Setup testenvironment for some functions that are tested | |
78 | in this module. In particular this functions stores attributes |
|
80 | in this module. In particular this functions stores attributes | |
79 | and other things that we need to stub in some test functions. |
|
81 | and other things that we need to stub in some test functions. | |
80 | This needs to be done on a function level and not module level because |
|
82 | This needs to be done on a function level and not module level because | |
81 | each testfunction needs a pristine environment. |
|
83 | each testfunction needs a pristine environment. | |
82 | """ |
|
84 | """ | |
83 | global oldstuff, platformstuff |
|
85 | global oldstuff, platformstuff | |
84 | oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, IPython.__file__, os.getcwd()) |
|
86 | oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, IPython.__file__, os.getcwd()) | |
85 |
|
87 | |||
86 | def teardown_environment(): |
|
88 | def teardown_environment(): | |
87 | """Restore things that were remembered by the setup_environment function |
|
89 | """Restore things that were remembered by the setup_environment function | |
88 | """ |
|
90 | """ | |
89 | (oldenv, os.name, sys.platform, path.get_home_dir, IPython.__file__, old_wd) = oldstuff |
|
91 | (oldenv, os.name, sys.platform, path.get_home_dir, IPython.__file__, old_wd) = oldstuff | |
90 | os.chdir(old_wd) |
|
92 | os.chdir(old_wd) | |
91 | reload(path) |
|
93 | reload(path) | |
92 |
|
94 | |||
93 | for key in list(env): |
|
95 | for key in list(env): | |
94 | if key not in oldenv: |
|
96 | if key not in oldenv: | |
95 | del env[key] |
|
97 | del env[key] | |
96 | env.update(oldenv) |
|
98 | env.update(oldenv) | |
97 | if hasattr(sys, 'frozen'): |
|
99 | if hasattr(sys, 'frozen'): | |
98 | del sys.frozen |
|
100 | del sys.frozen | |
99 |
|
101 | |||
100 |
|
102 | |||
101 | # Build decorator that uses the setup_environment/setup_environment |
|
103 | # Build decorator that uses the setup_environment/setup_environment | |
102 | @pytest.fixture |
|
104 | @pytest.fixture | |
103 | def environment(): |
|
105 | def environment(): | |
104 | setup_environment() |
|
106 | setup_environment() | |
105 | yield |
|
107 | yield | |
106 | teardown_environment() |
|
108 | teardown_environment() | |
107 |
|
109 | |||
108 |
|
110 | |||
109 | with_environment = pytest.mark.usefixtures("environment") |
|
111 | with_environment = pytest.mark.usefixtures("environment") | |
110 |
|
112 | |||
111 |
|
113 | |||
112 | @skip_if_not_win32 |
|
114 | @skip_if_not_win32 | |
113 | @with_environment |
|
115 | @with_environment | |
114 | def test_get_home_dir_1(): |
|
116 | def test_get_home_dir_1(): | |
115 | """Testcase for py2exe logic, un-compressed lib |
|
117 | """Testcase for py2exe logic, un-compressed lib | |
116 | """ |
|
118 | """ | |
117 | unfrozen = path.get_home_dir() |
|
119 | unfrozen = path.get_home_dir() | |
118 | sys.frozen = True |
|
120 | sys.frozen = True | |
119 |
|
121 | |||
120 | #fake filename for IPython.__init__ |
|
122 | #fake filename for IPython.__init__ | |
121 | IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py")) |
|
123 | IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py")) | |
122 |
|
124 | |||
123 | home_dir = path.get_home_dir() |
|
125 | home_dir = path.get_home_dir() | |
124 | assert home_dir == unfrozen |
|
126 | assert home_dir == unfrozen | |
125 |
|
127 | |||
126 |
|
128 | |||
127 | @skip_if_not_win32 |
|
129 | @skip_if_not_win32 | |
128 | @with_environment |
|
130 | @with_environment | |
129 | def test_get_home_dir_2(): |
|
131 | def test_get_home_dir_2(): | |
130 | """Testcase for py2exe logic, compressed lib |
|
132 | """Testcase for py2exe logic, compressed lib | |
131 | """ |
|
133 | """ | |
132 | unfrozen = path.get_home_dir() |
|
134 | unfrozen = path.get_home_dir() | |
133 | sys.frozen = True |
|
135 | sys.frozen = True | |
134 | #fake filename for IPython.__init__ |
|
136 | #fake filename for IPython.__init__ | |
135 | IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower() |
|
137 | IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower() | |
136 |
|
138 | |||
137 | home_dir = path.get_home_dir(True) |
|
139 | home_dir = path.get_home_dir(True) | |
138 | assert home_dir == unfrozen |
|
140 | assert home_dir == unfrozen | |
139 |
|
141 | |||
140 |
|
142 | |||
141 |
@skip_win32 |
|
143 | @skip_win32 | |
142 | @with_environment |
|
144 | @with_environment | |
143 | def test_get_home_dir_3(): |
|
145 | def test_get_home_dir_3(): | |
144 | """get_home_dir() uses $HOME if set""" |
|
146 | """get_home_dir() uses $HOME if set""" | |
145 | env["HOME"] = HOME_TEST_DIR |
|
147 | env["HOME"] = HOME_TEST_DIR | |
146 | home_dir = path.get_home_dir(True) |
|
148 | home_dir = path.get_home_dir(True) | |
147 | # get_home_dir expands symlinks |
|
149 | # get_home_dir expands symlinks | |
148 | assert home_dir == os.path.realpath(env["HOME"]) |
|
150 | assert home_dir == os.path.realpath(env["HOME"]) | |
149 |
|
151 | |||
150 |
|
152 | |||
151 | @with_environment |
|
153 | @with_environment | |
152 | def test_get_home_dir_4(): |
|
154 | def test_get_home_dir_4(): | |
153 | """get_home_dir() still works if $HOME is not set""" |
|
155 | """get_home_dir() still works if $HOME is not set""" | |
154 |
|
156 | |||
155 | if 'HOME' in env: del env['HOME'] |
|
157 | if 'HOME' in env: del env['HOME'] | |
156 | # this should still succeed, but we don't care what the answer is |
|
158 | # this should still succeed, but we don't care what the answer is | |
157 | home = path.get_home_dir(False) |
|
159 | home = path.get_home_dir(False) | |
158 |
|
160 | |||
159 |
@skip_win32 |
|
161 | @skip_win32 | |
160 | @with_environment |
|
162 | @with_environment | |
161 | def test_get_home_dir_5(): |
|
163 | def test_get_home_dir_5(): | |
162 | """raise HomeDirError if $HOME is specified, but not a writable dir""" |
|
164 | """raise HomeDirError if $HOME is specified, but not a writable dir""" | |
163 | env['HOME'] = abspath(HOME_TEST_DIR+'garbage') |
|
165 | env['HOME'] = abspath(HOME_TEST_DIR+'garbage') | |
164 | # set os.name = posix, to prevent My Documents fallback on Windows |
|
166 | # set os.name = posix, to prevent My Documents fallback on Windows | |
165 | os.name = 'posix' |
|
167 | os.name = 'posix' | |
166 | pytest.raises(path.HomeDirError, path.get_home_dir, True) |
|
168 | pytest.raises(path.HomeDirError, path.get_home_dir, True) | |
167 |
|
169 | |||
168 | # Should we stub wreg fully so we can run the test on all platforms? |
|
170 | # Should we stub wreg fully so we can run the test on all platforms? | |
169 | @skip_if_not_win32 |
|
171 | @skip_if_not_win32 | |
170 | @with_environment |
|
172 | @with_environment | |
171 | def test_get_home_dir_8(): |
|
173 | def test_get_home_dir_8(): | |
172 | """Using registry hack for 'My Documents', os=='nt' |
|
174 | """Using registry hack for 'My Documents', os=='nt' | |
173 |
|
175 | |||
174 | HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing. |
|
176 | HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing. | |
175 | """ |
|
177 | """ | |
176 | os.name = 'nt' |
|
178 | os.name = 'nt' | |
177 | # Remove from stub environment all keys that may be set |
|
179 | # Remove from stub environment all keys that may be set | |
178 | for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']: |
|
180 | for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']: | |
179 | env.pop(key, None) |
|
181 | env.pop(key, None) | |
180 |
|
182 | |||
181 | class key: |
|
183 | class key: | |
182 | def __enter__(self): |
|
184 | def __enter__(self): | |
183 | pass |
|
185 | pass | |
184 | def Close(self): |
|
186 | def Close(self): | |
185 | pass |
|
187 | pass | |
186 | def __exit__(*args, **kwargs): |
|
188 | def __exit__(*args, **kwargs): | |
187 | pass |
|
189 | pass | |
188 |
|
190 | |||
189 | with patch.object(wreg, 'OpenKey', return_value=key()), \ |
|
191 | with patch.object(wreg, 'OpenKey', return_value=key()), \ | |
190 | patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]): |
|
192 | patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]): | |
191 | home_dir = path.get_home_dir() |
|
193 | home_dir = path.get_home_dir() | |
192 | assert home_dir == abspath(HOME_TEST_DIR) |
|
194 | assert home_dir == abspath(HOME_TEST_DIR) | |
193 |
|
195 | |||
194 | @with_environment |
|
196 | @with_environment | |
195 | def test_get_xdg_dir_0(): |
|
197 | def test_get_xdg_dir_0(): | |
196 | """test_get_xdg_dir_0, check xdg_dir""" |
|
198 | """test_get_xdg_dir_0, check xdg_dir""" | |
197 | reload(path) |
|
199 | reload(path) | |
198 | path._writable_dir = lambda path: True |
|
200 | path._writable_dir = lambda path: True | |
199 | path.get_home_dir = lambda : 'somewhere' |
|
201 | path.get_home_dir = lambda : 'somewhere' | |
200 | os.name = "posix" |
|
202 | os.name = "posix" | |
201 | sys.platform = "linux2" |
|
203 | sys.platform = "linux2" | |
202 | env.pop('IPYTHON_DIR', None) |
|
204 | env.pop('IPYTHON_DIR', None) | |
203 | env.pop('IPYTHONDIR', None) |
|
205 | env.pop('IPYTHONDIR', None) | |
204 | env.pop('XDG_CONFIG_HOME', None) |
|
206 | env.pop('XDG_CONFIG_HOME', None) | |
205 |
|
207 | |||
206 | assert path.get_xdg_dir() == os.path.join("somewhere", ".config") |
|
208 | assert path.get_xdg_dir() == os.path.join("somewhere", ".config") | |
207 |
|
209 | |||
208 |
|
210 | |||
209 | @with_environment |
|
211 | @with_environment | |
210 | def test_get_xdg_dir_1(): |
|
212 | def test_get_xdg_dir_1(): | |
211 | """test_get_xdg_dir_1, check nonexistent xdg_dir""" |
|
213 | """test_get_xdg_dir_1, check nonexistent xdg_dir""" | |
212 | reload(path) |
|
214 | reload(path) | |
213 | path.get_home_dir = lambda : HOME_TEST_DIR |
|
215 | path.get_home_dir = lambda : HOME_TEST_DIR | |
214 | os.name = "posix" |
|
216 | os.name = "posix" | |
215 | sys.platform = "linux2" |
|
217 | sys.platform = "linux2" | |
216 | env.pop('IPYTHON_DIR', None) |
|
218 | env.pop('IPYTHON_DIR', None) | |
217 | env.pop('IPYTHONDIR', None) |
|
219 | env.pop('IPYTHONDIR', None) | |
218 | env.pop('XDG_CONFIG_HOME', None) |
|
220 | env.pop('XDG_CONFIG_HOME', None) | |
219 | assert path.get_xdg_dir() is None |
|
221 | assert path.get_xdg_dir() is None | |
220 |
|
222 | |||
221 | @with_environment |
|
223 | @with_environment | |
222 | def test_get_xdg_dir_2(): |
|
224 | def test_get_xdg_dir_2(): | |
223 | """test_get_xdg_dir_2, check xdg_dir default to ~/.config""" |
|
225 | """test_get_xdg_dir_2, check xdg_dir default to ~/.config""" | |
224 | reload(path) |
|
226 | reload(path) | |
225 | path.get_home_dir = lambda : HOME_TEST_DIR |
|
227 | path.get_home_dir = lambda : HOME_TEST_DIR | |
226 | os.name = "posix" |
|
228 | os.name = "posix" | |
227 | sys.platform = "linux2" |
|
229 | sys.platform = "linux2" | |
228 | env.pop('IPYTHON_DIR', None) |
|
230 | env.pop('IPYTHON_DIR', None) | |
229 | env.pop('IPYTHONDIR', None) |
|
231 | env.pop('IPYTHONDIR', None) | |
230 | env.pop('XDG_CONFIG_HOME', None) |
|
232 | env.pop('XDG_CONFIG_HOME', None) | |
231 | cfgdir=os.path.join(path.get_home_dir(), '.config') |
|
233 | cfgdir=os.path.join(path.get_home_dir(), '.config') | |
232 | if not os.path.exists(cfgdir): |
|
234 | if not os.path.exists(cfgdir): | |
233 | os.makedirs(cfgdir) |
|
235 | os.makedirs(cfgdir) | |
234 |
|
236 | |||
235 | assert path.get_xdg_dir() == cfgdir |
|
237 | assert path.get_xdg_dir() == cfgdir | |
236 |
|
238 | |||
237 | @with_environment |
|
239 | @with_environment | |
238 | def test_get_xdg_dir_3(): |
|
240 | def test_get_xdg_dir_3(): | |
239 | """test_get_xdg_dir_3, check xdg_dir not used on OS X""" |
|
241 | """test_get_xdg_dir_3, check xdg_dir not used on OS X""" | |
240 | reload(path) |
|
242 | reload(path) | |
241 | path.get_home_dir = lambda : HOME_TEST_DIR |
|
243 | path.get_home_dir = lambda : HOME_TEST_DIR | |
242 | os.name = "posix" |
|
244 | os.name = "posix" | |
243 | sys.platform = "darwin" |
|
245 | sys.platform = "darwin" | |
244 | env.pop('IPYTHON_DIR', None) |
|
246 | env.pop('IPYTHON_DIR', None) | |
245 | env.pop('IPYTHONDIR', None) |
|
247 | env.pop('IPYTHONDIR', None) | |
246 | env.pop('XDG_CONFIG_HOME', None) |
|
248 | env.pop('XDG_CONFIG_HOME', None) | |
247 | cfgdir=os.path.join(path.get_home_dir(), '.config') |
|
249 | cfgdir=os.path.join(path.get_home_dir(), '.config') | |
248 | os.makedirs(cfgdir, exist_ok=True) |
|
250 | os.makedirs(cfgdir, exist_ok=True) | |
249 |
|
251 | |||
250 | assert path.get_xdg_dir() is None |
|
252 | assert path.get_xdg_dir() is None | |
251 |
|
253 | |||
252 | def test_filefind(): |
|
254 | def test_filefind(): | |
253 | """Various tests for filefind""" |
|
255 | """Various tests for filefind""" | |
254 | f = tempfile.NamedTemporaryFile() |
|
256 | f = tempfile.NamedTemporaryFile() | |
255 | # print 'fname:',f.name |
|
257 | # print 'fname:',f.name | |
256 | alt_dirs = paths.get_ipython_dir() |
|
258 | alt_dirs = paths.get_ipython_dir() | |
257 | t = path.filefind(f.name, alt_dirs) |
|
259 | t = path.filefind(f.name, alt_dirs) | |
258 | # print 'found:',t |
|
260 | # print 'found:',t | |
259 |
|
261 | |||
260 |
|
262 | |||
261 | @dec.skip_if_not_win32 |
|
263 | @dec.skip_if_not_win32 | |
262 | def test_get_long_path_name_win32(): |
|
264 | def test_get_long_path_name_win32(): | |
263 | with TemporaryDirectory() as tmpdir: |
|
265 | with TemporaryDirectory() as tmpdir: | |
264 |
|
266 | |||
265 | # Make a long path. Expands the path of tmpdir prematurely as it may already have a long |
|
267 | # Make a long path. Expands the path of tmpdir prematurely as it may already have a long | |
266 | # path component, so ensure we include the long form of it |
|
268 | # path component, so ensure we include the long form of it | |
267 | long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name') |
|
269 | long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name') | |
268 | os.makedirs(long_path) |
|
270 | os.makedirs(long_path) | |
269 |
|
271 | |||
270 | # Test to see if the short path evaluates correctly. |
|
272 | # Test to see if the short path evaluates correctly. | |
271 | short_path = os.path.join(tmpdir, 'THISIS~1') |
|
273 | short_path = os.path.join(tmpdir, 'THISIS~1') | |
272 | evaluated_path = path.get_long_path_name(short_path) |
|
274 | evaluated_path = path.get_long_path_name(short_path) | |
273 | assert evaluated_path.lower() == long_path.lower() |
|
275 | assert evaluated_path.lower() == long_path.lower() | |
274 |
|
276 | |||
275 |
|
277 | |||
276 | @dec.skip_win32 |
|
278 | @dec.skip_win32 | |
277 | def test_get_long_path_name(): |
|
279 | def test_get_long_path_name(): | |
278 | p = path.get_long_path_name("/usr/local") |
|
280 | p = path.get_long_path_name("/usr/local") | |
279 | assert p == "/usr/local" |
|
281 | assert p == "/usr/local" | |
280 |
|
282 | |||
281 |
|
283 | |||
282 | class TestRaiseDeprecation(unittest.TestCase): |
|
284 | class TestRaiseDeprecation(unittest.TestCase): | |
283 |
|
285 | |||
284 | @dec.skip_win32 # can't create not-user-writable dir on win |
|
286 | @dec.skip_win32 # can't create not-user-writable dir on win | |
285 | @with_environment |
|
287 | @with_environment | |
286 | def test_not_writable_ipdir(self): |
|
288 | def test_not_writable_ipdir(self): | |
287 | tmpdir = tempfile.mkdtemp() |
|
289 | tmpdir = tempfile.mkdtemp() | |
288 | os.name = "posix" |
|
290 | os.name = "posix" | |
289 | env.pop('IPYTHON_DIR', None) |
|
291 | env.pop('IPYTHON_DIR', None) | |
290 | env.pop('IPYTHONDIR', None) |
|
292 | env.pop('IPYTHONDIR', None) | |
291 | env.pop('XDG_CONFIG_HOME', None) |
|
293 | env.pop('XDG_CONFIG_HOME', None) | |
292 | env['HOME'] = tmpdir |
|
294 | env['HOME'] = tmpdir | |
293 | ipdir = os.path.join(tmpdir, '.ipython') |
|
295 | ipdir = os.path.join(tmpdir, '.ipython') | |
294 | os.mkdir(ipdir, 0o555) |
|
296 | os.mkdir(ipdir, 0o555) | |
295 | try: |
|
297 | try: | |
296 | open(os.path.join(ipdir, "_foo_"), 'w').close() |
|
298 | open(os.path.join(ipdir, "_foo_"), 'w').close() | |
297 | except IOError: |
|
299 | except IOError: | |
298 | pass |
|
300 | pass | |
299 | else: |
|
301 | else: | |
300 | # I can still write to an unwritable dir, |
|
302 | # I can still write to an unwritable dir, | |
301 | # assume I'm root and skip the test |
|
303 | # assume I'm root and skip the test | |
302 | pytest.skip("I can't create directories that I can't write to") |
|
304 | pytest.skip("I can't create directories that I can't write to") | |
303 |
|
305 | |||
304 | with self.assertWarnsRegex(UserWarning, 'is not a writable location'): |
|
306 | with self.assertWarnsRegex(UserWarning, 'is not a writable location'): | |
305 | ipdir = paths.get_ipython_dir() |
|
307 | ipdir = paths.get_ipython_dir() | |
306 | env.pop('IPYTHON_DIR', None) |
|
308 | env.pop('IPYTHON_DIR', None) | |
307 |
|
309 | |||
308 | @with_environment |
|
310 | @with_environment | |
309 | def test_get_py_filename(): |
|
311 | def test_get_py_filename(): | |
310 | os.chdir(TMP_TEST_DIR) |
|
312 | os.chdir(TMP_TEST_DIR) | |
311 | with make_tempfile("foo.py"): |
|
313 | with make_tempfile("foo.py"): | |
312 | assert path.get_py_filename("foo.py") == "foo.py" |
|
314 | assert path.get_py_filename("foo.py") == "foo.py" | |
313 | assert path.get_py_filename("foo") == "foo.py" |
|
315 | assert path.get_py_filename("foo") == "foo.py" | |
314 | with make_tempfile("foo"): |
|
316 | with make_tempfile("foo"): | |
315 | assert path.get_py_filename("foo") == "foo" |
|
317 | assert path.get_py_filename("foo") == "foo" | |
316 | pytest.raises(IOError, path.get_py_filename, "foo.py") |
|
318 | pytest.raises(IOError, path.get_py_filename, "foo.py") | |
317 | pytest.raises(IOError, path.get_py_filename, "foo") |
|
319 | pytest.raises(IOError, path.get_py_filename, "foo") | |
318 | pytest.raises(IOError, path.get_py_filename, "foo.py") |
|
320 | pytest.raises(IOError, path.get_py_filename, "foo.py") | |
319 | true_fn = "foo with spaces.py" |
|
321 | true_fn = "foo with spaces.py" | |
320 | with make_tempfile(true_fn): |
|
322 | with make_tempfile(true_fn): | |
321 | assert path.get_py_filename("foo with spaces") == true_fn |
|
323 | assert path.get_py_filename("foo with spaces") == true_fn | |
322 | assert path.get_py_filename("foo with spaces.py") == true_fn |
|
324 | assert path.get_py_filename("foo with spaces.py") == true_fn | |
323 | pytest.raises(IOError, path.get_py_filename, '"foo with spaces.py"') |
|
325 | pytest.raises(IOError, path.get_py_filename, '"foo with spaces.py"') | |
324 | pytest.raises(IOError, path.get_py_filename, "'foo with spaces.py'") |
|
326 | pytest.raises(IOError, path.get_py_filename, "'foo with spaces.py'") | |
325 |
|
327 | |||
326 | @onlyif_unicode_paths |
|
328 | @onlyif_unicode_paths | |
327 | def test_unicode_in_filename(): |
|
329 | def test_unicode_in_filename(): | |
328 | """When a file doesn't exist, the exception raised should be safe to call |
|
330 | """When a file doesn't exist, the exception raised should be safe to call | |
329 | str() on - i.e. in Python 2 it must only have ASCII characters. |
|
331 | str() on - i.e. in Python 2 it must only have ASCII characters. | |
330 |
|
332 | |||
331 | https://github.com/ipython/ipython/issues/875 |
|
333 | https://github.com/ipython/ipython/issues/875 | |
332 | """ |
|
334 | """ | |
333 | try: |
|
335 | try: | |
334 | # these calls should not throw unicode encode exceptions |
|
336 | # these calls should not throw unicode encode exceptions | |
335 | path.get_py_filename('fooéè.py') |
|
337 | path.get_py_filename('fooéè.py') | |
336 | except IOError as ex: |
|
338 | except IOError as ex: | |
337 | str(ex) |
|
339 | str(ex) | |
338 |
|
340 | |||
339 |
|
341 | |||
340 | class TestShellGlob(unittest.TestCase): |
|
342 | class TestShellGlob(unittest.TestCase): | |
341 |
|
343 | |||
342 | @classmethod |
|
344 | @classmethod | |
343 | def setUpClass(cls): |
|
345 | def setUpClass(cls): | |
344 | cls.filenames_start_with_a = ['a0', 'a1', 'a2'] |
|
346 | cls.filenames_start_with_a = ['a0', 'a1', 'a2'] | |
345 | cls.filenames_end_with_b = ['0b', '1b', '2b'] |
|
347 | cls.filenames_end_with_b = ['0b', '1b', '2b'] | |
346 | cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b |
|
348 | cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b | |
347 | cls.tempdir = TemporaryDirectory() |
|
349 | cls.tempdir = TemporaryDirectory() | |
348 | td = cls.tempdir.name |
|
350 | td = cls.tempdir.name | |
349 |
|
351 | |||
350 | with cls.in_tempdir(): |
|
352 | with cls.in_tempdir(): | |
351 | # Create empty files |
|
353 | # Create empty files | |
352 | for fname in cls.filenames: |
|
354 | for fname in cls.filenames: | |
353 | open(os.path.join(td, fname), 'w').close() |
|
355 | open(os.path.join(td, fname), 'w').close() | |
354 |
|
356 | |||
355 | @classmethod |
|
357 | @classmethod | |
356 | def tearDownClass(cls): |
|
358 | def tearDownClass(cls): | |
357 | cls.tempdir.cleanup() |
|
359 | cls.tempdir.cleanup() | |
358 |
|
360 | |||
359 | @classmethod |
|
361 | @classmethod | |
360 | @contextmanager |
|
362 | @contextmanager | |
361 | def in_tempdir(cls): |
|
363 | def in_tempdir(cls): | |
362 | save = os.getcwd() |
|
364 | save = os.getcwd() | |
363 | try: |
|
365 | try: | |
364 | os.chdir(cls.tempdir.name) |
|
366 | os.chdir(cls.tempdir.name) | |
365 | yield |
|
367 | yield | |
366 | finally: |
|
368 | finally: | |
367 | os.chdir(save) |
|
369 | os.chdir(save) | |
368 |
|
370 | |||
369 | def check_match(self, patterns, matches): |
|
371 | def check_match(self, patterns, matches): | |
370 | with self.in_tempdir(): |
|
372 | with self.in_tempdir(): | |
371 | # glob returns unordered list. that's why sorted is required. |
|
373 | # glob returns unordered list. that's why sorted is required. | |
372 | assert sorted(path.shellglob(patterns)) == sorted(matches) |
|
374 | assert sorted(path.shellglob(patterns)) == sorted(matches) | |
373 |
|
375 | |||
374 | def common_cases(self): |
|
376 | def common_cases(self): | |
375 | return [ |
|
377 | return [ | |
376 | (['*'], self.filenames), |
|
378 | (['*'], self.filenames), | |
377 | (['a*'], self.filenames_start_with_a), |
|
379 | (['a*'], self.filenames_start_with_a), | |
378 | (['*c'], ['*c']), |
|
380 | (['*c'], ['*c']), | |
379 | (['*', 'a*', '*b', '*c'], self.filenames |
|
381 | (['*', 'a*', '*b', '*c'], self.filenames | |
380 | + self.filenames_start_with_a |
|
382 | + self.filenames_start_with_a | |
381 | + self.filenames_end_with_b |
|
383 | + self.filenames_end_with_b | |
382 | + ['*c']), |
|
384 | + ['*c']), | |
383 | (['a[012]'], self.filenames_start_with_a), |
|
385 | (['a[012]'], self.filenames_start_with_a), | |
384 | ] |
|
386 | ] | |
385 |
|
387 | |||
386 | @skip_win32 |
|
388 | @skip_win32 | |
387 | def test_match_posix(self): |
|
389 | def test_match_posix(self): | |
388 | for (patterns, matches) in self.common_cases() + [ |
|
390 | for (patterns, matches) in self.common_cases() + [ | |
389 | ([r'\*'], ['*']), |
|
391 | ([r'\*'], ['*']), | |
390 | ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a), |
|
392 | ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a), | |
391 | ([r'a\[012]'], ['a[012]']), |
|
393 | ([r'a\[012]'], ['a[012]']), | |
392 | ]: |
|
394 | ]: | |
393 | self.check_match(patterns, matches) |
|
395 | self.check_match(patterns, matches) | |
394 |
|
396 | |||
395 | @skip_if_not_win32 |
|
397 | @skip_if_not_win32 | |
396 | def test_match_windows(self): |
|
398 | def test_match_windows(self): | |
397 | for (patterns, matches) in self.common_cases() + [ |
|
399 | for (patterns, matches) in self.common_cases() + [ | |
398 | # In windows, backslash is interpreted as path |
|
400 | # In windows, backslash is interpreted as path | |
399 | # separator. Therefore, you can't escape glob |
|
401 | # separator. Therefore, you can't escape glob | |
400 | # using it. |
|
402 | # using it. | |
401 | ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a), |
|
403 | ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a), | |
402 | ([r'a\[012]'], [r'a\[012]']), |
|
404 | ([r'a\[012]'], [r'a\[012]']), | |
403 | ]: |
|
405 | ]: | |
404 | self.check_match(patterns, matches) |
|
406 | self.check_match(patterns, matches) | |
405 |
|
407 | |||
406 |
|
408 | |||
407 | # TODO : pytest.mark.parametrise once nose is gone. |
|
409 | # TODO : pytest.mark.parametrise once nose is gone. | |
408 | def test_unescape_glob(): |
|
410 | def test_unescape_glob(): | |
409 | assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?" |
|
411 | assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?" | |
410 | assert path.unescape_glob(r"\\*") == r"\*" |
|
412 | assert path.unescape_glob(r"\\*") == r"\*" | |
411 | assert path.unescape_glob(r"\\\*") == r"\*" |
|
413 | assert path.unescape_glob(r"\\\*") == r"\*" | |
412 | assert path.unescape_glob(r"\\a") == r"\a" |
|
414 | assert path.unescape_glob(r"\\a") == r"\a" | |
413 | assert path.unescape_glob(r"\a") == r"\a" |
|
415 | assert path.unescape_glob(r"\a") == r"\a" | |
414 |
|
416 | |||
415 |
|
417 | |||
416 | @onlyif_unicode_paths |
|
418 | @onlyif_unicode_paths | |
417 | def test_ensure_dir_exists(): |
|
419 | def test_ensure_dir_exists(): | |
418 | with TemporaryDirectory() as td: |
|
420 | with TemporaryDirectory() as td: | |
419 | d = os.path.join(td, 'βir') |
|
421 | d = os.path.join(td, 'βir') | |
420 | path.ensure_dir_exists(d) # create it |
|
422 | path.ensure_dir_exists(d) # create it | |
421 | assert os.path.isdir(d) |
|
423 | assert os.path.isdir(d) | |
422 | path.ensure_dir_exists(d) # no-op |
|
424 | path.ensure_dir_exists(d) # no-op | |
423 | f = os.path.join(td, 'Ζile') |
|
425 | f = os.path.join(td, 'Ζile') | |
424 | open(f, 'w').close() # touch |
|
426 | open(f, 'w').close() # touch | |
425 | with pytest.raises(IOError): |
|
427 | with pytest.raises(IOError): | |
426 | path.ensure_dir_exists(f) |
|
428 | path.ensure_dir_exists(f) | |
427 |
|
429 | |||
428 | class TestLinkOrCopy(unittest.TestCase): |
|
430 | class TestLinkOrCopy(unittest.TestCase): | |
429 | def setUp(self): |
|
431 | def setUp(self): | |
430 | self.tempdir = TemporaryDirectory() |
|
432 | self.tempdir = TemporaryDirectory() | |
431 | self.src = self.dst("src") |
|
433 | self.src = self.dst("src") | |
432 | with open(self.src, "w") as f: |
|
434 | with open(self.src, "w") as f: | |
433 | f.write("Hello, world!") |
|
435 | f.write("Hello, world!") | |
434 |
|
436 | |||
435 | def tearDown(self): |
|
437 | def tearDown(self): | |
436 | self.tempdir.cleanup() |
|
438 | self.tempdir.cleanup() | |
437 |
|
439 | |||
438 | def dst(self, *args): |
|
440 | def dst(self, *args): | |
439 | return os.path.join(self.tempdir.name, *args) |
|
441 | return os.path.join(self.tempdir.name, *args) | |
440 |
|
442 | |||
441 | def assert_inode_not_equal(self, a, b): |
|
443 | def assert_inode_not_equal(self, a, b): | |
442 | assert ( |
|
444 | assert ( | |
443 | os.stat(a).st_ino != os.stat(b).st_ino |
|
445 | os.stat(a).st_ino != os.stat(b).st_ino | |
444 | ), "%r and %r do reference the same indoes" % (a, b) |
|
446 | ), "%r and %r do reference the same indoes" % (a, b) | |
445 |
|
447 | |||
446 | def assert_inode_equal(self, a, b): |
|
448 | def assert_inode_equal(self, a, b): | |
447 | assert ( |
|
449 | assert ( | |
448 | os.stat(a).st_ino == os.stat(b).st_ino |
|
450 | os.stat(a).st_ino == os.stat(b).st_ino | |
449 | ), "%r and %r do not reference the same indoes" % (a, b) |
|
451 | ), "%r and %r do not reference the same indoes" % (a, b) | |
450 |
|
452 | |||
451 | def assert_content_equal(self, a, b): |
|
453 | def assert_content_equal(self, a, b): | |
452 | with open(a) as a_f: |
|
454 | with open(a) as a_f: | |
453 | with open(b) as b_f: |
|
455 | with open(b) as b_f: | |
454 | assert a_f.read() == b_f.read() |
|
456 | assert a_f.read() == b_f.read() | |
455 |
|
457 | |||
456 | @skip_win32 |
|
458 | @skip_win32 | |
457 | def test_link_successful(self): |
|
459 | def test_link_successful(self): | |
458 | dst = self.dst("target") |
|
460 | dst = self.dst("target") | |
459 | path.link_or_copy(self.src, dst) |
|
461 | path.link_or_copy(self.src, dst) | |
460 | self.assert_inode_equal(self.src, dst) |
|
462 | self.assert_inode_equal(self.src, dst) | |
461 |
|
463 | |||
462 | @skip_win32 |
|
464 | @skip_win32 | |
463 | def test_link_into_dir(self): |
|
465 | def test_link_into_dir(self): | |
464 | dst = self.dst("some_dir") |
|
466 | dst = self.dst("some_dir") | |
465 | os.mkdir(dst) |
|
467 | os.mkdir(dst) | |
466 | path.link_or_copy(self.src, dst) |
|
468 | path.link_or_copy(self.src, dst) | |
467 | expected_dst = self.dst("some_dir", os.path.basename(self.src)) |
|
469 | expected_dst = self.dst("some_dir", os.path.basename(self.src)) | |
468 | self.assert_inode_equal(self.src, expected_dst) |
|
470 | self.assert_inode_equal(self.src, expected_dst) | |
469 |
|
471 | |||
470 | @skip_win32 |
|
472 | @skip_win32 | |
471 | def test_target_exists(self): |
|
473 | def test_target_exists(self): | |
472 | dst = self.dst("target") |
|
474 | dst = self.dst("target") | |
473 | open(dst, "w").close() |
|
475 | open(dst, "w").close() | |
474 | path.link_or_copy(self.src, dst) |
|
476 | path.link_or_copy(self.src, dst) | |
475 | self.assert_inode_equal(self.src, dst) |
|
477 | self.assert_inode_equal(self.src, dst) | |
476 |
|
478 | |||
477 | @skip_win32 |
|
479 | @skip_win32 | |
478 | def test_no_link(self): |
|
480 | def test_no_link(self): | |
479 | real_link = os.link |
|
481 | real_link = os.link | |
480 | try: |
|
482 | try: | |
481 | del os.link |
|
483 | del os.link | |
482 | dst = self.dst("target") |
|
484 | dst = self.dst("target") | |
483 | path.link_or_copy(self.src, dst) |
|
485 | path.link_or_copy(self.src, dst) | |
484 | self.assert_content_equal(self.src, dst) |
|
486 | self.assert_content_equal(self.src, dst) | |
485 | self.assert_inode_not_equal(self.src, dst) |
|
487 | self.assert_inode_not_equal(self.src, dst) | |
486 | finally: |
|
488 | finally: | |
487 | os.link = real_link |
|
489 | os.link = real_link | |
488 |
|
490 | |||
489 | @skip_if_not_win32 |
|
491 | @skip_if_not_win32 | |
490 | def test_windows(self): |
|
492 | def test_windows(self): | |
491 | dst = self.dst("target") |
|
493 | dst = self.dst("target") | |
492 | path.link_or_copy(self.src, dst) |
|
494 | path.link_or_copy(self.src, dst) | |
493 | self.assert_content_equal(self.src, dst) |
|
495 | self.assert_content_equal(self.src, dst) | |
494 |
|
496 | |||
495 | def test_link_twice(self): |
|
497 | def test_link_twice(self): | |
496 | # Linking the same file twice shouldn't leave duplicates around. |
|
498 | # Linking the same file twice shouldn't leave duplicates around. | |
497 | # See https://github.com/ipython/ipython/issues/6450 |
|
499 | # See https://github.com/ipython/ipython/issues/6450 | |
498 | dst = self.dst('target') |
|
500 | dst = self.dst('target') | |
499 | path.link_or_copy(self.src, dst) |
|
501 | path.link_or_copy(self.src, dst) | |
500 | path.link_or_copy(self.src, dst) |
|
502 | path.link_or_copy(self.src, dst) | |
501 | self.assert_inode_equal(self.src, dst) |
|
503 | self.assert_inode_equal(self.src, dst) | |
502 | assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"] |
|
504 | assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"] |
General Comments 0
You need to be logged in to leave comments.
Login now