IOBinding.py 19.3 KB
Newer Older
David Scherer's avatar
David Scherer committed
1
import os
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
2
import types
3
import shlex
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
4 5
import sys
import codecs
6
import tempfile
7 8
import tkinter.filedialog as tkFileDialog
import tkinter.messagebox as tkMessageBox
David Scherer's avatar
David Scherer committed
9
import re
10
from tkinter import *
11
from tkinter.simpledialog import askstring
David Scherer's avatar
David Scherer committed
12

13
from idlelib.configHandler import idleConf
David Scherer's avatar
David Scherer committed
14

15
from codecs import BOM_UTF8
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
16 17 18 19 20 21

# Try setting the locale, so that we can find out
# what encoding to use
try:
    import locale
    locale.setlocale(locale.LC_CTYPE, "")
22
except (ImportError, locale.Error):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
23 24
    pass

25
# Encoding for file names
26
filesystemencoding = sys.getfilesystemencoding()  ### currently unused
27

28
locale_encoding = 'ascii'
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
29 30 31 32
if sys.platform == 'win32':
    # On Windows, we could use "mbcs". However, to give the user
    # a portable encoding name, we need to find the code page
    try:
33 34
        locale_encoding = locale.getdefaultlocale()[1]
        codecs.lookup(locale_encoding)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
35 36 37 38 39 40 41 42
    except LookupError:
        pass
else:
    try:
        # Different things can fail here: the locale module may not be
        # loaded, it may not offer nl_langinfo, or CODESET, or the
        # resulting codeset may be unknown to Python. We ignore all
        # these problems, falling back to ASCII
43 44
        locale_encoding = locale.nl_langinfo(locale.CODESET)
        if locale_encoding is None or locale_encoding is '':
45
            # situation occurs on Mac OS X
46 47
            locale_encoding = 'ascii'
        codecs.lookup(locale_encoding)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
48
    except (NameError, AttributeError, LookupError):
49
        # Try getdefaultlocale: it parses environment variables,
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
50 51 52
        # which may give a clue. Unfortunately, getdefaultlocale has
        # bugs that can cause ValueError.
        try:
53 54
            locale_encoding = locale.getdefaultlocale()[1]
            if locale_encoding is None or locale_encoding is '':
55
                # situation occurs on Mac OS X
56 57
                locale_encoding = 'ascii'
            codecs.lookup(locale_encoding)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
58 59 60
        except (ValueError, LookupError):
            pass

61 62 63 64
locale_encoding = locale_encoding.lower()

encoding = locale_encoding  ### KBK 07Sep07  This is used all over IDLE, check!
                            ### 'encoding' is used below in encode(), check!
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
65

66
coding_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)', re.ASCII)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
67

68
def coding_spec(data):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
69 70
    """Return the encoding declaration according to PEP 263.

71 72 73 74 75
    When checking encoded data, only the first two lines should be passed
    in to avoid a UnicodeDecodeError if the rest of the data is not unicode.
    The first two lines would contain the encoding specification.

    Raise a LookupError if the encoding is declared but unknown.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
76
    """
77
    if isinstance(data, bytes):
78 79 80 81 82
        # This encoding might be wrong. However, the coding
        # spec must be ASCII-only, so any non-ASCII characters
        # around here will be ignored. Decoding to Latin-1 should
        # never fail (except for memory outage)
        lines = data.decode('iso-8859-1')
83
    else:
84 85 86
        lines = data
    # consider only the first two lines
    if '\n' in lines:
87
        lst = lines.split('\n', 2)[:2]
88
    elif '\r' in lines:
89 90 91 92 93 94 95
        lst = lines.split('\r', 2)[:2]
    else:
        lst = [lines]
    for line in lst:
        match = coding_re.match(line)
        if match is not None:
            break
96
    else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
97 98 99 100 101 102
        return None
    name = match.group(1)
    try:
        codecs.lookup(name)
    except LookupError:
        # The standard encoding error does not indicate the encoding
103
        raise LookupError("Unknown encoding: "+name)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
104
    return name
David Scherer's avatar
David Scherer committed
105

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
106

David Scherer's avatar
David Scherer committed
107 108 109 110 111 112 113 114 115 116 117
class IOBinding:

    def __init__(self, editwin):
        self.editwin = editwin
        self.text = editwin.text
        self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
        self.__id_save = self.text.bind("<<save-window>>", self.save)
        self.__id_saveas = self.text.bind("<<save-window-as-file>>",
                                          self.save_as)
        self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
                                            self.save_a_copy)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
118
        self.fileencoding = None
119
        self.__id_print = self.text.bind("<<print-window>>", self.print_window)
120

David Scherer's avatar
David Scherer committed
121 122 123 124 125 126
    def close(self):
        # Undo command bindings
        self.text.unbind("<<open-window-from-file>>", self.__id_open)
        self.text.unbind("<<save-window>>", self.__id_save)
        self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
        self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
127
        self.text.unbind("<<print-window>>", self.__id_print)
David Scherer's avatar
David Scherer committed
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
        # Break cycles
        self.editwin = None
        self.text = None
        self.filename_change_hook = None

    def get_saved(self):
        return self.editwin.get_saved()

    def set_saved(self, flag):
        self.editwin.set_saved(flag)

    def reset_undo(self):
        self.editwin.reset_undo()

    filename_change_hook = None

    def set_filename_change_hook(self, hook):
        self.filename_change_hook = hook

    filename = None
148
    dirname = None
David Scherer's avatar
David Scherer committed
149 150

    def set_filename(self, filename):
151 152 153 154 155 156 157 158 159
        if filename and os.path.isdir(filename):
            self.filename = None
            self.dirname = filename
        else:
            self.filename = filename
            self.dirname = None
            self.set_saved(1)
            if self.filename_change_hook:
                self.filename_change_hook()
David Scherer's avatar
David Scherer committed
160

161
    def open(self, event=None, editFile=None):
162
        flist = self.editwin.flist
163
        # Save in case parent window is closed (ie, during askopenfile()).
164
        if flist:
165 166 167 168
            if not editFile:
                filename = self.askopenfile()
            else:
                filename=editFile
David Scherer's avatar
David Scherer committed
169
            if filename:
170 171 172 173 174 175 176 177 178 179 180
                # If editFile is valid and already open, flist.open will
                # shift focus to its existing window.
                # If the current window exists and is a fresh unnamed,
                # unmodified editor window (not an interpreter shell),
                # pass self.loadfile to flist.open so it will load the file
                # in the current window (if the file is not already open)
                # instead of a new window.
                if (self.editwin and
                        not getattr(self.editwin, 'interp', None) and
                        not self.filename and
                        self.get_saved()):
181
                    flist.open(filename, self.loadfile)
David Scherer's avatar
David Scherer committed
182
                else:
183
                    flist.open(filename)
David Scherer's avatar
David Scherer committed
184
            else:
185 186
                if self.text:
                    self.text.focus_set()
David Scherer's avatar
David Scherer committed
187
            return "break"
188

David Scherer's avatar
David Scherer committed
189 190 191 192 193 194
        # Code for use outside IDLE:
        if self.get_saved():
            reply = self.maybesave()
            if reply == "cancel":
                self.text.focus_set()
                return "break"
195 196 197 198
        if not editFile:
            filename = self.askopenfile()
        else:
            filename=editFile
David Scherer's avatar
David Scherer committed
199 200 201 202 203 204
        if filename:
            self.loadfile(filename)
        else:
            self.text.focus_set()
        return "break"

205 206
    eol = r"(\r\n)|\n|\r"  # \r\n (Windows), \n (UNIX), or \r (Mac)
    eol_re = re.compile(eol)
207
    eol_convention = os.linesep  # default
208

David Scherer's avatar
David Scherer committed
209 210 211
    def loadfile(self, filename):
        try:
            # open the file in binary mode so that we can handle
212
            # end-of-line convention ourselves.
213 214 215 216
            with open(filename, 'rb') as f:
                two_lines = f.readline() + f.readline()
                f.seek(0)
                bytes = f.read()
217
        except OSError as msg:
David Scherer's avatar
David Scherer committed
218
            tkMessageBox.showerror("I/O Error", str(msg), master=self.text)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
219
            return False
220
        chars, converted = self._decode(two_lines, bytes)
221 222 223 224 225
        if chars is None:
            tkMessageBox.showerror("Decoding Error",
                                   "File %s\nFailed to Decode" % filename,
                                   parent=self.text)
            return False
David Scherer's avatar
David Scherer committed
226
        # We now convert all end-of-lines to '\n's
227 228 229 230
        firsteol = self.eol_re.search(chars)
        if firsteol:
            self.eol_convention = firsteol.group(0)
            chars = self.eol_re.sub(r"\n", chars)
David Scherer's avatar
David Scherer committed
231 232 233 234 235
        self.text.delete("1.0", "end")
        self.set_filename(None)
        self.text.insert("1.0", chars)
        self.reset_undo()
        self.set_filename(filename)
236 237 238 239
        if converted:
            # We need to save the conversion results first
            # before being able to execute the code
            self.set_saved(False)
David Scherer's avatar
David Scherer committed
240
        self.text.mark_set("insert", "1.0")
241
        self.text.yview("insert")
242
        self.updaterecentfileslist(filename)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
243 244
        return True

245 246 247
    def _decode(self, two_lines, bytes):
        "Create a Unicode string."
        chars = None
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
248
        # Check presence of a UTF-8 signature first
249
        if bytes.startswith(BOM_UTF8):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
250
            try:
251 252
                chars = bytes[3:].decode("utf-8")
            except UnicodeDecodeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
253
                # has UTF-8 signature, but fails to decode...
254
                return None, False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
255 256
            else:
                # Indicates that this file originally had a BOM
257
                self.fileencoding = 'BOM'
258
                return chars, False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
259 260
        # Next look for coding specification
        try:
261
            enc = coding_spec(two_lines)
262
        except LookupError as name:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
263 264 265 266 267 268
            tkMessageBox.showerror(
                title="Error loading the file",
                message="The encoding '%s' is not known to this Python "\
                "installation. The file may not display correctly" % name,
                master = self.text)
            enc = None
269
        except UnicodeDecodeError:
270
            return None, False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
271 272
        if enc:
            try:
273 274
                chars = str(bytes, enc)
                self.fileencoding = enc
275
                return chars, False
276
            except UnicodeDecodeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
277
                pass
278
        # Try ascii:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
279
        try:
280 281
            chars = str(bytes, 'ascii')
            self.fileencoding = None
282
            return chars, False
283 284 285 286 287 288
        except UnicodeDecodeError:
            pass
        # Try utf-8:
        try:
            chars = str(bytes, 'utf-8')
            self.fileencoding = 'utf-8'
289
            return chars, False
290
        except UnicodeDecodeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
291 292 293 294
            pass
        # Finally, try the locale's encoding. This is deprecated;
        # the user should declare a non-ASCII encoding
        try:
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
            # Wait for the editor window to appear
            self.editwin.text.update()
            enc = askstring(
                "Specify file encoding",
                "The file's encoding is invalid for Python 3.x.\n"
                "IDLE will convert it to UTF-8.\n"
                "What is the current encoding of the file?",
                initialvalue = locale_encoding,
                parent = self.editwin.text)

            if enc:
                chars = str(bytes, enc)
                self.fileencoding = None
            return chars, True
        except (UnicodeDecodeError, LookupError):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
310
            pass
311
        return None, False  # None on failure
David Scherer's avatar
David Scherer committed
312 313 314 315 316 317

    def maybesave(self):
        if self.get_saved():
            return "yes"
        message = "Do you want to save %s before closing?" % (
            self.filename or "this untitled document")
318 319 320 321 322 323 324
        confirm = tkMessageBox.askyesnocancel(
                  title="Save On Close",
                  message=message,
                  default=tkMessageBox.YES,
                  master=self.text)
        if confirm:
            reply = "yes"
David Scherer's avatar
David Scherer committed
325 326 327
            self.save(None)
            if not self.get_saved():
                reply = "cancel"
328 329 330 331
        elif confirm is None:
            reply = "cancel"
        else:
            reply = "no"
David Scherer's avatar
David Scherer committed
332 333 334 335 336 337 338 339
        self.text.focus_set()
        return reply

    def save(self, event):
        if not self.filename:
            self.save_as(event)
        else:
            if self.writefile(self.filename):
340
                self.set_saved(True)
341 342 343 344
                try:
                    self.editwin.store_file_breaks()
                except AttributeError:  # may be a PyShell
                    pass
David Scherer's avatar
David Scherer committed
345 346 347 348 349 350 351 352 353
        self.text.focus_set()
        return "break"

    def save_as(self, event):
        filename = self.asksavefile()
        if filename:
            if self.writefile(filename):
                self.set_filename(filename)
                self.set_saved(1)
354 355 356 357
                try:
                    self.editwin.store_file_breaks()
                except AttributeError:
                    pass
David Scherer's avatar
David Scherer committed
358
        self.text.focus_set()
359
        self.updaterecentfileslist(filename)
David Scherer's avatar
David Scherer committed
360 361 362 363 364 365 366
        return "break"

    def save_a_copy(self, event):
        filename = self.asksavefile()
        if filename:
            self.writefile(filename)
        self.text.focus_set()
367
        self.updaterecentfileslist(filename)
David Scherer's avatar
David Scherer committed
368 369 370 371
        return "break"

    def writefile(self, filename):
        self.fixlastline()
372
        text = self.text.get("1.0", "end-1c")
373
        if self.eol_convention != "\n":
374
            text = text.replace("\n", self.eol_convention)
375
        chars = self.encode(text)
David Scherer's avatar
David Scherer committed
376
        try:
377 378
            with open(filename, "wb") as f:
                f.write(chars)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
379
            return True
380
        except OSError as msg:
David Scherer's avatar
David Scherer committed
381 382
            tkMessageBox.showerror("I/O Error", str(msg),
                                   master=self.text)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
383 384 385
            return False

    def encode(self, chars):
386
        if isinstance(chars, bytes):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
387 388 389
            # This is either plain ASCII, or Tk was returning mixed-encoding
            # text to us. Don't try to guess further.
            return chars
390 391 392
        # Preserve a BOM that might have been present on opening
        if self.fileencoding == 'BOM':
            return BOM_UTF8 + chars.encode("utf-8")
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
393 394 395 396 397 398
        # See whether there is anything non-ASCII in it.
        # If not, no need to figure out the encoding.
        try:
            return chars.encode('ascii')
        except UnicodeError:
            pass
399
        # Check if there is an encoding declared
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
400
        try:
401
            # a string, let coding_spec slice it to the first two lines
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
402 403
            enc = coding_spec(chars)
            failed = None
404
        except LookupError as msg:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
405 406
            failed = msg
            enc = None
407 408 409 410
        else:
            if not enc:
                # PEP 3120: default source encoding is UTF-8
                enc = 'utf-8'
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
411 412 413 414 415
        if enc:
            try:
                return chars.encode(enc)
            except UnicodeError:
                failed = "Invalid encoding '%s'" % enc
416 417 418 419 420 421 422
        tkMessageBox.showerror(
            "I/O Error",
            "%s.\nSaving as UTF-8" % failed,
            master = self.text)
        # Fallback: save as UTF-8, with BOM - ignoring the incorrect
        # declared encoding
        return BOM_UTF8 + chars.encode("utf-8")
423

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
424 425 426 427 428
    def fixlastline(self):
        c = self.text.get("end-2c")
        if c != '\n':
            self.text.insert("end-1c", "\n")

429
    def print_window(self, event):
430 431 432 433 434 435
        confirm = tkMessageBox.askokcancel(
                  title="Print",
                  message="Print to Default Printer",
                  default=tkMessageBox.OK,
                  master=self.text)
        if not confirm:
436 437
            self.text.focus_set()
            return "break"
438
        tempfilename = None
439 440
        saved = self.get_saved()
        if saved:
441
            filename = self.filename
442 443
        # shell undo is reset after every prompt, looks saved, probably isn't
        if not saved or filename is None:
444 445 446 447
            (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
            filename = tempfilename
            os.close(tfd)
            if not self.writefile(tempfilename):
448 449
                os.unlink(tempfilename)
                return "break"
450 451
        platform = os.name
        printPlatform = True
452
        if platform == 'posix': #posix platform
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
453 454
            command = idleConf.GetOption('main','General',
                                         'print-command-posix')
455 456 457 458
            command = command + " 2>&1"
        elif platform == 'nt': #win32 platform
            command = idleConf.GetOption('main','General','print-command-win')
        else: #no printing for this platform
459
            printPlatform = False
460
        if printPlatform:  #we can try to print for this platform
461
            command = command % shlex.quote(filename)
462
            pipe = os.popen(command, "r")
463
            # things can get ugly on NT if there is no printer available.
464 465 466
            output = pipe.read().strip()
            status = pipe.close()
            if status:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
467 468
                output = "Printing failed (exit status 0x%x)\n" % \
                         status + output
469 470 471 472
            if output:
                output = "Printing command: %s\n" % repr(command) + output
                tkMessageBox.showerror("Print status", output, master=self.text)
        else:  #no printing for this platform
473
            message = "Printing is not enabled for this platform: %s" % platform
474
            tkMessageBox.showinfo("Print status", message, master=self.text)
475 476
        if tempfilename:
            os.unlink(tempfilename)
477
        return "break"
478

David Scherer's avatar
David Scherer committed
479 480 481 482
    opendialog = None
    savedialog = None

    filetypes = [
483 484
        ("Python files", "*.py *.pyw", "TEXT"),
        ("Text files", "*.txt", "TEXT"),
David Scherer's avatar
David Scherer committed
485 486 487
        ("All files", "*"),
        ]

488 489
    defaultextension = '.py' if sys.platform == 'darwin' else ''

David Scherer's avatar
David Scherer committed
490 491 492 493 494
    def askopenfile(self):
        dir, base = self.defaultfilename("open")
        if not self.opendialog:
            self.opendialog = tkFileDialog.Open(master=self.text,
                                                filetypes=self.filetypes)
495 496
        filename = self.opendialog.show(initialdir=dir, initialfile=base)
        return filename
David Scherer's avatar
David Scherer committed
497 498 499 500

    def defaultfilename(self, mode="open"):
        if self.filename:
            return os.path.split(self.filename)
501 502
        elif self.dirname:
            return self.dirname, ""
David Scherer's avatar
David Scherer committed
503 504 505
        else:
            try:
                pwd = os.getcwd()
506
            except OSError:
David Scherer's avatar
David Scherer committed
507 508 509 510 511 512
                pwd = ""
            return pwd, ""

    def asksavefile(self):
        dir, base = self.defaultfilename("save")
        if not self.savedialog:
513 514 515 516
            self.savedialog = tkFileDialog.SaveAs(
                    master=self.text,
                    filetypes=self.filetypes,
                    defaultextension=self.defaultextension)
517 518
        filename = self.savedialog.show(initialdir=dir, initialfile=base)
        return filename
David Scherer's avatar
David Scherer committed
519

520
    def updaterecentfileslist(self,filename):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
521
        "Update recent file list on all editor windows"
522 523
        if self.editwin.flist:
            self.editwin.update_recent_files_list(filename)
524

David Scherer's avatar
David Scherer committed
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
def test():
    root = Tk()
    class MyEditWin:
        def __init__(self, text):
            self.text = text
            self.flist = None
            self.text.bind("<Control-o>", self.open)
            self.text.bind("<Control-s>", self.save)
            self.text.bind("<Alt-s>", self.save_as)
            self.text.bind("<Alt-z>", self.save_a_copy)
        def get_saved(self): return 0
        def set_saved(self, flag): pass
        def reset_undo(self): pass
        def open(self, event):
            self.text.event_generate("<<open-window-from-file>>")
        def save(self, event):
            self.text.event_generate("<<save-window>>")
        def save_as(self, event):
            self.text.event_generate("<<save-window-as-file>>")
        def save_a_copy(self, event):
            self.text.event_generate("<<save-copy-of-window-as-file>>")
    text = Text(root)
    text.pack()
    text.focus_set()
    editwin = MyEditWin(text)
    io = IOBinding(editwin)
    root.mainloop()

if __name__ == "__main__":
    test()