Read Programming Python Online

Authors: Mark Lutz

Tags: #COMPUTERS / Programming Languages / Python

Programming Python (92 page)

BOOK: Programming Python
8.26Mb size Format: txt, pdf, ePub
ads
PyEdit Source Code

The PyEdit program
consists of only a small configuration module and one main
source file, which is just over 1,000 lines long—a
.py
that can be either run or imported. For use on
Windows, there is also a one-line
.pyw
file that
just executes the
.py
file’s contents with an
execfile('textEditor.py')
call. The
.pyw
suffix avoids the DOS console streams window
pop up when launched by clicking on Windows.

Today,
.pyw
files can be both imported and
run, like normal
.py
files (they can also be
double-clicked, and launched by Python tools such as
os.system
and
os.startfile
), so we don’t really need a
separate file to support both import and console-less run modes. I
retained the
.py
, though, in order to see printed
text during development and to use PyEdit as a simple IDE—when the run
code option is selected, in nonfile mode, printed output from code being
edited shows up in PyEdit’s DOS console window in Windows. Clients will
normally import the
.py
file.

User configurations file

On to the code. First,
PyEdit’s user configuration module is listed in
Example 11-1
. This is mostly a
convenience, for providing an initial look-and-feel other than the
default. PyEdit is coded to work even if this module is missing or
contains syntax errors. This file is primarily intended for when
PyEdit is the top-level script run (in which case the file is imported
from the current directory), but you can also define your own version
of this file elsewhere on your module import search path to customize
PyEdit.

See
textEditor.py
ahead for
more on how this module’s settings are loaded. Its contents are loaded
by two different imports—one import for cosmetic settings assumes this
module itself (not its package) is on the module search path and skips
it if not found, and the other import for Unicode settings always
locates this file regardless of launch modes. Here’s what this
division of configuration labor means for clients:

  • Because the first import for cosmetic settings is relative
    to the module search path, not to the main file’s package, a new
    textConfig.py
    can be defined
    in each client application’s home directory to customize PyEdit
    windows per client.

  • Conversely, Unicode settings here are always loaded from
    this file using package relative imports if needed, because they
    are more critical and unlikely to vary. The package relative
    import used for this is equivalent to a full package import from
    the
    PP4E
    root, but not
    dependent upon directory structure.

Like much of the heuristic Unicode interface described earlier,
this import model is somewhat preliminary, and may require revision if
actual usage patterns warrant.

Example 11-1. PP4E\Gui\TextEditor\textConfig.py

"""
PyEdit (textEditor.py) user startup configuration module;
"""
#----------------------------------------------------------------------------------
# General configurations
# comment-out any setting in this section to accept Tk or program defaults;
# can also change font/colors from GUI menus, and resize window when open;
# imported via search path: can define per client app, skipped if not on the path;
#----------------------------------------------------------------------------------
# initial font # family, size, style
font = ('courier', 9, 'normal') # e.g., style: 'bold italic'
# initial color # default=white, black
bg = 'lightcyan' # colorname or RGB hexstr
fg = 'black' # e.g., 'powder blue', '#690f96'
# initial size
height = 20 # Tk default: 24 lines
width = 80 # Tk default: 80 characters
# search case-insensitive
caseinsens = True # default=1/True (on)
#----------------------------------------------------------------------------------
# 2.1: Unicode encoding behavior and names for file opens and saves;
# attempts the cases listed below in the order shown, until the first one
# that works; set all variables to false/empty/0 to use your platform's default
# (which is 'utf-8' on Windows, or 'ascii' or 'latin-1' on others like Unix);
# savesUseKnownEncoding: 0=No, 1=Yes for Save only, 2=Yes for Save and SaveAs;
# imported from this file always: sys.path if main, else package relative;
#----------------------------------------------------------------------------------
# 1) tries internally known type first (e.g., email charset)
opensAskUser = True # 2) if True, try user input next (prefill with defaults)
opensEncoding = '' # 3) if nonempty, try this encoding next: 'latin-1', 'cp500'
# 4) tries sys.getdefaultencoding() platform default next
# 5) uses binary mode bytes and Tk policy as the last resort
savesUseKnownEncoding = 1 # 1) if > 0, try known encoding from last open or save
savesAskUser = True # 2) if True, try user input next (prefill with known?)
savesEncoding = '' # 3) if nonempty, try this encoding next: 'utf-8', etc
# 4) tries sys.getdefaultencoding() as a last resort
Windows (and other) launch files

Next,
Example 11-2
gives
the
.pyw
launching file used to
suppress a DOS pop up on Windows when run in some modes (for instance,
when double-clicked), but still allow for a console when the
.py
file is run directly (to see the output of
edited code run in nonfile mode, for example). Clicking this directly
is similar to the behavior when PyEdit is run from the PyDemos or
PyGadgets demo launcher bars.

Example 11-2. PP4E\Gui\TextEditor\textEditorNoConsole.pyw

"""
run without a DOS pop up on Windows; could use just a .pyw for both
imports and launch, but .py file retained for seeing any printed text
"""
exec(open('textEditor.py').read()) # as if pasted here (or textEditor.main())

Example 11-2
serves
its purpose, but later in this book update project, I grew tired of
using Notepad to view text files from command lines run in arbitrary
places and wrote the script in
Example 11-3
to launch PyEdit
in a more general and automated fashion. This script disables the DOS
pop up, like
Example 11-2
, when clicked or
run via a desktop shortcut on Windows, but also takes care to
configure the module search path on machines where I haven’t used
Control Panel to do so, and allows for other launching scenarios where
the current working directory may not be the same as the script’s
directory.

Example 11-3. PP4E\Gui\TextEditor\pyedit.pyw

#!/usr/bin/python
"""
convenience script to launch pyedit from arbitrary places with the import path set
as required; sys.path for imports and open() must be relative to the known top-level
script's dir, not cwd -- cwd is script's dir if run by shortcut or icon click, but may
be anything if run from command-line typed into a shell console window: use argv path;
this is a .pyw to suppress console pop-up on Windows; add this script's dir to your
system PATH to run from command-lines; works on Unix too: / and \ handled portably;
"""
import sys, os
mydir = os.path.dirname(sys.argv[0]) # use my dir for open, path
sys.path.insert(1, os.sep.join([mydir] + ['..']*3)) # imports: PP4E root, 3 up
exec(open(os.path.join(mydir, 'textEditor.py')).read())

To run this from a command line in a console window, it simply
has to be on your system path—the action taken by the first line in
the following could be performed just once in Control Panel on
Windows:

C:\...\PP4E\Internet\Web>
set PATH=%PATH%;C:\...\PP4E\Gui\TextEditor
C:\...\PP4E\Internet\Web>
pyedit.pyw test-cookies.py

This script works on Unix, too, and is unnecessary if you set
your PYTHONPATH and PATH system variables (you could then just run
textEditor.py
directly), but I
don’t do so on all the machines I use. For more fun, try registering
this script to open “.txt” files automatically on your computer when
their icons are clicked or their names are typed alone on a command
line (if you can bear to part with Notepad, that is).

Main implementation file

And finally, the
module in
Example 11-4
is PyEdit’s
implementation. This file may run directly as a top-level script, or
it can be imported from other applications. Its code is organized by
the GUI’s main menu options. The main classes used to start and embed
a PyEdit object appear at the end of this file. Study this listing
while you experiment with PyEdit, to learn about its features and
techniques.

Example 11-4. PP4E\Gui\TextEditor\textEditor.py

"""
################################################################################
PyEdit 2.1: a Python/tkinter text file editor and component.
Uses the Tk text widget, plus GuiMaker menus and toolbar buttons to
implement a full-featured text editor that can be run as a standalone
program, and attached as a component to other GUIs. Also used by
PyMailGUI and PyView to edit mail text and image file notes, and by
PyMailGUI and PyDemos in pop-up mode to display source and text files.
New in version 2.1 (4E)
-updated to run under Python 3.X (3.1)
-added "grep" search menu option and dialog: threaded external files search
-verify app exit on quit if changes in other edit windows in process
-supports arbitrary Unicode encodings for files: per textConfig.py settings
-update change and font dialog implementations to allow many to be open
-runs self.update() before setting text in new editor for loadFirst
-various improvements to the Run Code option, per the next section
2.1 Run Code improvements:
-use base name after chdir to run code file, not possibly relative path
-use launch modes that support arguments for run code file mode on Windows
-run code inherits launchmodes backslash conversion (no longer required)
New in version 2.0 (3E)
-added simple font components input dialog
-use Tk 8.4 undo stack API to add undo/redo text modifications
-now verifies on quit, open, new, run, only if text modified and unsaved
-searches are case-insensitive now by default
-configuration module for initial font/color/size/searchcase
TBD (and suggested exercises):
-could also allow search case choice in GUI (not just config file)
-could use re patterns for searches and greps (see text chapter)
-could experiment with syntax-directed text colorization (see IDLE, others)
-could try to verify app exit for quit() in non-managed windows too?
-could queue each result as found in grep dialog thread to avoid delay
-could use images in toolbar buttons (per examples of this in Chapter 9)
-could scan line to map Tk insert position column to account for tabs on Info
-could experiment with "grep" tbd Unicode issues (see notes in the code);
################################################################################
"""
Version = '2.1'
import sys, os # platform, args, run tools
from tkinter import * # base widgets, constants
from tkinter.filedialog import Open, SaveAs # standard dialogs
from tkinter.messagebox import showinfo, showerror, askyesno
from tkinter.simpledialog import askstring, askinteger
from tkinter.colorchooser import askcolor
from PP4E.Gui.Tools.guimaker import * # Frame + menu/toolbar builders
# general configurations
try:
import textConfig # startup font and colors
configs = textConfig.__dict__ # work if not on the path or bad
except: # define in client app directory
configs = {}
helptext = """PyEdit version %s
April, 2010
(2.0: January, 2006)
(1.0: October, 2000)
Programming Python, 4th Edition
Mark Lutz, for O'Reilly Media, Inc.
A text editor program and embeddable object
component, written in Python/tkinter. Use
menu tear-offs and toolbar for quick access
to actions, and Alt-key shortcuts for menus.
Additions in version %s:
- supports Python 3.X
- new "grep" external files search dialog
- verifies app quit if other edit windows changed
- supports arbitrary Unicode encodings for files
- allows multiple change and font dialogs
- various improvements to the Run Code option
Prior version additions:
- font pick dialog
- unlimited undo/redo
- quit/open/new/run prompt save only if changed
- searches are case-insensitive
- startup configuration module textConfig.py
"""
START = '1.0' # index of first char: row=1,col=0
SEL_FIRST = SEL + '.first' # map sel tag to index
SEL_LAST = SEL + '.last' # same as 'sel.last'
FontScale = 0 # use bigger font on Linux
if sys.platform[:3] != 'win': # and other non-Windows boxes
FontScale = 3
################################################################################
# Main class: implements editor GUI, actions
# requires a flavor of GuiMaker to be mixed in by more specific subclasses;
# not a direct subclass of GuiMaker because that class takes multiple forms.
################################################################################
class TextEditor: # mix with menu/toolbar Frame class
startfiledir = '.' # for dialogs
editwindows = [] # for process-wide quit check
# Unicode configurations
# imported in class to allow overrides in subclass or self
if __name__ == '__main__':
from textConfig import ( # my dir is on the path
opensAskUser, opensEncoding,
savesUseKnownEncoding, savesAskUser, savesEncoding)
else:
from .textConfig import ( # 2.1: always from this package
opensAskUser, opensEncoding,
savesUseKnownEncoding, savesAskUser, savesEncoding)
ftypes = [('All files', '*'), # for file open dialog
('Text files', '.txt'), # customize in subclass
('Python files', '.py')] # or set in each instance
colors = [{'fg':'black', 'bg':'white'}, # color pick list
{'fg':'yellow', 'bg':'black'}, # first item is default
{'fg':'white', 'bg':'blue'}, # tailor me as desired
{'fg':'black', 'bg':'beige'}, # or do PickBg/Fg chooser
{'fg':'yellow', 'bg':'purple'},
{'fg':'black', 'bg':'brown'},
{'fg':'lightgreen', 'bg':'darkgreen'},
{'fg':'darkblue', 'bg':'orange'},
{'fg':'orange', 'bg':'darkblue'}]
fonts = [('courier', 9+FontScale, 'normal'), # platform-neutral fonts
('courier', 12+FontScale, 'normal'), # (family, size, style)
('courier', 10+FontScale, 'bold'), # or pop up a listbox
('courier', 10+FontScale, 'italic'), # make bigger on Linux
('times', 10+FontScale, 'normal'), # use 'bold italic' for 2
('helvetica', 10+FontScale, 'normal'), # also 'underline', etc.
('ariel', 10+FontScale, 'normal'),
('system', 10+FontScale, 'normal'),
('courier', 20+FontScale, 'normal')]
def __init__(self, loadFirst='', loadEncode=''):
if not isinstance(self, GuiMaker):
raise TypeError('TextEditor needs a GuiMaker mixin')
self.setFileName(None)
self.lastfind = None
self.openDialog = None
self.saveDialog = None
self.knownEncoding = None # 2.1 Unicode: till Open or Save
self.text.focus() # else must click in text
if loadFirst:
self.update() # 2.1: else @ line 2; see book
self.onOpen(loadFirst, loadEncode)
def start(self): # run by GuiMaker.__init__
self.menuBar = [ # configure menu/toolbar
('File', 0, # a GuiMaker menu def tree
[('Open...', 0, self.onOpen), # build in method for self
('Save', 0, self.onSave), # label, shortcut, callback
('Save As...', 5, self.onSaveAs),
('New', 0, self.onNew),
'separator',
('Quit...', 0, self.onQuit)]
),
('Edit', 0,
[('Undo', 0, self.onUndo),
('Redo', 0, self.onRedo),
'separator',
('Cut', 0, self.onCut),
('Copy', 1, self.onCopy),
('Paste', 0, self.onPaste),
'separator',
('Delete', 0, self.onDelete),
('Select All', 0, self.onSelectAll)]
),
('Search', 0,
[('Goto...', 0, self.onGoto),
('Find...', 0, self.onFind),
('Refind', 0, self.onRefind),
('Change...', 0, self.onChange),
('Grep...', 3, self.onGrep)]
),
('Tools', 0,
[('Pick Font...', 6, self.onPickFont),
('Font List', 0, self.onFontList),
'separator',
('Pick Bg...', 3, self.onPickBg),
('Pick Fg...', 0, self.onPickFg),
('Color List', 0, self.onColorList),
'separator',
('Info...', 0, self.onInfo),
('Clone', 1, self.onClone),
('Run Code', 0, self.onRunCode)]
)]
self.toolBar = [
('Save', self.onSave, {'side': LEFT}),
('Cut', self.onCut, {'side': LEFT}),
('Copy', self.onCopy, {'side': LEFT}),
('Paste', self.onPaste, {'side': LEFT}),
('Find', self.onRefind, {'side': LEFT}),
('Help', self.help, {'side': RIGHT}),
('Quit', self.onQuit, {'side': RIGHT})]
def makeWidgets(self): # run by GuiMaker.__init__
name = Label(self, bg='black', fg='white') # add below menu, above tool
name.pack(side=TOP, fill=X) # menu/toolbars are packed
# GuiMaker frame packs itself
vbar = Scrollbar(self)
hbar = Scrollbar(self, orient='horizontal')
text = Text(self, padx=5, wrap='none') # disable line wrapping
text.config(undo=1, autoseparators=1) # 2.0, default is 0, 1
vbar.pack(side=RIGHT, fill=Y)
hbar.pack(side=BOTTOM, fill=X) # pack text last
text.pack(side=TOP, fill=BOTH, expand=YES) # else sbars clipped
text.config(yscrollcommand=vbar.set) # call vbar.set on text move
text.config(xscrollcommand=hbar.set)
vbar.config(command=text.yview) # call text.yview on scroll move
hbar.config(command=text.xview) # or hbar['command']=text.xview
# 2.0: apply user configs or defaults
startfont = configs.get('font', self.fonts[0])
startbg = configs.get('bg', self.colors[0]['bg'])
startfg = configs.get('fg', self.colors[0]['fg'])
text.config(font=startfont, bg=startbg, fg=startfg)
if 'height' in configs: text.config(height=configs['height'])
if 'width' in configs: text.config(width =configs['width'])
self.text = text
self.filelabel = name
############################################################################
# File menu commands
############################################################################
def my_askopenfilename(self): # objects remember last result dir/file
if not self.openDialog:
self.openDialog = Open(initialdir=self.startfiledir,
filetypes=self.ftypes)
return self.openDialog.show()
def my_asksaveasfilename(self): # objects remember last result dir/file
if not self.saveDialog:
self.saveDialog = SaveAs(initialdir=self.startfiledir,
filetypes=self.ftypes)
return self.saveDialog.show()
def onOpen(self, loadFirst='', loadEncode=''):
"""
2.1: total rewrite for Unicode support; open in text mode with
an encoding passed in, input from the user, in textconfig, or
platform default, or open as binary bytes for arbitrary Unicode
encodings as last resort and drop \r in Windows end-lines if
present so text displays normally; content fetches are returned
as str, so need to encode on saves: keep encoding used here;
tests if file is okay ahead of time to try to avoid opens;
we could also load and manually decode bytes to str to avoid
multiple open attempts, but this is unlikely to try all cases;
encoding behavior is configurable in the local textConfig.py:
1) tries known type first if passed in by client (email charsets)
2) if opensAskUser True, try user input next (prefill wih defaults)
3) if opensEncoding nonempty, try this encoding next: 'latin-1', etc.
4) tries sys.getdefaultencoding() platform default next
5) uses binary mode bytes and Tk policy as the last resort
"""
if self.text_edit_modified(): # 2.0
if not askyesno('PyEdit', 'Text has changed: discard changes?'):
return
file = loadFirst or self.my_askopenfilename()
if not file:
return
if not os.path.isfile(file):
showerror('PyEdit', 'Could not open file ' + file)
return
# try known encoding if passed and accurate (e.g., email)
text = None # empty file = '' = False: test for None!
if loadEncode:
try:
text = open(file, 'r', encoding=loadEncode).read()
self.knownEncoding = loadEncode
except (UnicodeError, LookupError, IOError): # lookup: bad name
pass
# try user input, prefill with next choice as default
if text == None and self.opensAskUser:
self.update() # else dialog doesn't appear in rare cases
askuser = askstring('PyEdit', 'Enter Unicode encoding for open',
initialvalue=(self.opensEncoding or
sys.getdefaultencoding() or ''))
if askuser:
try:
text = open(file, 'r', encoding=askuser).read()
self.knownEncoding = askuser
except (UnicodeError, LookupError, IOError):
pass
# try config file (or before ask user?)
if text == None and self.opensEncoding:
try:
text = open(file, 'r', encoding=self.opensEncoding).read()
self.knownEncoding = self.opensEncoding
except (UnicodeError, LookupError, IOError):
pass
# try platform default (utf-8 on windows; try utf8 always?)
if text == None:
try:
text = open(file, 'r', encoding=sys.getdefaultencoding()).read()
self.knownEncoding = sys.getdefaultencoding()
except (UnicodeError, LookupError, IOError):
pass
# last resort: use binary bytes and rely on Tk to decode
if text == None:
try:
text = open(file, 'rb').read() # bytes for Unicode
text = text.replace(b'\r\n', b'\n') # for display, saves
self.knownEncoding = None
except IOError:
pass
if text == None:
showerror('PyEdit', 'Could not decode and open file ' + file)
else:
self.setAllText(text)
self.setFileName(file)
self.text.edit_reset() # 2.0: clear undo/redo stks
self.text.edit_modified(0) # 2.0: clear modified flag
def onSave(self):
self.onSaveAs(self.currfile) # may be None
def onSaveAs(self, forcefile=None):
"""
2.1: total rewrite for Unicode support: Text content is always
returned as a str, so we must deal with encodings to save to
a file here, regardless of open mode of the output file (binary
requires bytes, and text must encode); tries the encoding used
when opened or saved (if known), user input, config file setting,
and platform default last; most users can use platform default;
retains successful encoding name here for next save, because this
may be the first Save after New or a manual text insertion; Save
and SaveAs may both use last known encoding, per config file (it
probably should be used for Save, but SaveAs usage is unclear);
gui prompts are prefilled with the known encoding if there is one;
BOOK: Programming Python
8.26Mb size Format: txt, pdf, ePub
ads

Other books

Frankenstein Theory by Jack Wallen
A Well-Paid Slave by Brad Snyder
Snowy Encounters by Clarissa Yip
Shadow of the Vampire by Meagan Hatfield
A Breath of Frost by Alyxandra Harvey