Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.python > #77407 > unrolled thread

Editing text with an external editor in Python

Started bySteven D'Aprano <steve+comp.lang.python@pearwood.info>
First post2014-09-02 02:11 +1000
Last post2014-09-01 19:24 -0700
Articles 18 — 9 participants

Back to article view | Back to comp.lang.python


Contents

  Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 02:11 +1000
    Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 02:35 +1000
      Re: Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 04:23 +1000
        Re: Editing text with an external editor in Python Tim Chase <python.list@tim.thechases.com> - 2014-09-01 15:06 -0500
          Re: Editing text with an external editor in Python alister <alister.nospam.ware@ntlworld.com> - 2014-09-02 08:35 +0000
            Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 18:45 +1000
              Re: Editing text with an external editor in Python alister <alister.nospam.ware@ntlworld.com> - 2014-09-03 08:06 +0000
            Re: Editing text with an external editor in Python Terry Reedy <tjreedy@udel.edu> - 2014-09-02 17:14 -0400
            Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-03 07:36 +1000
            Re: Editing text with an external editor in Python Terry Reedy <tjreedy@udel.edu> - 2014-09-02 21:49 -0400
            Re: Editing text with an external editor in Python Zachary Ware <zachary.ware+pylist@gmail.com> - 2014-09-02 22:03 -0500
        Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 08:30 +1000
    Re: Editing text with an external editor in Python Roy Smith <roy@panix.com> - 2014-09-01 13:06 -0400
      Re: Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 04:02 +1000
        Re: Editing text with an external editor in Python Cameron Simpson <cs@zip.com.au> - 2014-09-02 08:14 +1000
          Re: Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 13:18 +1000
        Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 08:25 +1000
    Re: Editing text with an external editor in Python gschemenauer3@gmail.com - 2014-09-01 19:24 -0700

#77407 — Editing text with an external editor in Python

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2014-09-02 02:11 +1000
SubjectEditing text with an external editor in Python
Message-ID<54049ab7$0$29972$c3e8da3$5496439d@news.astraweb.com>
Python's input() or raw_input() function is good for getting a single line
of text from the user. But what if you want a more substantial chunk of
text from the user? Here's how to call out to an external editor such as
ed, nano, vim, emacs, and even GUI text editors:

import tempfile

def edit(editor, content=''):
    f = tempfile.NamedTemporaryFile(mode='w+')
    if content:
        f.write(content)
        f.flush()
    command = editor + " " + f.name
    status = os.system(command)
    f.seek(0, 0)
    text = f.read()
    f.close()
    assert not os.path.exists(f.name)
    return (status, text)


Anyone able to test it on Windows for me please?


More here: 

https://code.activestate.com/recipes/578926/


-- 
Steven

[toc] | [next] | [standalone]


#77408

FromChris Angelico <rosuav@gmail.com>
Date2014-09-02 02:35 +1000
Message-ID<mailman.13692.1409589319.18130.python-list@python.org>
In reply to#77407
On Tue, Sep 2, 2014 at 2:11 AM, Steven D'Aprano
<steve+comp.lang.python@pearwood.info> wrote:
> Anyone able to test it on Windows for me please?
>

Seems to partially work. I added an 'import os' at the top, and a
simple test call to the function, and it did give me my editor (nano)
and retrieved the text. It did give a warning, though:

----
C:\>Python34\python 123123123.py
cygwin warning:
  MS-DOS style path detected: C:\DOCUME~1\M\LOCALS~1\Temp\tmp94rcwd57
  Preferred POSIX equivalent is: /DOCUME~1/M/LOCALS~1/Temp/tmp94rcwd57
  CYGWIN environment variable option "nodosfilewarning" turns off this warning.
  Consult the user's guide for more details about POSIX paths:
    http://cygwin.com/cygwin-ug-net/using.html#using-pathnames
Your text is:
asdf
Hello, world!
----

Windows doesn't have a nice $EDITOR environment variable to call on,
so I'm not sure what the best way to actually choose an editor is. I
would hope that you can make it externally configurable (I've done
some very weird things with changed editors, like one that connects to
a TCP socket, alerts a server with its arguments, and then SIGSTOPs
itself, and the server sends the file's contents to another
socket-connected client that has a human at the other end, and when it
gets back a response from that client, it rewrites the file and
SIGCONTs the 'editor', which then terminates - so to all intents and
purposes, it's as if that program really did edit the file), but doing
that on Windows may not be easy.

You'll also have to cope with some other possibilities. What happens
if someone tries Notepad? (Don't try this at home. We are experts and
are testing on a closed track. Do not use Notepad unless you, too,
have thirty years of special effects experience.) Turns out it doesn't
like working with a file that another process has open. Nor can SciTE;
it shows an empty file on load, and then is unable to save. I suspect
the comment here is what's biting you:

https://docs.python.org/3.5/library/tempfile.html#tempfile.NamedTemporaryFile
"""Whether the name can be used to open the file a second time, while
the named temporary file is still open, varies across platforms (it
can be so used on Unix; it cannot on Windows NT or later)."""

So it works fairly nicely as long as you're using a Cygwin editor.
Otherwise, not so much. :(

ChrisA

[toc] | [prev] | [next] | [standalone]


#77411

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2014-09-02 04:23 +1000
Message-ID<5404b987$0$30001$c3e8da3$5496439d@news.astraweb.com>
In reply to#77408
Chris Angelico wrote:

> On Tue, Sep 2, 2014 at 2:11 AM, Steven D'Aprano
> <steve+comp.lang.python@pearwood.info> wrote:
>> Anyone able to test it on Windows for me please?
>>
> 
> Seems to partially work. I added an 'import os' at the top, and a
> simple test call to the function, and it did give me my editor (nano)
> and retrieved the text. It did give a warning, though:
> 
> ----
> C:\>Python34\python 123123123.py
> cygwin warning:
>   MS-DOS style path detected: C:\DOCUME~1\M\LOCALS~1\Temp\tmp94rcwd57
>   Preferred POSIX equivalent is: /DOCUME~1/M/LOCALS~1/Temp/tmp94rcwd57

That's arguably a Python bug. Under Cygwin, it should use POSIX paths rather
than Windows paths.

I believe that sys.platform tells you if you are running under Cygwin.


> Windows doesn't have a nice $EDITOR environment variable to call on,

Why not? It's your environment, you can create any environment variable you
like, even under Windows, right?


> so I'm not sure what the best way to actually choose an editor is. I
> would hope that you can make it externally configurable 

Read $VISUAL, if it exists, otherwise $EDITOR, if it exists, otherwise fall
back on something hard coded. Or read it from an ini file. Or create an
entry in the register. Whatever. That's up to the application which uses
this function, not the function itself.

Under XP and older the standard DOS editor is EDIT, but that's gone from
Windows 7. Believe it or not, I understand that you can use:

    copy con [filename.???]

to do basic line editing, which is terrifying, but I believe it works.

http://stackoverflow.com/a/22000756

And of course, you can install whatever third-party editors you like. There
are Windows ports of nano, vim and emacs.

But fundamentally, the de facto "standard editor" on Windows is Notepad.


> You'll also have to cope with some other possibilities. What happens
> if someone tries Notepad? (Don't try this at home. We are experts and
> are testing on a closed track. Do not use Notepad unless you, too,
> have thirty years of special effects experience.) Turns out it doesn't
> like working with a file that another process has open. 

Ah, I feared that would be the case. I'll have to think about a way around
that. It won't be as neat, or as secure, but it should be doable.



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#77414

FromTim Chase <python.list@tim.thechases.com>
Date2014-09-01 15:06 -0500
Message-ID<mailman.13694.1409602073.18130.python-list@python.org>
In reply to#77411
On 2014-09-02 04:23, Steven D'Aprano wrote:
> Read $VISUAL, if it exists, otherwise $EDITOR, if it exists,
> otherwise fall back on something hard coded. Or read it from an ini
> file. Or create an entry in the register. Whatever. That's up to
> the application which uses this function, not the function itself.
> 
> Under XP and older the standard DOS editor is EDIT, but that's gone
> from Windows 7. Believe it or not, I understand that you can use:
> 
>     copy con [filename.???]
> 
> to do basic line editing, which is terrifying, but I believe it
> works.

And according to [1], the venerable edlin is still available on Win8,
even if you don't have edit.  Though according to [2], it sounds like
MS-EDIT is still available on at least the 32-bit version of Win8
(it doesn't detail whether it comes out of the box on 64-bit Win8).

I don't have Win8, so I can't corroborate either application.

-tkc

[1] http://en.wikipedia.org/wiki/Edlin#History
[2] http://en.wikipedia.org/wiki/MS-DOS_Editor




[toc] | [prev] | [next] | [standalone]


#77437

Fromalister <alister.nospam.ware@ntlworld.com>
Date2014-09-02 08:35 +0000
Message-ID<zjfNv.198388$rQ.139558@fx13.am4>
In reply to#77414
On Mon, 01 Sep 2014 15:06:04 -0500, Tim Chase wrote:

> On 2014-09-02 04:23, Steven D'Aprano wrote:
>> Read $VISUAL, if it exists, otherwise $EDITOR, if it exists, otherwise
>> fall back on something hard coded. Or read it from an ini file. Or
>> create an entry in the register. Whatever. That's up to the application
>> which uses this function, not the function itself.
>> 
>> Under XP and older the standard DOS editor is EDIT, but that's gone
>> from Windows 7. Believe it or not, I understand that you can use:
>> 
>>     copy con [filename.???]
>> 
>> to do basic line editing, which is terrifying, but I believe it works.
> 
> And according to [1], the venerable edlin is still available on Win8,
> even if you don't have edit.  Though according to [2], it sounds like
> MS-EDIT is still available on at least the 32-bit version of Win8 (it
> doesn't detail whether it comes out of the box on 64-bit Win8).
> 
> I don't have Win8, so I can't corroborate either application.
> 
> -tkc
> 
> [1] http://en.wikipedia.org/wiki/Edlin#History [2]
> http://en.wikipedia.org/wiki/MS-DOS_Editor

if edlin is your only option then it would be better to spend you time 
writhing your own text editor!
Neither edlin or ms-edit are available on my Win 7 installation so I 
doubt that it has been resurrected for win 8



-- 
Elbonics, n.:
	The actions of two people maneuvering for one armrest in a movie
	theatre.
		-- "Sniglets", Rich Hall & Friends

[toc] | [prev] | [next] | [standalone]


#77438

FromChris Angelico <rosuav@gmail.com>
Date2014-09-02 18:45 +1000
Message-ID<mailman.13710.1409647561.18130.python-list@python.org>
In reply to#77437
On Tue, Sep 2, 2014 at 6:35 PM, alister
<alister.nospam.ware@ntlworld.com> wrote:
> if edlin is your only option then it would be better to spend you time
> writhing your own text editor!

Heh!

Considering how easy it is to deploy a multi-line edit widget in any
GUI toolkit, it shouldn't be too hard to write a GUI text editor. Now,
writing a *good* GUI text editor, that's a bit harder. And of course,
writing a text/console text editor, that's also less simple. But I put
it to you: How do you write your editor up to the point where you can
use it to write your editor? We have a bootstrapping problem!

ChrisA

[toc] | [prev] | [next] | [standalone]


#77473

Fromalister <alister.nospam.ware@ntlworld.com>
Date2014-09-03 08:06 +0000
Message-ID<1_zNv.229859$an2.171403@fx09.am4>
In reply to#77438
On Tue, 02 Sep 2014 18:45:54 +1000, Chris Angelico wrote:

> On Tue, Sep 2, 2014 at 6:35 PM, alister
> <alister.nospam.ware@ntlworld.com> wrote:
>> if edlin is your only option then it would be better to spend you time
>> writhing your own text editor!
> 
> Heh!
> 
> Considering how easy it is to deploy a multi-line edit widget in any GUI
> toolkit, it shouldn't be too hard to write a GUI text editor. Now,
> writing a *good* GUI text editor, that's a bit harder. And of course,
> writing a text/console text editor, that's also less simple. But I put
> it to you: How do you write your editor up to the point where you can
> use it to write your editor? We have a bootstrapping problem!
> 
> ChrisA

fortunately i have a proper operating system now so I don't have have to 
use edlin. in the unlikely event that a reasonably featured editor is not 
available I can always use cat :-)



-- 
Nothing is finished until the paperwork is done.

[toc] | [prev] | [next] | [standalone]


#77455

FromTerry Reedy <tjreedy@udel.edu>
Date2014-09-02 17:14 -0400
Message-ID<mailman.13719.1409692486.18130.python-list@python.org>
In reply to#77437
On 9/2/2014 4:45 AM, Chris Angelico wrote:
> On Tue, Sep 2, 2014 at 6:35 PM, alister
> <alister.nospam.ware@ntlworld.com> wrote:
>> if edlin is your only option then it would be better to spend you time
>> writhing your own text editor!
>
> Heh!
>
> Considering how easy it is to deploy a multi-line edit widget in any
> GUI toolkit, it shouldn't be too hard to write a GUI text editor.

Most Python installations have tkinter available. I quickly wrote an 
absolutely minimal script.

import tkinter as tk
root = tk.Tk()
text = tk.Text()
text.pack()
root.mainloop()

I tested tested the functions and wrote the following.

This is a test text entry.
Enter and Tab work as expected.
The Arrow (Cursor) keys work as expected.
CntL-Left and Cntl-Right move a word at time.
Home and End move to beginning and end of the line.
Cntl-Home and Cntl-Up move to the beginning of the text.
Cntl-End and Cntl-Donw move to the end of the text.
Shift + cursor movement selects between the begin and end slice positions.
PageUp and PageDown are inoperative.
Delete and Backspace work as expected.
At least on Windows, I can select text and delete,
or cut or copy to Clipboard.
I can also paste from the clipboard.
In otherwords, this is a functional minimal text entry widget.

I did not even know about Shift-movement selecting until I tried it.
Notepad has this. Thunderbird's text entry does not.

I think the above is adequate for most multi-line text entry.
In use, a save function would have to be added.
A help text with the above info would be good too (Idle needs this).

-- 
Terry Jan Reedy

[toc] | [prev] | [next] | [standalone]


#77456

FromChris Angelico <rosuav@gmail.com>
Date2014-09-03 07:36 +1000
Message-ID<mailman.13720.1409693783.18130.python-list@python.org>
In reply to#77437
On Wed, Sep 3, 2014 at 7:14 AM, Terry Reedy <tjreedy@udel.edu> wrote:
> import tkinter as tk
> root = tk.Tk()
> text = tk.Text()
> text.pack()
> root.mainloop()
>
> I tested tested the functions and wrote the following.
>
> This is a test text entry.
> Enter and Tab work as expected.
> The Arrow (Cursor) keys work as expected.
> CntL-Left and Cntl-Right move a word at time.
> Home and End move to beginning and end of the line.
> Cntl-Home and Cntl-Up move to the beginning of the text.
> Cntl-End and Cntl-Donw move to the end of the text.
> Shift + cursor movement selects between the begin and end slice positions.
> PageUp and PageDown are inoperative.
> Delete and Backspace work as expected.
> At least on Windows, I can select text and delete,
> or cut or copy to Clipboard.
> I can also paste from the clipboard.
> In otherwords, this is a functional minimal text entry widget.

Err, that's not all it takes to run an editor :) File opening and
saving would be kinda helpful, for a start...

But that's what I meant when I said that a multi-line entry field is
an extremely standard widget. (I'm a bit surprised that PgUp/PgDn
don't work, but the rest of what you say is perfectly standard.) It's
a bit harder to do an editor that doesn't just call on a GUI, and more
importantly, "tk.Text().pack()" does not make the world's best editor.
But it sure is handy when you want to embed an editor in something
else!

ChrisA

[toc] | [prev] | [next] | [standalone]


#77461

FromTerry Reedy <tjreedy@udel.edu>
Date2014-09-02 21:49 -0400
Message-ID<mailman.13722.1409708996.18130.python-list@python.org>
In reply to#77437
On 9/2/2014 5:36 PM, Chris Angelico wrote:
> On Wed, Sep 3, 2014 at 7:14 AM, Terry Reedy <tjreedy@udel.edu> wrote:
>> import tkinter as tk
>> root = tk.Tk()
>> text = tk.Text()
>> text.pack()
>> root.mainloop()
>>
>> I tested tested the functions and wrote the following.
>>
>> This is a test text entry.
>> Enter and Tab work as expected.
>> The Arrow (Cursor) keys work as expected.
>> CntL-Left and Cntl-Right move a word at time.
>> Home and End move to beginning and end of the line.
>> Cntl-Home and Cntl-Up move to the beginning of the text.
>> Cntl-End and Cntl-Donw move to the end of the text.
>> Shift + cursor movement selects between the begin and end slice positions.
>> PageUp and PageDown are inoperative.

My mistake. See below.

>> Delete and Backspace work as expected.
>> At least on Windows, I can select text and delete,
>> or cut or copy to Clipboard.
>> I can also paste from the clipboard.
>> In otherwords, this is a functional minimal text entry widget.

'Minimal' mean minimal, as in the minimal number of lines of code to 
actually work (4), as a the minimum needed to prove the concept, and a 
starting point for adding more things.

> Err, that's not all it takes to run an editor :) File opening

The opening premise of the thread was something like "Input() is good 
for getting a single line of text input from a user. What about getting 
multiple lines of text?"  Opening a non-empty existing file is not 
relevant to the purpose of extending input().  I intentionally said 
'text entry widget', not 'editor'.  For editing a particular existing 
text, the app might pre-load the widget with that text.

> saving would be kinda helpful, for a start...

It is really annoying when someone clips (erases) something I said and 
then says that I should have said what I did say.  Here it is again!

 >> In use, a save function would have to be added.

The function does not have to be attached to menu widget.  When one 
imports a patch into a mercurial repository, hg offers the opportunity 
to enter a multi-line commit message. Just the scenario this thread 
started with. It opens a text entry window something like tkinter.Text. 
It is preloaded with something like the following.

-----------------------------------X|
|                                   |
|===================================|
|Enter commit message above the line|
|or leave blank to abort commit.    |
|Close window when done.            |
-------------------------------------

Hg intercepts the window close event to grab the entered text, if any, 
before it disappears.

> But that's what I meant when I said that a multi-line entry field is
> an extremely standard widget. (I'm a bit surprised that PgUp/PgDn
> don't work,

My mistake. They do when I add more lines than fit in the window.

-- 
Terry Jan Reedy

[toc] | [prev] | [next] | [standalone]


#77463

FromZachary Ware <zachary.ware+pylist@gmail.com>
Date2014-09-02 22:03 -0500
Message-ID<mailman.13724.1409713432.18130.python-list@python.org>
In reply to#77437
On Tue, Sep 2, 2014 at 3:45 AM, Chris Angelico <rosuav@gmail.com> wrote:
> Considering how easy it is to deploy a multi-line edit widget in any
> GUI toolkit, it shouldn't be too hard to write a GUI text editor.

Coincidentally, I was annoyed enough to write the following program
sometime last week.  I was sick of messing with Notepad or Notepad++
as the git editor (and don't feel like learning vim just to write a
commit message), so I took 10 minutes to write this, packaged it up
with py2exe and pointed git at it.  Works like a charm for me!

#!C:/Python34/python.exe
"""Simple commit message tool.

Dead simple tkinter GUI for writing commit messages for git without messing
around with Notepad's line ending stupidity or Notepad++ being open or not.
"""

import os
import sys
import tkinter as tk

class Committer:
    def __init__(self, root, filename):
        self.root = root
        self.root.protocol('WM_DELETE_WINDOW', self.destroy)
        self.filename = filename
        self.text = tk.Text(root)
        self.text['width'] = 80
        self.text['height'] = 25
        self.text.pack()
        self.text.focus()
        with open(self.filename) as f:
            self.text.insert('end', f.read())
            self.text.mark_set("insert", "1.0")

    def destroy(self):
        message = self.text.get('1.0', 'end')
        with open(self.filename, 'w') as f:
            f.write(message)
        self.root.destroy()

def main():
    root = tk.Tk()
    assert os.path.exists(sys.argv[1]), sys.argv[1]
    committer = Committer(root, sys.argv[1])
    root.mainloop()

if __name__ == '__main__':
    main()

-- 
Zach

[toc] | [prev] | [next] | [standalone]


#77418

FromChris Angelico <rosuav@gmail.com>
Date2014-09-02 08:30 +1000
Message-ID<mailman.13698.1409610612.18130.python-list@python.org>
In reply to#77411
On Tue, Sep 2, 2014 at 4:23 AM, Steven D'Aprano
<steve+comp.lang.python@pearwood.info> wrote:
> Chris Angelico wrote:
>> C:\>Python34\python 123123123.py
>> cygwin warning:
>>   MS-DOS style path detected: C:\DOCUME~1\M\LOCALS~1\Temp\tmp94rcwd57
>>   Preferred POSIX equivalent is: /DOCUME~1/M/LOCALS~1/Temp/tmp94rcwd57
>
> That's arguably a Python bug. Under Cygwin, it should use POSIX paths rather
> than Windows paths.

Except that I wasn't; I ran Python 3.4 that was installed via the .msi
package, and from that Python ran nano that was presumably compiled
for Cygwin.

>> Windows doesn't have a nice $EDITOR environment variable to call on,
>
> Why not? It's your environment, you can create any environment variable you
> like, even under Windows, right?

Sure, but what I mean is, there's a general convention on Unix that
setting EDITOR will do that. You don't get to take advantage of
expectations that easily on Windows.

> But fundamentally, the de facto "standard editor" on Windows is Notepad.

Sadly so. Which is why I tried it...

>> You'll also have to cope with some other possibilities. What happens
>> if someone tries Notepad? (Don't try this at home. We are experts and
>> are testing on a closed track. Do not use Notepad unless you, too,
>> have thirty years of special effects experience.) Turns out it doesn't
>> like working with a file that another process has open.
>
> Ah, I feared that would be the case. I'll have to think about a way around
> that. It won't be as neat, or as secure, but it should be doable.

... and yeah. That's the problem.

ChrisA

[toc] | [prev] | [next] | [standalone]


#77409

FromRoy Smith <roy@panix.com>
Date2014-09-01 13:06 -0400
Message-ID<roy-FD5EAD.13055101092014@news.panix.com>
In reply to#77407
In article <54049ab7$0$29972$c3e8da3$5496439d@news.astraweb.com>,
 Steven D'Aprano <steve+comp.lang.python@pearwood.info> wrote:

> import tempfile
> 
> def edit(editor, content=''):
>     f = tempfile.NamedTemporaryFile(mode='w+')
>     [...]
>     command = editor + " " + f.name
>     status = os.system(command)

Hmmm.  Didn't we just have a thread about passing external data to 
shells?

$ mkdir '/tmp/;rm -rf;'
$ TMPDIR='/tmp/;rm -rf;' python
Python 2.7.3 (default, Sep 26 2013, 20:03:06) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import tempfile
>>> f = tempfile.NamedTemporaryFile()
>>> f.name
'/tmp/;rm -rf;/tmpW8HFTr'
>>>

[toc] | [prev] | [next] | [standalone]


#77410

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2014-09-02 04:02 +1000
Message-ID<5404b4b5$0$29976$c3e8da3$5496439d@news.astraweb.com>
In reply to#77409
Roy Smith wrote:

> Hmmm.  Didn't we just have a thread about passing external data to
> shells?
> 
> $ mkdir '/tmp/;rm -rf;'
> $ TMPDIR='/tmp/;rm -rf;' python
> Python 2.7.3 (default, Sep 26 2013, 20:03:06)
> [GCC 4.6.3] on linux2
> Type "help", "copyright", "credits" or "license" for more information.
>>>> import tempfile
>>>> f = tempfile.NamedTemporaryFile()
>>>> f.name
> '/tmp/;rm -rf;/tmpW8HFTr'

Seems like a lot of trouble to go to to erase your own system. Couldn't you
just run rm -rf / on your own system prior to launching Python?

But seriously, I'm not sure what attack vector you think you have found.
By definition, this is calling out to an external application, which might
do *anything*. It needs to be used in a trusted environment, like any other
tool which calls out to external applications.

[steve@ando ~]$ mkdir foo
[steve@ando ~]$ cd foo
[steve@ando foo]$ git init
Initialized empty Git repository in /home/steve/foo/.git/
[steve@ando foo]$ echo Some content > stuff.txt
[steve@ando foo]$ git add stuff.txt
[steve@ando foo]$ GIT_EDITOR="echo 'you got pwned' #" git commit
you got pwned
Aborting commit due to empty commit message.


I'm not really seeing how this is a security vulnerability. If somebody can
break into my system and set a hostile GIT_EDITOR, or TMPDIR, environment
variables, I've already lost.

As written, the edit() function takes two arguments: the name of an
application to call, and optionally initial contents to be edited.
Obviously the name of the application has to be trusted: either hard code
it, or get it from a trusted source like the VISUAL or EDITOR environment
variables. (It's *your* environment, you can set them to whatever editor
you want. If you want to set them to something hostile, that's your
prerogative.)

If an attacker has already compromised my editor, I've lost.

If I naively run arbitrary code provided by *untrusted* sources (say, I get
the editor from anonymous users over the internet), I've lost. I didn't
think I needed to explicitly say that.

On the other hand, the initial content need not be trusted, since it's just
text. The worst somebody could do is hurt my feelings. (Well, they could in
principle buffer-overflow the editor, or Python, but if you can't trust
Python and your editor to be resistant to that, you shouldn't use them.) If
the initial content could "leak" out and do bad things, that would be a
vulnerability I care about. Having the user destroy their own system by
deliberately misusing the function is not my problem:

# Don't do this. It would be bad.
edit("rm -rf / #")


Have I missed something? I really don't think this is a vulnerability, and I
don't see how using the subprocess module would make it safer.


-- 
Steven

[toc] | [prev] | [next] | [standalone]


#77416

FromCameron Simpson <cs@zip.com.au>
Date2014-09-02 08:14 +1000
Message-ID<mailman.13696.1409609665.18130.python-list@python.org>
In reply to#77410
On 02Sep2014 04:02, Steven D'Aprano <steve+comp.lang.python@pearwood.info> wrote:
>Roy Smith wrote:
>> Hmmm.  Didn't we just have a thread about passing external data to
>> shells?
>>
>> $ mkdir '/tmp/;rm -rf;'
>> $ TMPDIR='/tmp/;rm -rf;' python
>> Python 2.7.3 (default, Sep 26 2013, 20:03:06)
>> [GCC 4.6.3] on linux2
>> Type "help", "copyright", "credits" or "license" for more information.
>>>>> import tempfile
>>>>> f = tempfile.NamedTemporaryFile()
>>>>> f.name
>> '/tmp/;rm -rf;/tmpW8HFTr'
>
>Seems like a lot of trouble to go to to erase your own system. Couldn't you
>just run rm -rf / on your own system prior to launching Python?
>
>But seriously, I'm not sure what attack vector you think you have found.
>By definition, this is calling out to an external application, which might
>do *anything*. It needs to be used in a trusted environment, like any other
>tool which calls out to external applications.
[...]
>I'm not really seeing how this is a security vulnerability. If somebody can
>break into my system and set a hostile GIT_EDITOR, or TMPDIR, environment
>variables, I've already lost.
[...]
>Have I missed something? I really don't think this is a vulnerability, and I
>don't see how using the subprocess module would make it safer.

It is not just about being hacked.

It is about being robust in the face of unusual setups.

If I were producing this function for general use (even my own personal general 
use) it would need to be reliable. That includes things like $TMPDIR having 
spaces in it (or other unfortunate punctuation).

On any system where people use GUIs to manipulate files and folders, having 
spaces and arbitrary punctuation in pathnames is common. Pointing $TMPDIR at 
such a place for a special purpose is not unreasonable.

People keep assuming injection is all about malice and being hacked. It is not.  
It is also about robustness and reliability, and possible silent 
failure/misfunction.

Cheers,
Cameron Simpson <cs@zip.com.au>

Steph@ensoniq.com says...
| Motorcycle maintenence is an art, isn't it?
By the time you've finished, it's a black art.
         - Dave Parry <d.parry@ic.ac.uk>

[toc] | [prev] | [next] | [standalone]


#77430

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2014-09-02 13:18 +1000
Message-ID<54053724$0$6599$c3e8da3$5496439d@news.astraweb.com>
In reply to#77416
Cameron Simpson wrote:

> It is not just about being hacked.
> 
> It is about being robust in the face of unusual setups.
> 
> If I were producing this function for general use (even my own personal
> general use) it would need to be reliable. That includes things like
> $TMPDIR having spaces in it (or other unfortunate punctuation).

Ah, gotcha. That makes sense.



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#77417

FromChris Angelico <rosuav@gmail.com>
Date2014-09-02 08:25 +1000
Message-ID<mailman.13697.1409610340.18130.python-list@python.org>
In reply to#77410
On Tue, Sep 2, 2014 at 4:02 AM, Steven D'Aprano
<steve+comp.lang.python@pearwood.info> wrote:
> I'm not really seeing how this is a security vulnerability. If somebody can
> break into my system and set a hostile GIT_EDITOR, or TMPDIR, environment
> variables, I've already lost.

Agreed. If I'm calling on your program and setting EDITOR or
GIT_EDITOR or whatever to configure how you ask me to edit a file,
that's because it's *my* system. The aforementioned setup is actually
run as root; the 'editor' quite deliberately does almost nothing, but
I know it's safe because I'm the one in control, not because the
editor's sanitized.

ChrisA

[toc] | [prev] | [next] | [standalone]


#77427

Fromgschemenauer3@gmail.com
Date2014-09-01 19:24 -0700
Message-ID<845f1fd1-db0d-4559-91ea-b50b34e1e03d@googlegroups.com>
In reply to#77407
On Monday, September 1, 2014 11:11:34 AM UTC-5, Steven D'Aprano wrote:
> Python's input() or raw_input() function is good for getting a single line
> 
> of text from the user. But what if you want a more substantial chunk of
> 
> text from the user? Here's how to call out to an external editor such as
> 
> ed, nano, vim, emacs, and even GUI text editors:
> 
> 
> 
> import tempfile
> 
> 
> 
> def edit(editor, content=''):
> 
>     f = tempfile.NamedTemporaryFile(mode='w+')
> 
>     if content:
> 
>         f.write(content)
> 
>         f.flush()
> 
>     command = editor + " " + f.name
> 
>     status = os.system(command)
> 
>     f.seek(0, 0)
> 
>     text = f.read()
> 
>     f.close()
> 
>     assert not os.path.exists(f.name)
> 
>     return (status, text)
> 
> 
> 
> 
> 
> Anyone able to test it on Windows for me please?
> 
> 
> 
> 
> 
> More here: 
> 
> 
> 
> https://code.activestate.com/recipes/578926/
> 
> 
> 
> 
> 
> -- 
> 
> Steven

here's a full blown text editor (GUI) that gets a "substantial chunk of text" from the user.  It's almost finished but is perfectly functional right now.  Two files:

main file:

#!/usr/bin/env python

import os
import sys
import glob
import webbrowser

from GtkApp       import *
from GPYedit_conf import Preferences

APPLICATION_NAME = 'GPYedit'

class GPYedit(GtkApp_Toplevel):

      # For each tab in the notebook, we will store
      # our data representing each file (or empty buffer)
      # in a list.  Each item is a dictionary keeping track of the:
      #   - Python file object
      #   - Three components of editing area: scrolled window, textview, and buffer (per tab)
      #   - Full pathname of the file being edited
      #   - Text shown in the notebook widget tabs

      open_files = [ ]


      # Keep track of which buffer we're dealing with.
      # Each time the notebook page is switched, this number
      # will change (see 'on_nb_page_switched' callback).  This value 
      # is used as the index into the open files list to get at the
      # file-specific information and widgets.

      current_tab = 0


      # User preferences will be accessible through this attribute.
      # The Preferences class will be initialized from directives
      # found in the gpyedit_settings.ini file.

      preferences = Preferences()

      def __init__(this):
            """
            This is where it all starts.  Begin by setting
            the window geometry and title and decide whether
            to create a new empty file or use the arguments provided.

            """
            GtkApp_Toplevel.__init__(this)
            this.window.set_title("GPYedit")
            (width, height) = GPYedit.preferences.get_window_dimensions()
            this.window.set_default_size(width, height)
            this.build_GUI()
            if len(sys.argv) > 1:
                  names = sys.argv[1:]
                  for name in names:
                        if os.path.exists(name) and os.path.isfile(name):
                              this.tab_new_from_contents(name)
                        else:
                              print 'File "' + name + '" doesn\'t exist.'
            else:
                  this.create_new_file()


      def build_GUI(this):
            """
            Create the main interface components.
            These are
               - vbox: Main vertical box for laying out widgets
               - menu_bar: self explanatory
               - notebook: The tabbed container holding our file buffers
            """
            this.vbox = gtk.VBox(False, 0)
            this.menu_bar = gtk.MenuBar()
            this.notebook = gtk.Notebook()
            this.notebook.set_scrollable(True)
            this.notebook.connect("switch-page", this.on_nb_page_switch)
            this.create_menus()
            this.create_toolbar()
            this.vbox.pack_start(this.notebook, True, True, 0)
            this.window.add(this.vbox)


      def create_new_file(this, menu_item = None):
            """
            Create a blank buffer with no associated Python file object or name (yet)
            NOTE: menu_item is a parameter here because
            this method will be used as a signal handler
            also for the File menu 'new' item and the prototype
            for that handler requires this parameter.  It is not used though.

            """
            (scrolled_win, textview, buf) = this.editing_area_new()
            label = gtk.Label("Untitled")
            edit_area = { 'scrolled_window': scrolled_win, 'textview': textview, 'buffer': buf }
            # Store everything we know
            GPYedit.open_files.append({'file_object': None,
                                       'edit_area':   edit_area,
                                       'filename':    None,
                                       'label':       label})
            index = this.notebook.append_page(scrolled_win, label)
            this.notebook.show_all()
            return GPYedit.open_files[index]


      def tab_new_from_contents(this, filename):
            """
            Open a new tab and dump the contents of a file
            into it.

            """
            if this.check_for_used_file_name(filename):
                  return

            (scrolled_win, textview, buf) = this.editing_area_new()
            fobj = open(filename, 'r+w')
            data = fobj.read()
            if data != '': buf.set_text(data)
            buf.set_modified(False)
            label = gtk.Label(os.path.basename(filename))
            edit_area = { 'scrolled_window': scrolled_win, 'textview': textview, 'buffer': buf }
            # Store everything we know
            GPYedit.open_files.append({'file_object': fobj,
                                       'edit_area':   edit_area,
                                       'filename':    filename,
                                       'label':       label})
            index = this.notebook.append_page(scrolled_win, label)
            this.notebook.show_all()
            return GPYedit.open_files[index]


      def open_file(this, menu_item = None):
            """
            Open a file.  The action performed depends on whether
            the current tab with focus has an associated file name.
            If it is an empty buffer, then the name the user selects
            in the file selector is opened in this tab otherwise
            a new one is created to house the new file contents.

            """
            # May need to revise a little bit.  What happens when there
            # is no tab?

            error = False

            chooser = gtk.FileChooserDialog("Open A File", this.window)
            chooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
            chooser.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
            response = chooser.run()
            if response == gtk.RESPONSE_OK:
                  filename = chooser.get_filename()
                  if os.path.exists(filename) and os.path.isfile(filename):
                        if this.check_for_used_file_name(filename):
                              # Throw error dialog box?
                              gtk.Widget.destroy(chooser)
                              return
                        try:
                              data = GPYedit.open_files[GPYedit.current_tab]
                        except IndexError:
                              # If there are no tabs, we can't grab the file data
                              # so make sure there's something to work with.
                              data = this.create_new_file()
                        if data['filename'] is None and not data['edit_area']['buffer'].get_modified():
                              obj = open(filename, 'r+w')
                              contents = obj.read()
                              data['file_object'] = obj
                              data['edit_area']['buffer'].set_text(contents)   # Insertion..
                              data['edit_area']['buffer'].set_modified(False)  # ..But no user interaction (yet)
                              data['filename'] = filename
                              data['label'] = os.path.basename(data['filename'])
                              this.notebook.set_tab_label_text(data['edit_area']['scrolled_window'], data['label'])
                              GPYedit.open_files[GPYedit.current_tab] = data
                              this.set_window_title(filename)
                        else:
                              data = this.tab_new_from_contents(filename)
                  else:
                        error = gtk.MessageDialog(parent = this.window,
                                                  type = gtk.MESSAGE_ERROR,
                                                  buttons = gtk.BUTTONS_OK,
                                                  message_format = "The file '" + filename + "' doesn't exist!")

            gtk.Widget.destroy(chooser)

            if error:
                  error.run()
                  error.destroy()


      def close_file(this, menu_item = None):
            """
            Close a file.  Determines whether the 'file' to be closed
            is just a scratch buffer with some text in it or if it has
            a file name (in which case there is an associated Python file object).
            If the buffer has been modified since it was either opened or the user
            typed some text in, give them a chance to save the data to a file on disk
            before removing the tab from the notebook widget.
            """
            if this.notebook.get_n_pages() == 0:
                  return
            current_file = GPYedit.open_files[GPYedit.current_tab]
            if current_file['edit_area']['buffer'].get_modified() == True:
                  prompt = gtk.Dialog("Unsaved Changes", this.window)
                  prompt.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                     gtk.STOCK_CLEAR, gtk.RESPONSE_NO,
                                     gtk.STOCK_YES, gtk.RESPONSE_YES)
                  content_area = prompt.get_content_area()
                  #name = this.notebook.get_tab_label_text(current_file['edit_area']['scrolled_window'])
                  if current_file['filename'] is None:
                        name = 'Untitled'
                  else:
                        name = os.path.basename(current_file['filename'])
                  label = gtk.Label("Save changes to '" + name + "'?")
                  content_area.pack_start(label, True, True, 0)
                  prompt.show_all()
                  response = prompt.run()
                  prompt.destroy()
                  if response == gtk.RESPONSE_CANCEL:
                        return
                  elif response == gtk.RESPONSE_NO:
                        if current_file['file_object']:
                              current_file['file_object'].close()
                  elif response == gtk.RESPONSE_YES:
                        (start, end) = current_file['edit_area']['buffer'].get_bounds()
                        if current_file['filename'] is None:
                              (action, selected_filename) = this.run_save_as_dialog()
                              if action == gtk.RESPONSE_OK and selected_filename is not None:
                                    this.save_file(selected_filename,
                                                   current_file['edit_area']['buffer'].get_text(start, end))
                        else:
                              this.save_file()
            else:
                  if current_file['filename'] is not None:
                        current_file['file_object'].close()

            tab_to_remove = GPYedit.current_tab
            this.notebook.remove_page(tab_to_remove)
            if this.notebook.get_n_pages() == 0:
                  this.set_window_title(alt_title = APPLICATION_NAME)
            del GPYedit.open_files[tab_to_remove]


      def save_file(this, filename = None, data = ''):
            """
            Write the contents of a buffer to a file on disk.
            """
            if this.notebook.get_n_pages() == 0:
                  return

            current_file = GPYedit.open_files[GPYedit.current_tab]

            (start, end) = current_file['edit_area']['buffer'].get_bounds()

            text_to_write = current_file['edit_area']['buffer'].get_text(start, end)

            if filename is not None:
                  if len(data) > 0:
                        obj = open(filename, 'w')
                        obj.write(data)
                        obj.close()
                  else:
                        # Filename given but no data passed. Dump contents of current buffer
                        # into a file specified by 'filename' argument.
                        obj = open(filename, 'w')
                        obj.write(text_to_write)
                        obj.close()
            else:
                  # Filename to save to was not provided.
                  # If the current buffer has no file name, then show a "Save As"
                  # dialog and prompt them to specify a file name.
                  if current_file['filename'] is None:
                        (action, selected_filename) = this.run_save_as_dialog()
                        if action == gtk.RESPONSE_OK:
                              current_file['filename'] = selected_filename
                              current_file['file_object'] = open(selected_filename, 'w+')
                              current_file['file_object'].write(text_to_write)
                              current_file['label'].set_text(os.path.basename(selected_filename))
                              current_file['edit_area']['buffer'].set_modified(False)
                              GPYedit.open_files[GPYedit.current_tab] = current_file
                              this.set_window_title(selected_filename)
                  else:
                        # Current buffer has associated file name.
                        # Save to that file.
                        curr_file_obj = current_file['file_object']
                        curr_file_obj.truncate(0)
                        curr_file_obj.seek(0)
                        curr_file_obj.write(text_to_write)
                        current_file['edit_area']['buffer'].set_modified(False)


      def set_window_title(this, filename = None, alt_title = ''):
            """
            Set the window title to a specific string with alt_title
            or work with an expected file name.  The format for showing
            the title information is:
               filename (directory path) - application name
            """
            if alt_title:
                  this.window.set_title(alt_title)
            elif filename is None:
                  this.window.set_title("Untitled - " + APPLICATION_NAME)   # Default title
            else:
                  (dirpath, fname) = os.path.split(filename)
                  this.window.set_title(fname + " (" + dirpath + ") - " + APPLICATION_NAME)


      def select_all(this):
            """
            Select all the text in the editing area.
            """
            current_file = GPYedit.open_files[GPYedit.current_tab]
            buf = current_file['edit_area']['buffer']
            (start, end) = buf.get_bounds()
            buf.select_range(start, end)


      def copy_to_clipboard(this):
            """
            Copy some selected text to the clipboard.
            """
            current_file = GPYedit.open_files[GPYedit.current_tab]
            clipboard = gtk.clipboard_get()
            current_file['edit_area']['buffer'].copy_clipboard(clipboard)


      def popup_search_box(this, menu_item = None):
            """
            Display the search dialog.
            """
            dial = gtk.Dialog("Find and Replace", this.window)
            dial.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                             gtk.STOCK_CONVERT, gtk.RESPONSE_APPLY,
                             gtk.STOCK_FIND, gtk.RESPONSE_OK)
            dial.set_response_sensitive(gtk.RESPONSE_OK, False)
            dial.set_response_sensitive(gtk.RESPONSE_APPLY, False)
            table = gtk.Table(4, 2, False)
            table.set_row_spacings(8)
            table.set_col_spacings(8)
            find_label = gtk.Label("Search for:")
            find_label.set_alignment(0, 0.5)
            replace_label = gtk.Label("Replace with:")
            replace_label.set_alignment(0, 0.5)
            find_entry = gtk.Entry()
            replace_entry = gtk.Entry()
            case_sens = gtk.CheckButton("Case sensitive")
            replace_all = gtk.CheckButton("Replace all occurences")
            table.attach(find_label, 0, 1, 0, 1)
            table.attach(find_entry, 1, 2, 0, 1)
            table.attach(replace_label, 0, 1, 1, 2)
            table.attach(replace_entry, 1, 2, 1, 2)
            table.attach(case_sens, 0, 2, 2, 3)
            table.attach(replace_all, 0, 2, 3, 4)
            content_area = dial.get_content_area()
            content_area.pack_start(table)
            table.set_border_width(8)
            find_entry.connect("insert-text", this.search_buttons_sensitive, dial)
            find_entry.connect("backspace", this.search_buttons_insensitive, dial)
            dt_id = find_entry.connect("delete-text", this.search_buttons_insensitive_del_text, dial)
            find_entry.set_data('del_text_sig_id', dt_id)
            widgets = {'find_entry': find_entry,
                       'replace_entry': replace_entry,
                       'match_case': case_sens,
                       'replace_all': replace_all}
            dial.connect("response", this.search_dialog_response, widgets)
            dial.show_all()
            dial.run()


      def search_dialog_response(this, dialog, response, widgets):
            """
            Process the response returned from the search and replace dialog.
            """
            if response == gtk.RESPONSE_OK:
                  this.document_search(widgets)
            elif response == gtk.RESPONSE_CANCEL:
                  dialog.destroy()
            elif response == gtk.RESPONSE_APPLY:
                  this.document_replace(widgets)


      def document_search(this, widgets):
            """
            Function not finished
            By default, do a forward search on the document in
            the tab with focus.  To work, this function should
            get these widgets:
               - find entry text box with the search text
               - replace entry with replacement text
               - match case checkbox
               - text buffer for current file
            """
            if widgets['match_case'].get_active():
                  case_sensitive = True
            else:
                  case_sensitive = False

            current_file = GPYedit.open_files[GPYedit.current_tab]

            tbuffer = current_file['edit_area']['buffer']
            start_iter = tbuffer.get_start_iter()
            search_text = widgets['find_entry'].get_text()
            (begin, end) = start_iter.forward_search(search_text, gtk.TEXT_SEARCH_TEXT_ONLY)
            tbuffer.select_range(begin, end)
            
            
      def document_replace(this, widgets):
            """
            Find a search string and replace it with new text.
            By default, only one replacement is done but with
            the 'replace all occurences' checkbox selected then
            it will perform a global search and replace.
            """
            current_file = GPYedit.open_files[GPYedit.current_tab]
            tbuffer = current_file['edit_area']['buffer']
            start_iter = tbuffer.get_start_iter()
            search_text = widgets['find_entry'].get_text()
            if widgets['replace_entry'].get_text_length() > 0:
                  (lower, upper) = tbuffer.get_bounds()
                  bufdata = tbuffer.get_text(lower, upper)
                  replace_str = widgets['replace_entry'].get_text()
                  if widgets['replace_all'].get_active():
                        # REPLACE ALL
                        updated_text = bufdata.replace(search_text, replace_str)
                  else:
                        # REPLACE ONCE
                        updated_text = bufdata.replace(search_text, replace_str, 1)
                  tbuffer.set_text(updated_text)
                  tbuffer.set_modified(False)
            else:
                  # ERROR: NO REPLACEMENT TEXT GIVEN
                  pass


      def search_buttons_sensitive(this, editable, new_text, new_text_length, pos, search_dialog):
            """
            Determine whether the buttons should be sensitive, thereby
            allowing the user to search, if there is text in the search box.
            """
            if editable.get_text_length() > 0:
                  return
            if new_text_length > 0:
                  search_dialog.set_response_sensitive(gtk.RESPONSE_OK, True)
                  search_dialog.set_response_sensitive(gtk.RESPONSE_APPLY, True)


      def search_buttons_insensitive(this, editable, search_dialog):
            """
            Make the search buttons insensitive when there is no
            text in the search box.
            """
            if editable.get_text_length() == 1:
                  search_dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
                  search_dialog.set_response_sensitive(gtk.RESPONSE_APPLY, False)

      def search_buttons_insensitive_del_text(this, editable, start, end, search_dialog):
            """
            Similar to search_buttons_insensitive(), except that this
            handler is connected for the 'delete-text' signal.  It allows
            a user to highlight some text and delete it all at once.  In the
            case where they select and delete everything in the search box, the
            buttons along the bottom of the search dialog should become unusable.
            """
            if editable.get_text_length() > 0:
                  editable.handler_block(editable.get_data('del_text_sig_id'))
                  editable.delete_text(start, end)
                  editable.handler_unblock(editable.get_data('del_text_sig_id'))

            if editable.get_text_length() == 0:
                  search_dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
                  search_dialog.set_response_sensitive(gtk.RESPONSE_APPLY, False)


      def check_for_used_file_name(this, name):
            """
            Any given file should only be opened in one tab.
            This method returns True if a specified file's name
            is already in use.
            """
            for element in GPYedit.open_files:
                  values = element.values()
                  if name in values:
                        return True


      def delete_event(this, widget, event, data = None):
            """
            Override method to close all files if a user clicks the 'X'
            button to close the text editor without first manually closing
            them via the File > Close menu option. Just explicitly cleaning up.
            """
            for element in GPYedit.open_files:
                  file_to_clean_up = element['file_object']
                  if file_to_clean_up:
                        file_to_clean_up.close()


      def run_save_as_dialog(this):
            """
            Display a Save As dialog box and allow the user to specify
            a file name to save to.
            """
            save_as = gtk.FileChooserDialog("Save As",
                                            this.window,
                                            gtk.FILE_CHOOSER_ACTION_SAVE,
                                            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                             gtk.STOCK_SAVE, gtk.RESPONSE_OK))
            action_id = save_as.run()
            save_as.hide()
            if action_id == gtk.RESPONSE_OK:
                  ret_val = (action_id, save_as.get_filename())
            else:
                  ret_val = (action_id, None)
            save_as.destroy()
            return ret_val


      def editing_area_new(this):
            """
            Build the set of widgets necessary to allow
            for the editing of text.  This includes:
               - scrolled window: allow viewing area to scroll
               - text view: widget to edit text
            """
            scrolled_win = gtk.ScrolledWindow()
            scrolled_win.set_shadow_type(gtk.SHADOW_ETCHED_IN)
            scrolled_win.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
            scrolled_win.set_border_width(3)
            textview = gtk.TextView()
            textview.set_left_margin(3)
            textview.set_right_margin(3)
            textview.set_pixels_above_lines(1)
            buf = textview.get_buffer()
            scrolled_win.add(textview)

            textview.modify_base(gtk.STATE_NORMAL,
               gtk.gdk.Color(GPYedit.preferences.get_background_color()))

            textview.modify_text(gtk.STATE_NORMAL,
               gtk.gdk.Color(GPYedit.preferences.get_foreground_color()))

            # Set the default font used for editing

            textview.modify_font(
               pango.FontDescription(GPYedit.preferences.get_font()))

            # Return a tuple of the three elements.
            # Note that the scrolled window itself holds the
            # textview which in turn contains the buffer.  But
            # this helps avoid having to extract the children of
            # the scrolled window every time we need access to either
            # the view or the buffer inside it.

            return (scrolled_win, textview, buf)


      def create_toolbar(this):
            """
            Create toolbar and buttons before packing into
            the main window.
            """
            this.toolbar = gtk.Toolbar()

            # Make toolbar widget buttons
            this.tb_new = gtk.ToolButton(gtk.STOCK_NEW)
            this.tb_open = gtk.ToolButton(gtk.STOCK_OPEN)
            this.tb_save = gtk.ToolButton(gtk.STOCK_SAVE)
            this.tb_save_as = gtk.ToolButton(gtk.STOCK_SAVE_AS)

            # Insert buttons into toolbar
            this.toolbar.insert(this.tb_new, 0)
            this.toolbar.insert(this.tb_open, 1)
            this.toolbar.insert(this.tb_save, 2)
            this.toolbar.insert(this.tb_save_as, 3)

            this.view_menu_toolbar.connect("toggled", this.toggle_tb_visible)

            # Tool bar 'new' button creates a new file.  The method signature
            # doesn't match the required parameters for this signal though so
            # we use a sort of pass-through function to get there.
            this.tb_new.connect("clicked",  lambda tool_item: this.create_new_file())
            this.tb_open.connect("clicked", lambda tool_item: this.open_file())
            this.tb_save.connect("clicked", lambda tool_item: this.save_file())

            # Pack toolbar into window vbox
            this.vbox.pack_start(this.toolbar, False, False, 0)


      def toggle_tb_visible(this, widget, data = None):
            """
            Callback to control visiblity of the toolbar
            """
            if widget.get_active():
                  this.toolbar.show()
            else:
                  this.toolbar.hide()


      def on_nb_page_switch(this, notebook, page, page_num):
            """
            Each time the user selects a tab to work with, change
            the internal tab indicator so that it can be used to get
            the relevant data associated with that tab.  This is a callback
            and there is no need to call directly.  See GTK+ 'switch-page' signal.
            """
            GPYedit.current_tab = page_num

            file_info = GPYedit.open_files[GPYedit.current_tab]

            if file_info['filename'] is not None:
                  this.set_window_title(file_info['filename'])
            else:
                  this.set_window_title()


      def open_by_pattern(this):
            """
            Open all files that match a shell-style pattern.
            """
            items = glob.glob(os.environ['HOME'] + os.sep + '*.php')
            for item in items:
                  this.tab_new_from_contents(item)
                  


      def create_menus(this):
            """
            Create the menu bar and associated menu items.
            """
            accel_group = gtk.AccelGroup()

            # Associate with main window
            this.window.add_accel_group(accel_group)

            # Create File menu
            this.file_menu = gtk.Menu()
            this.file_menu.set_accel_group(accel_group)
            this.file_menu_item = gtk.MenuItem("File")
            this.file_menu_item.set_submenu(this.file_menu)

            # Create menu items
            this.file_menu_new = gtk.ImageMenuItem(gtk.STOCK_NEW)
            this.file_menu_new.add_accelerator("activate", accel_group, ord('n'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.file_menu_open = gtk.ImageMenuItem(gtk.STOCK_OPEN)
            this.file_menu_open.add_accelerator("activate", accel_group, ord('o'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.file_menu_open_files_by_pattern = gtk.MenuItem("Open Files By Pattern")
            this.file_menu_save = gtk.ImageMenuItem(gtk.STOCK_SAVE)
            this.file_menu_save.add_accelerator("activate", accel_group, ord('s'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.file_menu_save_as = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
            this.file_menu_save_as.add_accelerator("activate", accel_group, ord('s'), gtk.gdk.SHIFT_MASK | gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.file_menu_close = gtk.ImageMenuItem(gtk.STOCK_CLOSE)
            this.file_menu_close.add_accelerator("activate", accel_group, ord('w'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.file_menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
            this.file_menu_quit.add_accelerator("activate", accel_group, ord('q'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)

            # Add them to File menu
            this.file_menu.append(this.file_menu_new)
            this.file_menu.append(this.file_menu_open)
            this.file_menu.append(this.file_menu_open_files_by_pattern)
            this.file_menu.append(this.file_menu_save)
            this.file_menu.append(this.file_menu_save_as)
            this.file_menu.append(this.file_menu_close)
            this.file_menu.append(this.file_menu_quit)

            # Connect signals
            this.file_menu_new.connect("activate", this.create_new_file)
            this.file_menu_open.connect("activate", this.open_file)
            this.file_menu_open_files_by_pattern.connect("activate", lambda menu_item: this.open_by_pattern())
            this.file_menu_save.connect("activate", lambda menu_item: this.save_file())
            this.file_menu_close.connect("activate", this.close_file)
            this.file_menu_quit.connect("activate", gtk.main_quit)

            # Create Edit menu
            this.edit_menu = gtk.Menu()
            this.edit_menu.set_accel_group(accel_group)
            this.edit_menu_item = gtk.MenuItem("Edit")
            this.edit_menu_item.set_submenu(this.edit_menu)

            # Create menu items
            this.edit_menu_cut = gtk.ImageMenuItem(gtk.STOCK_CUT)
            this.edit_menu_cut.add_accelerator("activate", accel_group, ord('x'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.edit_menu_copy = gtk.ImageMenuItem(gtk.STOCK_COPY)
            this.edit_menu_copy.add_accelerator("activate", accel_group, ord('c'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.edit_menu_paste = gtk.ImageMenuItem(gtk.STOCK_PASTE)
            this.edit_menu_paste.add_accelerator("activate", accel_group, ord('v'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.edit_menu_select_all = gtk.MenuItem("Select All")
            this.edit_menu_select_all.add_accelerator("activate", accel_group, ord('a'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
            this.edit_menu_preferences = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)

            # Add them to Edit menu
            this.edit_menu.append(this.edit_menu_cut)
            this.edit_menu.append(this.edit_menu_copy)
            this.edit_menu.append(this.edit_menu_paste)
            this.edit_menu.append(this.edit_menu_select_all)
            this.edit_menu.append(this.edit_menu_preferences)

            # Connect signals
            this.edit_menu_copy.connect("activate", lambda menu_item: this.copy_to_clipboard())
            this.edit_menu_select_all.connect("activate", lambda menu_item: this.select_all())

            # Create View menu
            this.view_menu = gtk.Menu()
            this.view_menu_item = gtk.MenuItem("View")
            this.view_menu_item.set_submenu(this.view_menu)

            # Create menu items
            this.view_menu_toolbar = gtk.CheckMenuItem("Toolbar")
            this.view_menu_toolbar.set_active(True)
            this.view_menu_file_explorer_pane = gtk.CheckMenuItem("File Browser Pane")

            # Add them to View menu
            this.view_menu.append(this.view_menu_toolbar)
            this.view_menu.append(this.view_menu_file_explorer_pane)

            # Create Search menu
            this.search_menu = gtk.Menu()
            this.search_menu_item = gtk.MenuItem("Search")
            this.search_menu_item.set_submenu(this.search_menu)

            # Create menu items
            this.search_menu_s_and_r = gtk.ImageMenuItem(gtk.STOCK_FIND_AND_REPLACE)

            # Add them to Search menu
            this.search_menu.append(this.search_menu_s_and_r)

            # Connect signals
            this.search_menu_s_and_r.connect("activate", this.popup_search_box)

            # Create Help menu
            this.help_menu = gtk.Menu()
            this.help_menu_item = gtk.MenuItem("Help")
            this.help_menu_item.set_submenu(this.help_menu)

            # Create menu items
            this.help_menu_about = gtk.ImageMenuItem(gtk.STOCK_HELP)

            # Add them to Help menu
            this.help_menu.append(this.help_menu_about)

            # Add menus to the menubar
            this.menu_bar.append(this.file_menu_item)
            this.menu_bar.append(this.edit_menu_item)
            this.menu_bar.append(this.view_menu_item)
            this.menu_bar.append(this.search_menu_item)
            this.menu_bar.append(this.help_menu_item)

            # Pack menu bar into main window
            this.vbox.pack_start(this.menu_bar, False, False, 0)


########## Main ##########

if __name__ == "__main__": GPYedit().main()

##########################


second file:


import os

from utils import find_file

CONFIG_FILE = "gpyedit_settings.ini"

class Preferences:

      """
      This class holds and manages the user's preferences which
      will be stored permanently in the gpyedit_settings.ini file.
      """

      settings = \
       {
         "window_width":     779,
         "window_height":    419,
         "font_face":        "monospace",
         "background_color": "#FFFFFF",
         "foreground_color": "#000000"
       }

      def __init__(this):
            """
            Read the configuration file and set up the
            preferences so that they are ready to be accessed
            by the main application.

            """
            if not os.path.exists(CONFIG_FILE): config_file_location = find_file(CONFIG_FILE, os.environ["HOME"])
            else:
                  config_file_location = os.getcwd() + os.sep + CONFIG_FILE

            if config_file_location is None: return  # Configuration not found. Use default settings
            else:
                  this.process_options(open(config_file_location, "r").readlines())


      def process_options(this, config):
            """
            Parse configuration options in the gpyedit_settings.ini file.
            """
            for option in config:
                  data = option.split(":")      # Get [option, value]
                  if len(data) != 2: continue
                  (opt, val) = (data[0].strip(), data[1].strip())
                  for setting in this.settings.keys():
                        if opt == setting:
                              this.settings[setting] = val


      def get_font(this):
            """
            Return the font that should be used for editing text.
            """
            return Preferences.settings["font_face"]


      def get_background_color(this):
            """
            Return the background color of the editing area.
            """
            return Preferences.settings["background_color"]


      def get_foreground_color(this):
            """
            Return the foreground color of the editing area.
            """
            return Preferences.settings["foreground_color"]


      def get_window_dimensions(this):
            """
            Retrieve the toplevel window dimensions as a width and height
            """
            return (int(this.settings["window_width"]), int(this.settings["window_height"]))


      def run_dialog(this):
            """
            Create the preferences dialog box accessible through Edit > Preferences in
            the menu bar.
            """
            
---------------------------------------------

maybe that'll help you see how it's done.

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.python


csiph-web