Coverage for iomenu.py: 12%
311 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1import io
2import os
3import shlex
4import sys
5import tempfile
6import tokenize
8from tkinter import filedialog
9from tkinter import messagebox
10from tkinter.simpledialog import askstring
12from idlelib.config import idleConf
13from idlelib.util import py_extensions
15py_extensions = ' '.join("*"+ext for ext in py_extensions)
16encoding = 'utf-8'
17errors = 'surrogatepass' if sys.platform == 'win32' else 'surrogateescape'
20class IOBinding:
21# One instance per editor Window so methods know which to save, close.
22# Open returns focus to self.editwin if aborted.
23# EditorWindow.open_module, others, belong here.
25 def __init__(self, editwin):
26 self.editwin = editwin
27 self.text = editwin.text
28 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
29 self.__id_save = self.text.bind("<<save-window>>", self.save)
30 self.__id_saveas = self.text.bind("<<save-window-as-file>>",
31 self.save_as)
32 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
33 self.save_a_copy)
34 self.fileencoding = 'utf-8'
35 self.__id_print = self.text.bind("<<print-window>>", self.print_window)
37 def close(self):
38 # Undo command bindings
39 self.text.unbind("<<open-window-from-file>>", self.__id_open)
40 self.text.unbind("<<save-window>>", self.__id_save)
41 self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
42 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
43 self.text.unbind("<<print-window>>", self.__id_print)
44 # Break cycles
45 self.editwin = None
46 self.text = None
47 self.filename_change_hook = None
49 def get_saved(self):
50 return self.editwin.get_saved()
52 def set_saved(self, flag):
53 self.editwin.set_saved(flag)
55 def reset_undo(self):
56 self.editwin.reset_undo()
58 filename_change_hook = None
60 def set_filename_change_hook(self, hook):
61 self.filename_change_hook = hook
63 filename = None
64 dirname = None
66 def set_filename(self, filename):
67 if filename and os.path.isdir(filename):
68 self.filename = None
69 self.dirname = filename
70 else:
71 self.filename = filename
72 self.dirname = None
73 self.set_saved(1)
74 if self.filename_change_hook:
75 self.filename_change_hook()
77 def open(self, event=None, editFile=None):
78 flist = self.editwin.flist
79 # Save in case parent window is closed (ie, during askopenfile()).
80 if flist:
81 if not editFile:
82 filename = self.askopenfile()
83 else:
84 filename=editFile
85 if filename:
86 # If editFile is valid and already open, flist.open will
87 # shift focus to its existing window.
88 # If the current window exists and is a fresh unnamed,
89 # unmodified editor window (not an interpreter shell),
90 # pass self.loadfile to flist.open so it will load the file
91 # in the current window (if the file is not already open)
92 # instead of a new window.
93 if (self.editwin and
94 not getattr(self.editwin, 'interp', None) and
95 not self.filename and
96 self.get_saved()):
97 flist.open(filename, self.loadfile)
98 else:
99 flist.open(filename)
100 else:
101 if self.text:
102 self.text.focus_set()
103 return "break"
105 # Code for use outside IDLE:
106 if self.get_saved():
107 reply = self.maybesave()
108 if reply == "cancel":
109 self.text.focus_set()
110 return "break"
111 if not editFile:
112 filename = self.askopenfile()
113 else:
114 filename=editFile
115 if filename:
116 self.loadfile(filename)
117 else:
118 self.text.focus_set()
119 return "break"
121 eol_convention = os.linesep # default
123 def loadfile(self, filename):
124 try:
125 try:
126 with tokenize.open(filename) as f:
127 chars = f.read()
128 fileencoding = f.encoding
129 eol_convention = f.newlines
130 converted = False
131 except (UnicodeDecodeError, SyntaxError):
132 # Wait for the editor window to appear
133 self.editwin.text.update()
134 enc = askstring(
135 "Specify file encoding",
136 "The file's encoding is invalid for Python 3.x.\n"
137 "IDLE will convert it to UTF-8.\n"
138 "What is the current encoding of the file?",
139 initialvalue='utf-8',
140 parent=self.editwin.text)
141 with open(filename, encoding=enc) as f:
142 chars = f.read()
143 fileencoding = f.encoding
144 eol_convention = f.newlines
145 converted = True
146 except OSError as err:
147 messagebox.showerror("I/O Error", str(err), parent=self.text)
148 return False
149 except UnicodeDecodeError:
150 messagebox.showerror("Decoding Error",
151 "File %s\nFailed to Decode" % filename,
152 parent=self.text)
153 return False
155 if not isinstance(eol_convention, str):
156 # If the file does not contain line separators, it is None.
157 # If the file contains mixed line separators, it is a tuple.
158 if eol_convention is not None:
159 messagebox.showwarning("Mixed Newlines",
160 "Mixed newlines detected.\n"
161 "The file will be changed on save.",
162 parent=self.text)
163 converted = True
164 eol_convention = os.linesep # default
166 self.text.delete("1.0", "end")
167 self.set_filename(None)
168 self.fileencoding = fileencoding
169 self.eol_convention = eol_convention
170 self.text.insert("1.0", chars)
171 self.reset_undo()
172 self.set_filename(filename)
173 if converted:
174 # We need to save the conversion results first
175 # before being able to execute the code
176 self.set_saved(False)
177 self.text.mark_set("insert", "1.0")
178 self.text.yview("insert")
179 self.updaterecentfileslist(filename)
180 return True
182 def maybesave(self):
183 if self.get_saved():
184 return "yes"
185 message = "Do you want to save %s before closing?" % (
186 self.filename or "this untitled document")
187 confirm = messagebox.askyesnocancel(
188 title="Save On Close",
189 message=message,
190 default=messagebox.YES,
191 parent=self.text)
192 if confirm:
193 reply = "yes"
194 self.save(None)
195 if not self.get_saved():
196 reply = "cancel"
197 elif confirm is None:
198 reply = "cancel"
199 else:
200 reply = "no"
201 self.text.focus_set()
202 return reply
204 def save(self, event):
205 if not self.filename:
206 self.save_as(event)
207 else:
208 if self.writefile(self.filename):
209 self.set_saved(True)
210 try:
211 self.editwin.store_file_breaks()
212 except AttributeError: # may be a PyShell
213 pass
214 self.text.focus_set()
215 return "break"
217 def save_as(self, event):
218 filename = self.asksavefile()
219 if filename:
220 if self.writefile(filename):
221 self.set_filename(filename)
222 self.set_saved(1)
223 try:
224 self.editwin.store_file_breaks()
225 except AttributeError:
226 pass
227 self.text.focus_set()
228 self.updaterecentfileslist(filename)
229 return "break"
231 def save_a_copy(self, event):
232 filename = self.asksavefile()
233 if filename:
234 self.writefile(filename)
235 self.text.focus_set()
236 self.updaterecentfileslist(filename)
237 return "break"
239 def writefile(self, filename):
240 text = self.fixnewlines()
241 chars = self.encode(text)
242 try:
243 with open(filename, "wb") as f:
244 f.write(chars)
245 f.flush()
246 os.fsync(f.fileno())
247 return True
248 except OSError as msg:
249 messagebox.showerror("I/O Error", str(msg),
250 parent=self.text)
251 return False
253 def fixnewlines(self):
254 """Return text with os eols.
256 Add prompts if shell else final \n if missing.
257 """
259 if hasattr(self.editwin, "interp"): # Saving shell.
260 text = self.editwin.get_prompt_text('1.0', self.text.index('end-1c'))
261 else:
262 if self.text.get("end-2c") != '\n':
263 self.text.insert("end-1c", "\n") # Changes 'end-1c' value.
264 text = self.text.get('1.0', "end-1c")
265 if self.eol_convention != "\n":
266 text = text.replace("\n", self.eol_convention)
267 return text
269 def encode(self, chars):
270 if isinstance(chars, bytes):
271 # This is either plain ASCII, or Tk was returning mixed-encoding
272 # text to us. Don't try to guess further.
273 return chars
274 # Preserve a BOM that might have been present on opening
275 if self.fileencoding == 'utf-8-sig':
276 return chars.encode('utf-8-sig')
277 # See whether there is anything non-ASCII in it.
278 # If not, no need to figure out the encoding.
279 try:
280 return chars.encode('ascii')
281 except UnicodeEncodeError:
282 pass
283 # Check if there is an encoding declared
284 try:
285 encoded = chars.encode('ascii', 'replace')
286 enc, _ = tokenize.detect_encoding(io.BytesIO(encoded).readline)
287 return chars.encode(enc)
288 except SyntaxError as err:
289 failed = str(err)
290 except UnicodeEncodeError:
291 failed = "Invalid encoding '%s'" % enc
292 messagebox.showerror(
293 "I/O Error",
294 "%s.\nSaving as UTF-8" % failed,
295 parent=self.text)
296 # Fallback: save as UTF-8, with BOM - ignoring the incorrect
297 # declared encoding
298 return chars.encode('utf-8-sig')
300 def print_window(self, event):
301 confirm = messagebox.askokcancel(
302 title="Print",
303 message="Print to Default Printer",
304 default=messagebox.OK,
305 parent=self.text)
306 if not confirm:
307 self.text.focus_set()
308 return "break"
309 tempfilename = None
310 saved = self.get_saved()
311 if saved:
312 filename = self.filename
313 # shell undo is reset after every prompt, looks saved, probably isn't
314 if not saved or filename is None:
315 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
316 filename = tempfilename
317 os.close(tfd)
318 if not self.writefile(tempfilename):
319 os.unlink(tempfilename)
320 return "break"
321 platform = os.name
322 printPlatform = True
323 if platform == 'posix': #posix platform
324 command = idleConf.GetOption('main','General',
325 'print-command-posix')
326 command = command + " 2>&1"
327 elif platform == 'nt': #win32 platform
328 command = idleConf.GetOption('main','General','print-command-win')
329 else: #no printing for this platform
330 printPlatform = False
331 if printPlatform: #we can try to print for this platform
332 command = command % shlex.quote(filename)
333 pipe = os.popen(command, "r")
334 # things can get ugly on NT if there is no printer available.
335 output = pipe.read().strip()
336 status = pipe.close()
337 if status:
338 output = "Printing failed (exit status 0x%x)\n" % \
339 status + output
340 if output:
341 output = "Printing command: %s\n" % repr(command) + output
342 messagebox.showerror("Print status", output, parent=self.text)
343 else: #no printing for this platform
344 message = "Printing is not enabled for this platform: %s" % platform
345 messagebox.showinfo("Print status", message, parent=self.text)
346 if tempfilename:
347 os.unlink(tempfilename)
348 return "break"
350 opendialog = None
351 savedialog = None
353 filetypes = (
354 ("Python files", py_extensions, "TEXT"),
355 ("Text files", "*.txt", "TEXT"),
356 ("All files", "*"),
357 )
359 defaultextension = '.py' if sys.platform == 'darwin' else ''
361 def askopenfile(self):
362 dir, base = self.defaultfilename("open")
363 if not self.opendialog:
364 self.opendialog = filedialog.Open(parent=self.text,
365 filetypes=self.filetypes)
366 filename = self.opendialog.show(initialdir=dir, initialfile=base)
367 return filename
369 def defaultfilename(self, mode="open"):
370 if self.filename:
371 return os.path.split(self.filename)
372 elif self.dirname:
373 return self.dirname, ""
374 else:
375 try:
376 pwd = os.getcwd()
377 except OSError:
378 pwd = ""
379 return pwd, ""
381 def asksavefile(self):
382 dir, base = self.defaultfilename("save")
383 if not self.savedialog:
384 self.savedialog = filedialog.SaveAs(
385 parent=self.text,
386 filetypes=self.filetypes,
387 defaultextension=self.defaultextension)
388 filename = self.savedialog.show(initialdir=dir, initialfile=base)
389 return filename
391 def updaterecentfileslist(self,filename):
392 "Update recent file list on all editor windows"
393 if self.editwin.flist:
394 self.editwin.update_recent_files_list(filename)
396def _io_binding(parent): # htest #
397 from tkinter import Toplevel, Text
399 root = Toplevel(parent)
400 root.title("Test IOBinding")
401 x, y = map(int, parent.geometry().split('+')[1:])
402 root.geometry("+%d+%d" % (x, y + 175))
403 class MyEditWin:
404 def __init__(self, text):
405 self.text = text
406 self.flist = None
407 self.text.bind("<Control-o>", self.open)
408 self.text.bind('<Control-p>', self.print)
409 self.text.bind("<Control-s>", self.save)
410 self.text.bind("<Alt-s>", self.saveas)
411 self.text.bind('<Control-c>', self.savecopy)
412 def get_saved(self): return 0
413 def set_saved(self, flag): pass
414 def reset_undo(self): pass
415 def open(self, event):
416 self.text.event_generate("<<open-window-from-file>>")
417 def print(self, event):
418 self.text.event_generate("<<print-window>>")
419 def save(self, event):
420 self.text.event_generate("<<save-window>>")
421 def saveas(self, event):
422 self.text.event_generate("<<save-window-as-file>>")
423 def savecopy(self, event):
424 self.text.event_generate("<<save-copy-of-window-as-file>>")
426 text = Text(root)
427 text.pack()
428 text.focus_set()
429 editwin = MyEditWin(text)
430 IOBinding(editwin)
432if __name__ == "__main__": 432 ↛ 433line 432 didn't jump to line 433, because the condition on line 432 was never true
433 from unittest import main
434 main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
436 from idlelib.idle_test.htest import run
437 run(_io_binding)