1import codecs 2from codecs import BOM_UTF8 3import os 4import re 5import shlex 6import sys 7import tempfile 8 9import tkinter.filedialog as tkFileDialog 10import tkinter.messagebox as tkMessageBox 11from tkinter.simpledialog import askstring 12 13import idlelib 14from idlelib.config import idleConf 15 16if idlelib.testing: # Set True by test.test_idle to avoid setlocale. 17 encoding = 'utf-8' 18else: 19 # Try setting the locale, so that we can find out 20 # what encoding to use 21 try: 22 import locale 23 locale.setlocale(locale.LC_CTYPE, "") 24 except (ImportError, locale.Error): 25 pass 26 27 locale_decode = 'ascii' 28 if sys.platform == 'win32': 29 # On Windows, we could use "mbcs". However, to give the user 30 # a portable encoding name, we need to find the code page 31 try: 32 locale_encoding = locale.getdefaultlocale()[1] 33 codecs.lookup(locale_encoding) 34 except LookupError: 35 pass 36 else: 37 try: 38 # Different things can fail here: the locale module may not be 39 # loaded, it may not offer nl_langinfo, or CODESET, or the 40 # resulting codeset may be unknown to Python. We ignore all 41 # these problems, falling back to ASCII 42 locale_encoding = locale.nl_langinfo(locale.CODESET) 43 if locale_encoding is None or locale_encoding == '': 44 # situation occurs on macOS 45 locale_encoding = 'ascii' 46 codecs.lookup(locale_encoding) 47 except (NameError, AttributeError, LookupError): 48 # Try getdefaultlocale: it parses environment variables, 49 # which may give a clue. Unfortunately, getdefaultlocale has 50 # bugs that can cause ValueError. 51 try: 52 locale_encoding = locale.getdefaultlocale()[1] 53 if locale_encoding is None or locale_encoding == '': 54 # situation occurs on macOS 55 locale_encoding = 'ascii' 56 codecs.lookup(locale_encoding) 57 except (ValueError, LookupError): 58 pass 59 60 locale_encoding = locale_encoding.lower() 61 62 encoding = locale_encoding 63 # Encoding is used in multiple files; locale_encoding nowhere. 64 # The only use of 'encoding' below is in _decode as initial value 65 # of deprecated block asking user for encoding. 66 # Perhaps use elsewhere should be reviewed. 67 68coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII) 69blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) 70 71def coding_spec(data): 72 """Return the encoding declaration according to PEP 263. 73 74 When checking encoded data, only the first two lines should be passed 75 in to avoid a UnicodeDecodeError if the rest of the data is not unicode. 76 The first two lines would contain the encoding specification. 77 78 Raise a LookupError if the encoding is declared but unknown. 79 """ 80 if isinstance(data, bytes): 81 # This encoding might be wrong. However, the coding 82 # spec must be ASCII-only, so any non-ASCII characters 83 # around here will be ignored. Decoding to Latin-1 should 84 # never fail (except for memory outage) 85 lines = data.decode('iso-8859-1') 86 else: 87 lines = data 88 # consider only the first two lines 89 if '\n' in lines: 90 lst = lines.split('\n', 2)[:2] 91 elif '\r' in lines: 92 lst = lines.split('\r', 2)[:2] 93 else: 94 lst = [lines] 95 for line in lst: 96 match = coding_re.match(line) 97 if match is not None: 98 break 99 if not blank_re.match(line): 100 return None 101 else: 102 return None 103 name = match.group(1) 104 try: 105 codecs.lookup(name) 106 except LookupError: 107 # The standard encoding error does not indicate the encoding 108 raise LookupError("Unknown encoding: "+name) 109 return name 110 111 112class IOBinding: 113# One instance per editor Window so methods know which to save, close. 114# Open returns focus to self.editwin if aborted. 115# EditorWindow.open_module, others, belong here. 116 117 def __init__(self, editwin): 118 self.editwin = editwin 119 self.text = editwin.text 120 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open) 121 self.__id_save = self.text.bind("<<save-window>>", self.save) 122 self.__id_saveas = self.text.bind("<<save-window-as-file>>", 123 self.save_as) 124 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>", 125 self.save_a_copy) 126 self.fileencoding = None 127 self.__id_print = self.text.bind("<<print-window>>", self.print_window) 128 129 def close(self): 130 # Undo command bindings 131 self.text.unbind("<<open-window-from-file>>", self.__id_open) 132 self.text.unbind("<<save-window>>", self.__id_save) 133 self.text.unbind("<<save-window-as-file>>",self.__id_saveas) 134 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy) 135 self.text.unbind("<<print-window>>", self.__id_print) 136 # Break cycles 137 self.editwin = None 138 self.text = None 139 self.filename_change_hook = None 140 141 def get_saved(self): 142 return self.editwin.get_saved() 143 144 def set_saved(self, flag): 145 self.editwin.set_saved(flag) 146 147 def reset_undo(self): 148 self.editwin.reset_undo() 149 150 filename_change_hook = None 151 152 def set_filename_change_hook(self, hook): 153 self.filename_change_hook = hook 154 155 filename = None 156 dirname = None 157 158 def set_filename(self, filename): 159 if filename and os.path.isdir(filename): 160 self.filename = None 161 self.dirname = filename 162 else: 163 self.filename = filename 164 self.dirname = None 165 self.set_saved(1) 166 if self.filename_change_hook: 167 self.filename_change_hook() 168 169 def open(self, event=None, editFile=None): 170 flist = self.editwin.flist 171 # Save in case parent window is closed (ie, during askopenfile()). 172 if flist: 173 if not editFile: 174 filename = self.askopenfile() 175 else: 176 filename=editFile 177 if filename: 178 # If editFile is valid and already open, flist.open will 179 # shift focus to its existing window. 180 # If the current window exists and is a fresh unnamed, 181 # unmodified editor window (not an interpreter shell), 182 # pass self.loadfile to flist.open so it will load the file 183 # in the current window (if the file is not already open) 184 # instead of a new window. 185 if (self.editwin and 186 not getattr(self.editwin, 'interp', None) and 187 not self.filename and 188 self.get_saved()): 189 flist.open(filename, self.loadfile) 190 else: 191 flist.open(filename) 192 else: 193 if self.text: 194 self.text.focus_set() 195 return "break" 196 197 # Code for use outside IDLE: 198 if self.get_saved(): 199 reply = self.maybesave() 200 if reply == "cancel": 201 self.text.focus_set() 202 return "break" 203 if not editFile: 204 filename = self.askopenfile() 205 else: 206 filename=editFile 207 if filename: 208 self.loadfile(filename) 209 else: 210 self.text.focus_set() 211 return "break" 212 213 eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) 214 eol_re = re.compile(eol) 215 eol_convention = os.linesep # default 216 217 def loadfile(self, filename): 218 try: 219 # open the file in binary mode so that we can handle 220 # end-of-line convention ourselves. 221 with open(filename, 'rb') as f: 222 two_lines = f.readline() + f.readline() 223 f.seek(0) 224 bytes = f.read() 225 except OSError as msg: 226 tkMessageBox.showerror("I/O Error", str(msg), parent=self.text) 227 return False 228 chars, converted = self._decode(two_lines, bytes) 229 if chars is None: 230 tkMessageBox.showerror("Decoding Error", 231 "File %s\nFailed to Decode" % filename, 232 parent=self.text) 233 return False 234 # We now convert all end-of-lines to '\n's 235 firsteol = self.eol_re.search(chars) 236 if firsteol: 237 self.eol_convention = firsteol.group(0) 238 chars = self.eol_re.sub(r"\n", chars) 239 self.text.delete("1.0", "end") 240 self.set_filename(None) 241 self.text.insert("1.0", chars) 242 self.reset_undo() 243 self.set_filename(filename) 244 if converted: 245 # We need to save the conversion results first 246 # before being able to execute the code 247 self.set_saved(False) 248 self.text.mark_set("insert", "1.0") 249 self.text.yview("insert") 250 self.updaterecentfileslist(filename) 251 return True 252 253 def _decode(self, two_lines, bytes): 254 "Create a Unicode string." 255 chars = None 256 # Check presence of a UTF-8 signature first 257 if bytes.startswith(BOM_UTF8): 258 try: 259 chars = bytes[3:].decode("utf-8") 260 except UnicodeDecodeError: 261 # has UTF-8 signature, but fails to decode... 262 return None, False 263 else: 264 # Indicates that this file originally had a BOM 265 self.fileencoding = 'BOM' 266 return chars, False 267 # Next look for coding specification 268 try: 269 enc = coding_spec(two_lines) 270 except LookupError as name: 271 tkMessageBox.showerror( 272 title="Error loading the file", 273 message="The encoding '%s' is not known to this Python "\ 274 "installation. The file may not display correctly" % name, 275 parent = self.text) 276 enc = None 277 except UnicodeDecodeError: 278 return None, False 279 if enc: 280 try: 281 chars = str(bytes, enc) 282 self.fileencoding = enc 283 return chars, False 284 except UnicodeDecodeError: 285 pass 286 # Try ascii: 287 try: 288 chars = str(bytes, 'ascii') 289 self.fileencoding = None 290 return chars, False 291 except UnicodeDecodeError: 292 pass 293 # Try utf-8: 294 try: 295 chars = str(bytes, 'utf-8') 296 self.fileencoding = 'utf-8' 297 return chars, False 298 except UnicodeDecodeError: 299 pass 300 # Finally, try the locale's encoding. This is deprecated; 301 # the user should declare a non-ASCII encoding 302 try: 303 # Wait for the editor window to appear 304 self.editwin.text.update() 305 enc = askstring( 306 "Specify file encoding", 307 "The file's encoding is invalid for Python 3.x.\n" 308 "IDLE will convert it to UTF-8.\n" 309 "What is the current encoding of the file?", 310 initialvalue = encoding, 311 parent = self.editwin.text) 312 313 if enc: 314 chars = str(bytes, enc) 315 self.fileencoding = None 316 return chars, True 317 except (UnicodeDecodeError, LookupError): 318 pass 319 return None, False # None on failure 320 321 def maybesave(self): 322 if self.get_saved(): 323 return "yes" 324 message = "Do you want to save %s before closing?" % ( 325 self.filename or "this untitled document") 326 confirm = tkMessageBox.askyesnocancel( 327 title="Save On Close", 328 message=message, 329 default=tkMessageBox.YES, 330 parent=self.text) 331 if confirm: 332 reply = "yes" 333 self.save(None) 334 if not self.get_saved(): 335 reply = "cancel" 336 elif confirm is None: 337 reply = "cancel" 338 else: 339 reply = "no" 340 self.text.focus_set() 341 return reply 342 343 def save(self, event): 344 if not self.filename: 345 self.save_as(event) 346 else: 347 if self.writefile(self.filename): 348 self.set_saved(True) 349 try: 350 self.editwin.store_file_breaks() 351 except AttributeError: # may be a PyShell 352 pass 353 self.text.focus_set() 354 return "break" 355 356 def save_as(self, event): 357 filename = self.asksavefile() 358 if filename: 359 if self.writefile(filename): 360 self.set_filename(filename) 361 self.set_saved(1) 362 try: 363 self.editwin.store_file_breaks() 364 except AttributeError: 365 pass 366 self.text.focus_set() 367 self.updaterecentfileslist(filename) 368 return "break" 369 370 def save_a_copy(self, event): 371 filename = self.asksavefile() 372 if filename: 373 self.writefile(filename) 374 self.text.focus_set() 375 self.updaterecentfileslist(filename) 376 return "break" 377 378 def writefile(self, filename): 379 self.fixlastline() 380 text = self.text.get("1.0", "end-1c") 381 if self.eol_convention != "\n": 382 text = text.replace("\n", self.eol_convention) 383 chars = self.encode(text) 384 try: 385 with open(filename, "wb") as f: 386 f.write(chars) 387 return True 388 except OSError as msg: 389 tkMessageBox.showerror("I/O Error", str(msg), 390 parent=self.text) 391 return False 392 393 def encode(self, chars): 394 if isinstance(chars, bytes): 395 # This is either plain ASCII, or Tk was returning mixed-encoding 396 # text to us. Don't try to guess further. 397 return chars 398 # Preserve a BOM that might have been present on opening 399 if self.fileencoding == 'BOM': 400 return BOM_UTF8 + chars.encode("utf-8") 401 # See whether there is anything non-ASCII in it. 402 # If not, no need to figure out the encoding. 403 try: 404 return chars.encode('ascii') 405 except UnicodeError: 406 pass 407 # Check if there is an encoding declared 408 try: 409 # a string, let coding_spec slice it to the first two lines 410 enc = coding_spec(chars) 411 failed = None 412 except LookupError as msg: 413 failed = msg 414 enc = None 415 else: 416 if not enc: 417 # PEP 3120: default source encoding is UTF-8 418 enc = 'utf-8' 419 if enc: 420 try: 421 return chars.encode(enc) 422 except UnicodeError: 423 failed = "Invalid encoding '%s'" % enc 424 tkMessageBox.showerror( 425 "I/O Error", 426 "%s.\nSaving as UTF-8" % failed, 427 parent = self.text) 428 # Fallback: save as UTF-8, with BOM - ignoring the incorrect 429 # declared encoding 430 return BOM_UTF8 + chars.encode("utf-8") 431 432 def fixlastline(self): 433 c = self.text.get("end-2c") 434 if c != '\n': 435 self.text.insert("end-1c", "\n") 436 437 def print_window(self, event): 438 confirm = tkMessageBox.askokcancel( 439 title="Print", 440 message="Print to Default Printer", 441 default=tkMessageBox.OK, 442 parent=self.text) 443 if not confirm: 444 self.text.focus_set() 445 return "break" 446 tempfilename = None 447 saved = self.get_saved() 448 if saved: 449 filename = self.filename 450 # shell undo is reset after every prompt, looks saved, probably isn't 451 if not saved or filename is None: 452 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') 453 filename = tempfilename 454 os.close(tfd) 455 if not self.writefile(tempfilename): 456 os.unlink(tempfilename) 457 return "break" 458 platform = os.name 459 printPlatform = True 460 if platform == 'posix': #posix platform 461 command = idleConf.GetOption('main','General', 462 'print-command-posix') 463 command = command + " 2>&1" 464 elif platform == 'nt': #win32 platform 465 command = idleConf.GetOption('main','General','print-command-win') 466 else: #no printing for this platform 467 printPlatform = False 468 if printPlatform: #we can try to print for this platform 469 command = command % shlex.quote(filename) 470 pipe = os.popen(command, "r") 471 # things can get ugly on NT if there is no printer available. 472 output = pipe.read().strip() 473 status = pipe.close() 474 if status: 475 output = "Printing failed (exit status 0x%x)\n" % \ 476 status + output 477 if output: 478 output = "Printing command: %s\n" % repr(command) + output 479 tkMessageBox.showerror("Print status", output, parent=self.text) 480 else: #no printing for this platform 481 message = "Printing is not enabled for this platform: %s" % platform 482 tkMessageBox.showinfo("Print status", message, parent=self.text) 483 if tempfilename: 484 os.unlink(tempfilename) 485 return "break" 486 487 opendialog = None 488 savedialog = None 489 490 filetypes = ( 491 ("Python files", "*.py *.pyw", "TEXT"), 492 ("Text files", "*.txt", "TEXT"), 493 ("All files", "*"), 494 ) 495 496 defaultextension = '.py' if sys.platform == 'darwin' else '' 497 498 def askopenfile(self): 499 dir, base = self.defaultfilename("open") 500 if not self.opendialog: 501 self.opendialog = tkFileDialog.Open(parent=self.text, 502 filetypes=self.filetypes) 503 filename = self.opendialog.show(initialdir=dir, initialfile=base) 504 return filename 505 506 def defaultfilename(self, mode="open"): 507 if self.filename: 508 return os.path.split(self.filename) 509 elif self.dirname: 510 return self.dirname, "" 511 else: 512 try: 513 pwd = os.getcwd() 514 except OSError: 515 pwd = "" 516 return pwd, "" 517 518 def asksavefile(self): 519 dir, base = self.defaultfilename("save") 520 if not self.savedialog: 521 self.savedialog = tkFileDialog.SaveAs( 522 parent=self.text, 523 filetypes=self.filetypes, 524 defaultextension=self.defaultextension) 525 filename = self.savedialog.show(initialdir=dir, initialfile=base) 526 return filename 527 528 def updaterecentfileslist(self,filename): 529 "Update recent file list on all editor windows" 530 if self.editwin.flist: 531 self.editwin.update_recent_files_list(filename) 532 533def _io_binding(parent): # htest # 534 from tkinter import Toplevel, Text 535 536 root = Toplevel(parent) 537 root.title("Test IOBinding") 538 x, y = map(int, parent.geometry().split('+')[1:]) 539 root.geometry("+%d+%d" % (x, y + 175)) 540 class MyEditWin: 541 def __init__(self, text): 542 self.text = text 543 self.flist = None 544 self.text.bind("<Control-o>", self.open) 545 self.text.bind('<Control-p>', self.print) 546 self.text.bind("<Control-s>", self.save) 547 self.text.bind("<Alt-s>", self.saveas) 548 self.text.bind('<Control-c>', self.savecopy) 549 def get_saved(self): return 0 550 def set_saved(self, flag): pass 551 def reset_undo(self): pass 552 def open(self, event): 553 self.text.event_generate("<<open-window-from-file>>") 554 def print(self, event): 555 self.text.event_generate("<<print-window>>") 556 def save(self, event): 557 self.text.event_generate("<<save-window>>") 558 def saveas(self, event): 559 self.text.event_generate("<<save-window-as-file>>") 560 def savecopy(self, event): 561 self.text.event_generate("<<save-copy-of-window-as-file>>") 562 563 text = Text(root) 564 text.pack() 565 text.focus_set() 566 editwin = MyEditWin(text) 567 IOBinding(editwin) 568 569if __name__ == "__main__": 570 from unittest import main 571 main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False) 572 573 from idlelib.idle_test.htest import run 574 run(_io_binding) 575