iomenu.py 20.3 KB
Newer Older
1 2
import codecs
from codecs import BOM_UTF8
David Scherer's avatar
David Scherer committed
3
import os
4
import re
5
import shlex
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
6
import sys
7
import tempfile
8

9 10
import tkinter.filedialog as tkFileDialog
import tkinter.messagebox as tkMessageBox
11
from tkinter.simpledialog import askstring
David Scherer's avatar
David Scherer committed
12

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

16 17
if idlelib.testing:  # Set True by test.test_idle to avoid setlocale.
    encoding = 'utf-8'
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
18
else:
19 20
    # Try setting the locale, so that we can find out
    # what encoding to use
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
21
    try:
22 23 24 25 26 27 28 29 30
        import locale
        locale.setlocale(locale.LC_CTYPE, "")
    except (ImportError, locale.Error):
        pass

    locale_decode = 'ascii'
    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
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
31
        try:
32
            locale_encoding = locale.getdefaultlocale()[1]
33 34 35 36 37 38 39 40 41 42
            codecs.lookup(locale_encoding)
        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
            locale_encoding = locale.nl_langinfo(locale.CODESET)
43
            if locale_encoding is None or locale_encoding is '':
44
                # situation occurs on Mac OS X
45 46
                locale_encoding = 'ascii'
            codecs.lookup(locale_encoding)
47 48 49 50 51 52 53 54 55 56 57 58
        except (NameError, AttributeError, LookupError):
            # Try getdefaultlocale: it parses environment variables,
            # which may give a clue. Unfortunately, getdefaultlocale has
            # bugs that can cause ValueError.
            try:
                locale_encoding = locale.getdefaultlocale()[1]
                if locale_encoding is None or locale_encoding is '':
                    # situation occurs on Mac OS X
                    locale_encoding = 'ascii'
                codecs.lookup(locale_encoding)
            except (ValueError, LookupError):
                pass
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
59

60
    locale_encoding = locale_encoding.lower()
61

62 63 64 65 66
    encoding = locale_encoding
    # Encoding is used in multiple files; locale_encoding nowhere.
    # The only use of 'encoding' below is in _decode as initial value
    # of deprecated block asking user for encoding.
    # Perhaps use elsewhere should be reviewed.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
67

68
coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII)
69
blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
70

71
def coding_spec(data):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
72 73
    """Return the encoding declaration according to PEP 263.

74 75 76 77 78
    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
79
    """
80
    if isinstance(data, bytes):
81 82 83 84 85
        # 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')
86
    else:
87 88 89
        lines = data
    # consider only the first two lines
    if '\n' in lines:
90
        lst = lines.split('\n', 2)[:2]
91
    elif '\r' in lines:
92 93 94 95 96 97 98
        lst = lines.split('\r', 2)[:2]
    else:
        lst = [lines]
    for line in lst:
        match = coding_re.match(line)
        if match is not None:
            break
99 100
        if not blank_re.match(line):
            return None
101
    else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
102 103 104 105 106 107
        return None
    name = match.group(1)
    try:
        codecs.lookup(name)
    except LookupError:
        # The standard encoding error does not indicate the encoding
108
        raise LookupError("Unknown encoding: "+name)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
109
    return name
David Scherer's avatar
David Scherer committed
110

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

David Scherer's avatar
David Scherer committed
112
class IOBinding:
113 114 115
# One instance per editor Window so methods know which to save, close.
# Open returns focus to self.editwin if aborted.
# EditorWindow.open_module, others, belong here.
David Scherer's avatar
David Scherer committed
116 117 118 119 120 121 122 123 124 125

    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
126
        self.fileencoding = None
127
        self.__id_print = self.text.bind("<<print-window>>", self.print_window)
128

David Scherer's avatar
David Scherer committed
129 130 131 132 133 134
    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)
135
        self.text.unbind("<<print-window>>", self.__id_print)
David Scherer's avatar
David Scherer committed
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
        # 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
156
    dirname = None
David Scherer's avatar
David Scherer committed
157 158

    def set_filename(self, filename):
159 160 161 162 163 164 165 166 167
        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
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 174 175 176
            if not editFile:
                filename = self.askopenfile()
            else:
                filename=editFile
David Scherer's avatar
David Scherer committed
177
            if filename:
178 179 180 181 182 183 184 185 186 187 188
                # 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()):
189
                    flist.open(filename, self.loadfile)
David Scherer's avatar
David Scherer committed
190
                else:
191
                    flist.open(filename)
David Scherer's avatar
David Scherer committed
192
            else:
193 194
                if self.text:
                    self.text.focus_set()
David Scherer's avatar
David Scherer committed
195
            return "break"
196

David Scherer's avatar
David Scherer committed
197 198 199 200 201 202
        # Code for use outside IDLE:
        if self.get_saved():
            reply = self.maybesave()
            if reply == "cancel":
                self.text.focus_set()
                return "break"
203 204 205 206
        if not editFile:
            filename = self.askopenfile()
        else:
            filename=editFile
David Scherer's avatar
David Scherer committed
207 208 209 210 211 212
        if filename:
            self.loadfile(filename)
        else:
            self.text.focus_set()
        return "break"

213 214
    eol = r"(\r\n)|\n|\r"  # \r\n (Windows), \n (UNIX), or \r (Mac)
    eol_re = re.compile(eol)
215
    eol_convention = os.linesep  # default
216

David Scherer's avatar
David Scherer committed
217 218 219
    def loadfile(self, filename):
        try:
            # open the file in binary mode so that we can handle
220
            # end-of-line convention ourselves.
221 222 223 224
            with open(filename, 'rb') as f:
                two_lines = f.readline() + f.readline()
                f.seek(0)
                bytes = f.read()
225
        except OSError as msg:
226
            tkMessageBox.showerror("I/O Error", str(msg), parent=self.text)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
227
            return False
228
        chars, converted = self._decode(two_lines, bytes)
229 230 231 232 233
        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
234
        # We now convert all end-of-lines to '\n's
235 236 237 238
        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
239 240 241 242 243
        self.text.delete("1.0", "end")
        self.set_filename(None)
        self.text.insert("1.0", chars)
        self.reset_undo()
        self.set_filename(filename)
244 245 246 247
        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
248
        self.text.mark_set("insert", "1.0")
249
        self.text.yview("insert")
250
        self.updaterecentfileslist(filename)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
251 252
        return True

253 254 255
    def _decode(self, two_lines, bytes):
        "Create a Unicode string."
        chars = None
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
256
        # Check presence of a UTF-8 signature first
257
        if bytes.startswith(BOM_UTF8):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
258
            try:
259 260
                chars = bytes[3:].decode("utf-8")
            except UnicodeDecodeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
261
                # has UTF-8 signature, but fails to decode...
262
                return None, False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
263 264
            else:
                # Indicates that this file originally had a BOM
265
                self.fileencoding = 'BOM'
266
                return chars, False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
267 268
        # Next look for coding specification
        try:
269
            enc = coding_spec(two_lines)
270
        except LookupError as name:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
271 272 273 274
            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,
275
                parent = self.text)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
276
            enc = None
277
        except UnicodeDecodeError:
278
            return None, False
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
279 280
        if enc:
            try:
281 282
                chars = str(bytes, enc)
                self.fileencoding = enc
283
                return chars, False
284
            except UnicodeDecodeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
285
                pass
286
        # Try ascii:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
287
        try:
288 289
            chars = str(bytes, 'ascii')
            self.fileencoding = None
290
            return chars, False
291 292 293 294 295 296
        except UnicodeDecodeError:
            pass
        # Try utf-8:
        try:
            chars = str(bytes, 'utf-8')
            self.fileencoding = 'utf-8'
297
            return chars, False
298
        except UnicodeDecodeError:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
299 300 301 302
            pass
        # Finally, try the locale's encoding. This is deprecated;
        # the user should declare a non-ASCII encoding
        try:
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?",
310
                initialvalue = encoding,
311 312 313 314 315 316 317
                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
318
            pass
319
        return None, False  # None on failure
David Scherer's avatar
David Scherer committed
320 321 322 323 324 325

    def maybesave(self):
        if self.get_saved():
            return "yes"
        message = "Do you want to save %s before closing?" % (
            self.filename or "this untitled document")
326 327 328 329
        confirm = tkMessageBox.askyesnocancel(
                  title="Save On Close",
                  message=message,
                  default=tkMessageBox.YES,
330
                  parent=self.text)
331 332
        if confirm:
            reply = "yes"
David Scherer's avatar
David Scherer committed
333 334 335
            self.save(None)
            if not self.get_saved():
                reply = "cancel"
336 337 338 339
        elif confirm is None:
            reply = "cancel"
        else:
            reply = "no"
David Scherer's avatar
David Scherer committed
340 341 342 343 344 345 346 347
        self.text.focus_set()
        return reply

    def save(self, event):
        if not self.filename:
            self.save_as(event)
        else:
            if self.writefile(self.filename):
348
                self.set_saved(True)
349 350 351 352
                try:
                    self.editwin.store_file_breaks()
                except AttributeError:  # may be a PyShell
                    pass
David Scherer's avatar
David Scherer committed
353 354 355 356 357 358 359 360 361
        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)
362 363 364 365
                try:
                    self.editwin.store_file_breaks()
                except AttributeError:
                    pass
David Scherer's avatar
David Scherer committed
366
        self.text.focus_set()
367
        self.updaterecentfileslist(filename)
David Scherer's avatar
David Scherer committed
368 369 370 371 372 373 374
        return "break"

    def save_a_copy(self, event):
        filename = self.asksavefile()
        if filename:
            self.writefile(filename)
        self.text.focus_set()
375
        self.updaterecentfileslist(filename)
David Scherer's avatar
David Scherer committed
376 377 378 379
        return "break"

    def writefile(self, filename):
        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)
David Scherer's avatar
David Scherer committed
384
        try:
385 386
            with open(filename, "wb") as f:
                f.write(chars)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
387
            return True
388
        except OSError as msg:
David Scherer's avatar
David Scherer committed
389
            tkMessageBox.showerror("I/O Error", str(msg),
390
                                   parent=self.text)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
391 392 393
            return False

    def encode(self, chars):
394
        if isinstance(chars, bytes):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
395 396 397
            # This is either plain ASCII, or Tk was returning mixed-encoding
            # text to us. Don't try to guess further.
            return chars
398 399 400
        # 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
401 402 403 404 405 406
        # 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
407
        # Check if there is an encoding declared
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
408
        try:
409
            # a string, let coding_spec slice it to the first two lines
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
410 411
            enc = coding_spec(chars)
            failed = None
412
        except LookupError as msg:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
413 414
            failed = msg
            enc = None
415 416 417 418
        else:
            if not enc:
                # PEP 3120: default source encoding is UTF-8
                enc = 'utf-8'
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
419 420 421 422 423
        if enc:
            try:
                return chars.encode(enc)
            except UnicodeError:
                failed = "Invalid encoding '%s'" % enc
424 425 426
        tkMessageBox.showerror(
            "I/O Error",
            "%s.\nSaving as UTF-8" % failed,
427
            parent = self.text)
428 429 430
        # Fallback: save as UTF-8, with BOM - ignoring the incorrect
        # declared encoding
        return BOM_UTF8 + chars.encode("utf-8")
431

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
432 433 434 435 436
    def fixlastline(self):
        c = self.text.get("end-2c")
        if c != '\n':
            self.text.insert("end-1c", "\n")

437
    def print_window(self, event):
438 439 440 441
        confirm = tkMessageBox.askokcancel(
                  title="Print",
                  message="Print to Default Printer",
                  default=tkMessageBox.OK,
442
                  parent=self.text)
443
        if not confirm:
444 445
            self.text.focus_set()
            return "break"
446
        tempfilename = None
447 448
        saved = self.get_saved()
        if saved:
449
            filename = self.filename
450 451
        # shell undo is reset after every prompt, looks saved, probably isn't
        if not saved or filename is None:
452 453 454 455
            (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
            filename = tempfilename
            os.close(tfd)
            if not self.writefile(tempfilename):
456 457
                os.unlink(tempfilename)
                return "break"
458 459
        platform = os.name
        printPlatform = True
460
        if platform == 'posix': #posix platform
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
461 462
            command = idleConf.GetOption('main','General',
                                         'print-command-posix')
463 464 465 466
            command = command + " 2>&1"
        elif platform == 'nt': #win32 platform
            command = idleConf.GetOption('main','General','print-command-win')
        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 473 474
            output = pipe.read().strip()
            status = pipe.close()
            if status:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
475 476
                output = "Printing failed (exit status 0x%x)\n" % \
                         status + output
477 478
            if output:
                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 484
        if tempfilename:
            os.unlink(tempfilename)
485
        return "break"
486

David Scherer's avatar
David Scherer committed
487 488 489
    opendialog = None
    savedialog = None

490
    filetypes = (
491 492
        ("Python files", "*.py *.pyw", "TEXT"),
        ("Text files", "*.txt", "TEXT"),
David Scherer's avatar
David Scherer committed
493
        ("All files", "*"),
494
        )
David Scherer's avatar
David Scherer committed
495

496 497
    defaultextension = '.py' if sys.platform == 'darwin' else ''

David Scherer's avatar
David Scherer committed
498 499 500
    def askopenfile(self):
        dir, base = self.defaultfilename("open")
        if not self.opendialog:
501
            self.opendialog = tkFileDialog.Open(parent=self.text,
David Scherer's avatar
David Scherer committed
502
                                                filetypes=self.filetypes)
503 504
        filename = self.opendialog.show(initialdir=dir, initialfile=base)
        return filename
David Scherer's avatar
David Scherer committed
505 506 507 508

    def defaultfilename(self, mode="open"):
        if self.filename:
            return os.path.split(self.filename)
509 510
        elif self.dirname:
            return self.dirname, ""
David Scherer's avatar
David Scherer committed
511 512 513
        else:
            try:
                pwd = os.getcwd()
514
            except OSError:
David Scherer's avatar
David Scherer committed
515 516 517 518 519 520
                pwd = ""
            return pwd, ""

    def asksavefile(self):
        dir, base = self.defaultfilename("save")
        if not self.savedialog:
521
            self.savedialog = tkFileDialog.SaveAs(
522
                    parent=self.text,
523 524
                    filetypes=self.filetypes,
                    defaultextension=self.defaultextension)
525 526
        filename = self.savedialog.show(initialdir=dir, initialfile=base)
        return filename
David Scherer's avatar
David Scherer committed
527

528
    def updaterecentfileslist(self,filename):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
529
        "Update recent file list on all editor windows"
530 531
        if self.editwin.flist:
            self.editwin.update_recent_files_list(filename)
532

533
def _io_binding(parent):  # htest #
534 535 536
    from tkinter import Toplevel, Text

    root = Toplevel(parent)
537
    root.title("Test IOBinding")
538 539
    x, y = map(int, parent.geometry().split('+')[1:])
    root.geometry("+%d+%d" % (x, y + 175))
David Scherer's avatar
David Scherer committed
540 541 542 543 544
    class MyEditWin:
        def __init__(self, text):
            self.text = text
            self.flist = None
            self.text.bind("<Control-o>", self.open)
545
            self.text.bind('<Control-p>', self.print)
David Scherer's avatar
David Scherer committed
546
            self.text.bind("<Control-s>", self.save)
547 548
            self.text.bind("<Alt-s>", self.saveas)
            self.text.bind('<Control-c>', self.savecopy)
David Scherer's avatar
David Scherer committed
549 550 551 552 553
        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>>")
554 555
        def print(self, event):
            self.text.event_generate("<<print-window>>")
David Scherer's avatar
David Scherer committed
556 557
        def save(self, event):
            self.text.event_generate("<<save-window>>")
558 559 560 561
        def saveas(self, event):
            self.text.event_generate("<<save-window-as-file>>")
        def savecopy(self, event):
            self.text.event_generate("<<save-copy-of-window-as-file>>")
562

David Scherer's avatar
David Scherer committed
563 564 565 566
    text = Text(root)
    text.pack()
    text.focus_set()
    editwin = MyEditWin(text)
567
    IOBinding(editwin)
David Scherer's avatar
David Scherer committed
568 569

if __name__ == "__main__":
570 571
    from unittest import main
    main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
572

573 574
    from idlelib.idle_test.htest import run
    run(_io_binding)