PageRenderTime 772ms CodeModel.GetById 171ms app.highlight 394ms RepoModel.GetById 117ms app.codeStats 0ms

/Lib/idlelib/IOBinding.py

http://unladen-swallow.googlecode.com/
Python | 594 lines | 497 code | 39 blank | 58 comment | 64 complexity | 8b7ba3bdd9c27d2640355f63d030127e MD5 | raw file
  1# changes by dscherer@cmu.edu
  2#   - IOBinding.open() replaces the current window with the opened file,
  3#     if the current window is both unmodified and unnamed
  4#   - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh
  5#     end-of-line conventions, instead of relying on the standard library,
  6#     which will only understand the local convention.
  7
  8import os
  9import types
 10import sys
 11import codecs
 12import tempfile
 13import tkFileDialog
 14import tkMessageBox
 15import re
 16from Tkinter import *
 17from SimpleDialog import SimpleDialog
 18
 19from configHandler import idleConf
 20
 21try:
 22    from codecs import BOM_UTF8
 23except ImportError:
 24    # only available since Python 2.3
 25    BOM_UTF8 = '\xef\xbb\xbf'
 26
 27# Try setting the locale, so that we can find out
 28# what encoding to use
 29try:
 30    import locale
 31    locale.setlocale(locale.LC_CTYPE, "")
 32except (ImportError, locale.Error):
 33    pass
 34
 35# Encoding for file names
 36filesystemencoding = sys.getfilesystemencoding()
 37
 38encoding = "ascii"
 39if sys.platform == 'win32':
 40    # On Windows, we could use "mbcs". However, to give the user
 41    # a portable encoding name, we need to find the code page
 42    try:
 43        encoding = locale.getdefaultlocale()[1]
 44        codecs.lookup(encoding)
 45    except LookupError:
 46        pass
 47else:
 48    try:
 49        # Different things can fail here: the locale module may not be
 50        # loaded, it may not offer nl_langinfo, or CODESET, or the
 51        # resulting codeset may be unknown to Python. We ignore all
 52        # these problems, falling back to ASCII
 53        encoding = locale.nl_langinfo(locale.CODESET)
 54        if encoding is None or encoding is '':
 55            # situation occurs on Mac OS X
 56            encoding = 'ascii'
 57        codecs.lookup(encoding)
 58    except (NameError, AttributeError, LookupError):
 59        # Try getdefaultlocale well: it parses environment variables,
 60        # which may give a clue. Unfortunately, getdefaultlocale has
 61        # bugs that can cause ValueError.
 62        try:
 63            encoding = locale.getdefaultlocale()[1]
 64            if encoding is None or encoding is '':
 65                # situation occurs on Mac OS X
 66                encoding = 'ascii'
 67            codecs.lookup(encoding)
 68        except (ValueError, LookupError):
 69            pass
 70
 71encoding = encoding.lower()
 72
 73coding_re = re.compile("coding[:=]\s*([-\w_.]+)")
 74
 75class EncodingMessage(SimpleDialog):
 76    "Inform user that an encoding declaration is needed."
 77    def __init__(self, master, enc):
 78        self.should_edit = False
 79
 80        self.root = top = Toplevel(master)
 81        top.bind("<Return>", self.return_event)
 82        top.bind("<Escape>", self.do_ok)
 83        top.protocol("WM_DELETE_WINDOW", self.wm_delete_window)
 84        top.wm_title("I/O Warning")
 85        top.wm_iconname("I/O Warning")
 86        self.top = top
 87
 88        l1 = Label(top,
 89            text="Non-ASCII found, yet no encoding declared. Add a line like")
 90        l1.pack(side=TOP, anchor=W)
 91        l2 = Entry(top, font="courier")
 92        l2.insert(0, "# -*- coding: %s -*-" % enc)
 93        # For some reason, the text is not selectable anymore if the
 94        # widget is disabled.
 95        # l2['state'] = DISABLED
 96        l2.pack(side=TOP, anchor = W, fill=X)
 97        l3 = Label(top, text="to your file\n"
 98                   "Choose OK to save this file as %s\n"
 99                   "Edit your general options to silence this warning" % enc)
100        l3.pack(side=TOP, anchor = W)
101
102        buttons = Frame(top)
103        buttons.pack(side=TOP, fill=X)
104        # Both return and cancel mean the same thing: do nothing
105        self.default = self.cancel = 0
106        b1 = Button(buttons, text="Ok", default="active",
107                    command=self.do_ok)
108        b1.pack(side=LEFT, fill=BOTH, expand=1)
109        b2 = Button(buttons, text="Edit my file",
110                    command=self.do_edit)
111        b2.pack(side=LEFT, fill=BOTH, expand=1)
112
113        self._set_transient(master)
114
115    def do_ok(self):
116        self.done(0)
117
118    def do_edit(self):
119        self.done(1)
120
121def coding_spec(str):
122    """Return the encoding declaration according to PEP 263.
123
124    Raise LookupError if the encoding is declared but unknown.
125    """
126    # Only consider the first two lines
127    str = str.split("\n")[:2]
128    str = "\n".join(str)
129
130    match = coding_re.search(str)
131    if not match:
132        return None
133    name = match.group(1)
134    # Check whether the encoding is known
135    import codecs
136    try:
137        codecs.lookup(name)
138    except LookupError:
139        # The standard encoding error does not indicate the encoding
140        raise LookupError, "Unknown encoding "+name
141    return name
142
143
144class IOBinding:
145
146    def __init__(self, editwin):
147        self.editwin = editwin
148        self.text = editwin.text
149        self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
150        self.__id_save = self.text.bind("<<save-window>>", self.save)
151        self.__id_saveas = self.text.bind("<<save-window-as-file>>",
152                                          self.save_as)
153        self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
154                                            self.save_a_copy)
155        self.fileencoding = None
156        self.__id_print = self.text.bind("<<print-window>>", self.print_window)
157
158    def close(self):
159        # Undo command bindings
160        self.text.unbind("<<open-window-from-file>>", self.__id_open)
161        self.text.unbind("<<save-window>>", self.__id_save)
162        self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
163        self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
164        self.text.unbind("<<print-window>>", self.__id_print)
165        # Break cycles
166        self.editwin = None
167        self.text = None
168        self.filename_change_hook = None
169
170    def get_saved(self):
171        return self.editwin.get_saved()
172
173    def set_saved(self, flag):
174        self.editwin.set_saved(flag)
175
176    def reset_undo(self):
177        self.editwin.reset_undo()
178
179    filename_change_hook = None
180
181    def set_filename_change_hook(self, hook):
182        self.filename_change_hook = hook
183
184    filename = None
185    dirname = None
186
187    def set_filename(self, filename):
188        if filename and os.path.isdir(filename):
189            self.filename = None
190            self.dirname = filename
191        else:
192            self.filename = filename
193            self.dirname = None
194            self.set_saved(1)
195            if self.filename_change_hook:
196                self.filename_change_hook()
197
198    def open(self, event=None, editFile=None):
199        if self.editwin.flist:
200            if not editFile:
201                filename = self.askopenfile()
202            else:
203                filename=editFile
204            if filename:
205                # If the current window has no filename and hasn't been
206                # modified, we replace its contents (no loss).  Otherwise
207                # we open a new window.  But we won't replace the
208                # shell window (which has an interp(reter) attribute), which
209                # gets set to "not modified" at every new prompt.
210                try:
211                    interp = self.editwin.interp
212                except AttributeError:
213                    interp = None
214                if not self.filename and self.get_saved() and not interp:
215                    self.editwin.flist.open(filename, self.loadfile)
216                else:
217                    self.editwin.flist.open(filename)
218            else:
219                self.text.focus_set()
220            return "break"
221        #
222        # Code for use outside IDLE:
223        if self.get_saved():
224            reply = self.maybesave()
225            if reply == "cancel":
226                self.text.focus_set()
227                return "break"
228        if not editFile:
229            filename = self.askopenfile()
230        else:
231            filename=editFile
232        if filename:
233            self.loadfile(filename)
234        else:
235            self.text.focus_set()
236        return "break"
237
238    eol = r"(\r\n)|\n|\r"  # \r\n (Windows), \n (UNIX), or \r (Mac)
239    eol_re = re.compile(eol)
240    eol_convention = os.linesep # Default
241
242    def loadfile(self, filename):
243        try:
244            # open the file in binary mode so that we can handle
245            #   end-of-line convention ourselves.
246            f = open(filename,'rb')
247            chars = f.read()
248            f.close()
249        except IOError, msg:
250            tkMessageBox.showerror("I/O Error", str(msg), master=self.text)
251            return False
252
253        chars = self.decode(chars)
254        # We now convert all end-of-lines to '\n's
255        firsteol = self.eol_re.search(chars)
256        if firsteol:
257            self.eol_convention = firsteol.group(0)
258            if isinstance(self.eol_convention, unicode):
259                # Make sure it is an ASCII string
260                self.eol_convention = self.eol_convention.encode("ascii")
261            chars = self.eol_re.sub(r"\n", chars)
262
263        self.text.delete("1.0", "end")
264        self.set_filename(None)
265        self.text.insert("1.0", chars)
266        self.reset_undo()
267        self.set_filename(filename)
268        self.text.mark_set("insert", "1.0")
269        self.text.see("insert")
270        self.updaterecentfileslist(filename)
271        return True
272
273    def decode(self, chars):
274        """Create a Unicode string
275
276        If that fails, let Tcl try its best
277        """
278        # Check presence of a UTF-8 signature first
279        if chars.startswith(BOM_UTF8):
280            try:
281                chars = chars[3:].decode("utf-8")
282            except UnicodeError:
283                # has UTF-8 signature, but fails to decode...
284                return chars
285            else:
286                # Indicates that this file originally had a BOM
287                self.fileencoding = BOM_UTF8
288                return chars
289        # Next look for coding specification
290        try:
291            enc = coding_spec(chars)
292        except LookupError, name:
293            tkMessageBox.showerror(
294                title="Error loading the file",
295                message="The encoding '%s' is not known to this Python "\
296                "installation. The file may not display correctly" % name,
297                master = self.text)
298            enc = None
299        if enc:
300            try:
301                return unicode(chars, enc)
302            except UnicodeError:
303                pass
304        # If it is ASCII, we need not to record anything
305        try:
306            return unicode(chars, 'ascii')
307        except UnicodeError:
308            pass
309        # Finally, try the locale's encoding. This is deprecated;
310        # the user should declare a non-ASCII encoding
311        try:
312            chars = unicode(chars, encoding)
313            self.fileencoding = encoding
314        except UnicodeError:
315            pass
316        return chars
317
318    def maybesave(self):
319        if self.get_saved():
320            return "yes"
321        message = "Do you want to save %s before closing?" % (
322            self.filename or "this untitled document")
323        m = tkMessageBox.Message(
324            title="Save On Close",
325            message=message,
326            icon=tkMessageBox.QUESTION,
327            type=tkMessageBox.YESNOCANCEL,
328            master=self.text)
329        reply = m.show()
330        if reply == "yes":
331            self.save(None)
332            if not self.get_saved():
333                reply = "cancel"
334        self.text.focus_set()
335        return reply
336
337    def save(self, event):
338        if not self.filename:
339            self.save_as(event)
340        else:
341            if self.writefile(self.filename):
342                self.set_saved(1)
343                try:
344                    self.editwin.store_file_breaks()
345                except AttributeError:  # may be a PyShell
346                    pass
347        self.text.focus_set()
348        return "break"
349
350    def save_as(self, event):
351        filename = self.asksavefile()
352        if filename:
353            if self.writefile(filename):
354                self.set_filename(filename)
355                self.set_saved(1)
356                try:
357                    self.editwin.store_file_breaks()
358                except AttributeError:
359                    pass
360        self.text.focus_set()
361        self.updaterecentfileslist(filename)
362        return "break"
363
364    def save_a_copy(self, event):
365        filename = self.asksavefile()
366        if filename:
367            self.writefile(filename)
368        self.text.focus_set()
369        self.updaterecentfileslist(filename)
370        return "break"
371
372    def writefile(self, filename):
373        self.fixlastline()
374        chars = self.encode(self.text.get("1.0", "end-1c"))
375        if self.eol_convention != "\n":
376            chars = chars.replace("\n", self.eol_convention)
377        try:
378            f = open(filename, "wb")
379            f.write(chars)
380            f.flush()
381            f.close()
382            return True
383        except IOError, msg:
384            tkMessageBox.showerror("I/O Error", str(msg),
385                                   master=self.text)
386            return False
387
388    def encode(self, chars):
389        if isinstance(chars, types.StringType):
390            # This is either plain ASCII, or Tk was returning mixed-encoding
391            # text to us. Don't try to guess further.
392            return chars
393        # See whether there is anything non-ASCII in it.
394        # If not, no need to figure out the encoding.
395        try:
396            return chars.encode('ascii')
397        except UnicodeError:
398            pass
399        # If there is an encoding declared, try this first.
400        try:
401            enc = coding_spec(chars)
402            failed = None
403        except LookupError, msg:
404            failed = msg
405            enc = None
406        if enc:
407            try:
408                return chars.encode(enc)
409            except UnicodeError:
410                failed = "Invalid encoding '%s'" % enc
411        if failed:
412            tkMessageBox.showerror(
413                "I/O Error",
414                "%s. Saving as UTF-8" % failed,
415                master = self.text)
416        # If there was a UTF-8 signature, use that. This should not fail
417        if self.fileencoding == BOM_UTF8 or failed:
418            return BOM_UTF8 + chars.encode("utf-8")
419        # Try the original file encoding next, if any
420        if self.fileencoding:
421            try:
422                return chars.encode(self.fileencoding)
423            except UnicodeError:
424                tkMessageBox.showerror(
425                    "I/O Error",
426                    "Cannot save this as '%s' anymore. Saving as UTF-8" \
427                    % self.fileencoding,
428                    master = self.text)
429                return BOM_UTF8 + chars.encode("utf-8")
430        # Nothing was declared, and we had not determined an encoding
431        # on loading. Recommend an encoding line.
432        config_encoding = idleConf.GetOption("main","EditorWindow",
433                                             "encoding")
434        if config_encoding == 'utf-8':
435            # User has requested that we save files as UTF-8
436            return BOM_UTF8 + chars.encode("utf-8")
437        ask_user = True
438        try:
439            chars = chars.encode(encoding)
440            enc = encoding
441            if config_encoding == 'locale':
442                ask_user = False
443        except UnicodeError:
444            chars = BOM_UTF8 + chars.encode("utf-8")
445            enc = "utf-8"
446        if not ask_user:
447            return chars
448        dialog = EncodingMessage(self.editwin.top, enc)
449        dialog.go()
450        if dialog.num == 1:
451            # User asked us to edit the file
452            encline = "# -*- coding: %s -*-\n" % enc
453            firstline = self.text.get("1.0", "2.0")
454            if firstline.startswith("#!"):
455                # Insert encoding after #! line
456                self.text.insert("2.0", encline)
457            else:
458                self.text.insert("1.0", encline)
459            return self.encode(self.text.get("1.0", "end-1c"))
460        return chars
461
462    def fixlastline(self):
463        c = self.text.get("end-2c")
464        if c != '\n':
465            self.text.insert("end-1c", "\n")
466
467    def print_window(self, event):
468        m = tkMessageBox.Message(
469            title="Print",
470            message="Print to Default Printer",
471            icon=tkMessageBox.QUESTION,
472            type=tkMessageBox.OKCANCEL,
473            default=tkMessageBox.OK,
474            master=self.text)
475        reply = m.show()
476        if reply != tkMessageBox.OK:
477            self.text.focus_set()
478            return "break"
479        tempfilename = None
480        saved = self.get_saved()
481        if saved:
482            filename = self.filename
483        # shell undo is reset after every prompt, looks saved, probably isn't
484        if not saved or filename is None:
485            (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
486            filename = tempfilename
487            os.close(tfd)
488            if not self.writefile(tempfilename):
489                os.unlink(tempfilename)
490                return "break"
491        platform=os.name
492        printPlatform=1
493        if platform == 'posix': #posix platform
494            command = idleConf.GetOption('main','General',
495                                         'print-command-posix')
496            command = command + " 2>&1"
497        elif platform == 'nt': #win32 platform
498            command = idleConf.GetOption('main','General','print-command-win')
499        else: #no printing for this platform
500            printPlatform=0
501        if printPlatform:  #we can try to print for this platform
502            command = command % filename
503            pipe = os.popen(command, "r")
504            # things can get ugly on NT if there is no printer available.
505            output = pipe.read().strip()
506            status = pipe.close()
507            if status:
508                output = "Printing failed (exit status 0x%x)\n" % \
509                         status + output
510            if output:
511                output = "Printing command: %s\n" % repr(command) + output
512                tkMessageBox.showerror("Print status", output, master=self.text)
513        else:  #no printing for this platform
514            message="Printing is not enabled for this platform: %s" % platform
515            tkMessageBox.showinfo("Print status", message, master=self.text)
516        if tempfilename:
517            os.unlink(tempfilename)
518        return "break"
519
520    opendialog = None
521    savedialog = None
522
523    filetypes = [
524        ("Python and text files", "*.py *.pyw *.txt", "TEXT"),
525        ("All text files", "*", "TEXT"),
526        ("All files", "*"),
527        ]
528
529    def askopenfile(self):
530        dir, base = self.defaultfilename("open")
531        if not self.opendialog:
532            self.opendialog = tkFileDialog.Open(master=self.text,
533                                                filetypes=self.filetypes)
534        filename = self.opendialog.show(initialdir=dir, initialfile=base)
535        if isinstance(filename, unicode):
536            filename = filename.encode(filesystemencoding)
537        return filename
538
539    def defaultfilename(self, mode="open"):
540        if self.filename:
541            return os.path.split(self.filename)
542        elif self.dirname:
543            return self.dirname, ""
544        else:
545            try:
546                pwd = os.getcwd()
547            except os.error:
548                pwd = ""
549            return pwd, ""
550
551    def asksavefile(self):
552        dir, base = self.defaultfilename("save")
553        if not self.savedialog:
554            self.savedialog = tkFileDialog.SaveAs(master=self.text,
555                                                  filetypes=self.filetypes)
556        filename = self.savedialog.show(initialdir=dir, initialfile=base)
557        if isinstance(filename, unicode):
558            filename = filename.encode(filesystemencoding)
559        return filename
560
561    def updaterecentfileslist(self,filename):
562        "Update recent file list on all editor windows"
563        self.editwin.update_recent_files_list(filename)
564
565def test():
566    root = Tk()
567    class MyEditWin:
568        def __init__(self, text):
569            self.text = text
570            self.flist = None
571            self.text.bind("<Control-o>", self.open)
572            self.text.bind("<Control-s>", self.save)
573            self.text.bind("<Alt-s>", self.save_as)
574            self.text.bind("<Alt-z>", self.save_a_copy)
575        def get_saved(self): return 0
576        def set_saved(self, flag): pass
577        def reset_undo(self): pass
578        def open(self, event):
579            self.text.event_generate("<<open-window-from-file>>")
580        def save(self, event):
581            self.text.event_generate("<<save-window>>")
582        def save_as(self, event):
583            self.text.event_generate("<<save-window-as-file>>")
584        def save_a_copy(self, event):
585            self.text.event_generate("<<save-copy-of-window-as-file>>")
586    text = Text(root)
587    text.pack()
588    text.focus_set()
589    editwin = MyEditWin(text)
590    io = IOBinding(editwin)
591    root.mainloop()
592
593if __name__ == "__main__":
594    test()