For other screenshots
showing PyEdit in action, see the coverage of the
following client programs:
PyDemos in
Chapter 10
deploys
PyEdit pop-ups to show source-code files.
PyView later in this chapter embeds PyEdit to display image
note files.
PyMailGUI in
Chapter 14
uses
PyEdit to display email text, text attachments, and source.
The last of these especially makes heavy use of PyEdit’s
functionality and includes screenshots showing PyEdit displaying
additional Unicode text with Internationalized character sets. In this
role, the text is either parsed from messages or loaded from temporary
files, with encodings determined by mail headers.
I’ve updated this
example in both the third and fourth editions of this
book. Because this chapter is intended to reflect realistic programming
practice, and because this example reflects that way that software
evolves over time, this section and the one following it provide a quick
rundown of some of the major changes made along the way to help you
study the code.
Since the current version inherits all the enhancements of the one
preceding it, let’s begin with the previous version’s additions. In the
third edition, PyEdit was enhanced with:
A simple font specification dialog
Unlimited undo and redo of editing operations
File modified tests whenever content might be erased or
changed
A user configurations module
Here are some quick notes about these extensions.
For the third edition
of the book, PyEdit grew a
font input
dialog
—a simple, three-entry, nonmodal dialog where you can
type the font family, size, and style, instead of picking them from a
list of preset options. Though functional, you can find more
sophisticated tkinter font selection dialogs in both the public domain
and within the implementation of Python’s standard IDLE development
GUI (as mentioned earlier, it is itself a Python/tkinter
program).
Also new in the
third edition, PyEdit supports unlimited edit
undo and redo
, as well as an accurate
modified
check before quit, open, run, and new
actions to prompt for saves. It now verifies exits or overwrites only
if text has been changed, instead of always asking naïvely. The
underlying Tk 8.4 (or later) library provides an API, which makes both
these enhancements simple—Tk keeps undo and redo stacks automatically.
They are enabled with theText
widget’sundo
configuration option
and are accessed with the widget methodsedit_undo
andedit_redo
. Similarly,edit_reset
clears the stacks (e.g., after a
new file open), andedit_modified
checks or sets the automatic text modified flag.
It’s also possible to undo cuts and pastes right after you’ve
done them (simply paste back from the clipboard or cut the pasted and
selected text), but the new undo/redo operations are more complete and
simpler to use. Undo was a suggested exercise in the second edition of
this book, but it has been made almost trivial by the new Tk
API.
For usability, the
third edition’s version of PyEdit also allows users to
set startup
configuration options
by assigning
variables in a module,textConfig.py
. If present on the module
search path when PyEdit is imported or run, these assignments give
initial values for font, colors, text window size, and search case
sensitivity. Fonts and colors can be changed interactively in the
menus and windows can be freely resized, so this is largely just a
convenience. Also note that this module’s settings will be inherited
by all instances of PyEdit if it is importable in the client
program—even when it is a pop-up window or an embedded component of
another application. Client applications may define their own version
or configure this file on the module search path per their
needs.
Besides the updates
described in the prior section, the following additional
enhancements were made for this current fourth edition of this
book:
PyEdit has been ported to run under Python 3.1, and its
tkinter library.
The nonmodal change and font dialogs were fixed to work better
if multiple instance windows are open: they now use per-dialog
state.
A Quit request in main windows now verifies program exit if
any other edit windows in the process have changed content, instead
of exiting silently.
There’s a new Grep menu option and dialog for searching
external files; searches are run in threads to avoid blocking the
GUI and to allow multiple searches to overlap in time and support
Unicode text.
There was a minor fix for initial positioning when text is
inserted initially into a newly created editor, reflecting a change
in underlying libraries.
The Run Code option for files now uses the base file name
instead of the full directory path after achdir
to better support relative paths;
allows for command-line arguments to code run from files; and
inherits a patch made in
Chapter 5
’slaunch
modes
which converts/
to\
in script paths. In addition, this option always now runs anupdate
between pop-up dialogs to
ensure proper display.
Perhaps most prominently, PyEdit now processes files in such a
way as to support display and editing of text with arbitrary
Unicode encodings
, to the extent allowed by the
underlying Tk GUI library for Unicode strings. Specifically, Unicode
is taken into account when opening and saving files; when displaying
text in the GUI; and when searching files in directories.
The following sections provide additional implementation notes on
these changes.
The
change
dialog in
the prior version saved its entry widgets on the text
editor object, which meant that the most recent change dialog’s fields
were used for every change dialog open. This could even lead to
program aborts for finds in an older change dialog window if newer
ones had been closed, since the closed window’s widgets had been
destroyed—an unanticipated usage mode, which has been present since at
least the second edition, and which I’d like to chalk up to operator
error, but which was really a lesson in state retention! The same
phenomenon existed in the
font
dialog—its most
recently opened instance stole the show, though its brute force
exception handler prevented program aborts (it issued error pop ups
instead). To fix, the change and font dialogs now send
per-dialog-window input fields as arguments to their callbacks. We
could instead allow just one of each dialog to be open, but that’s
less functional.
Though not quite
as grievous, PyEdit also used to ignore changes in other
open edit windows on Quit in main windows. As a policy, on a Quit in
the GUI, pop-up edit windows destroy themselves only, but main edit
windows run a tkinterquit
to end
the entire program. Although all windows verify closes if their own
content has changed, other edit windows were ignored in the prior
version—quitting a main window could lose changes in other windows
closed on program exit.
To do better, this version keeps a list of all open managed edit
windows in the process; on Quit in main windows it checks them all for
changes, and verifies exit if any have changed. This scheme isn’t
foolproof (it doesn’t address quits run on widgets outside PyEdit’s
scope), but it is an improvement. A more ultimate solution probably
involves redefining or intercepting tkinter’s ownquit
method. To avoid getting too detailed
here, I’ll defer more on this topic until later in this section (see
the
event coverage
ahead); also see the relevant comments near the end of PyEdit’s source
file for implementation notes.
In addition,
there is a new Grep option in the Search pull-down menu,
which implements an external file search tool. This tool scans an
entire directory tree for files whose names match a pattern, and which
contain a given search string. Names of matches are popped up in a new
nonmodal scrolled list window, with lines that identify all matches by
filename, line number, and line content. Clicking on a list item opens
the matched file in a new nonmodal and in-process PyEdit pop-up edit
window and automatically moves to and selects the line of the match.
This achieves its goal by reusing much code we wrote earlier:
Thefind
utility we wrote
in
Chapter 6
to do its tree
walking
The scrolled list utility we coded in
Chapter 9
for displaying
matches
The form row builder we wrote in
Chapter 10
for the nonmodal input
dialog
The existing PyEdit pop-up window mode logic to display
matched files on request
The existing PyEdit go-to callback and logic to move to the
matched line in a file
To avoid blocking the GUI
while files are searched during tree walks, Grep runs
searches in parallel
threads
. This also allows
multiple greps to be running at once and to overlap in time
arbitrarily (especially useful if you grep in larger trees, such as
Python’s own library or full source trees). The standard threads,
queues, andafter
timer loops
technique we learned in
Chapter 10
is
applied here—non-GUI producer threads find matches and place them on
a queue to be detected by a timer loop in the main GUI
thread.
As coded, a timer loop is run only when a grep is in progress,
and each grep uses its own thread, timer loop, and queue. There may
be multiple threads and loops running, and there may be other
unrelated threads, queues, and timer loops in the process. For
instance, an attached PyEdit component in
Chapter 14
’s PyMailGUI program can run grep
threads and loops of its own, while PyMailGUI runs its own
email-related threads and queue checker. Each loop’s handler is
dispatched independently from the tkinter event stream processor.
Because of the simpler structure here, the generalthreadtools
callback queue of
Chapter 10
is not used here. For more notes
on grep thread implementation see the source code ahead, and compare
to file
_unthreaded
-
text
Editor.py
in the examples package, a
nonthreaded version of PyEdit.
If you study the
Grep option’s code, you’ll notice that it also allows
input of a tree-wide Unicode encoding, and catches and skips any
Unicode decoding error exceptions generated both when processing
file content and walking the tree’s filenames. As we learned in
Chapters
4
and
6
, files opened in text mode in
Python 3.X must be decodable per a provided or platform default
Unicode encoding. This is particular problematic for Grep, as
directory trees may contain files of arbitrarily mixed encoding
types.
In fact, it’s common on Windows to have files with content in
ASCII, UTF-8, and UTF-16 form mixed in the same tree (Notepad’s
“ANSI,” “Utf-8,” and “Unicode”), and even others in trees that
contain content obtained from the Web or email. Opening all these
with UTF-8 would trigger exceptions in Python 3.X, and opening all
these in binary mode yields encoded text that will likely fail to
match a search key string. Technically, to compare at all, we’d
still have to decode the bytes read to text or encode the search key
string to bytes, and the two would only match if the encodings used
both succeed and agree.
To allow for mixed encoding trees, the Grep dialog opens in
text mode and allows an encoding name to be input and used to decode
file content for all files in the tree searched. This encoding name
is prefilled with the platform content default for convenience, as
this will often suffice. To search trees of mixed file types, users
may run multiple Greps with different encoding names. The names of
files searched might fail to decode as well, but this is largely
ignored in the current release: they are assumed to satisfy the
platform filename convention, and end the search if they don’t (see
Chapters
4
and
6
for more on filename encoding
issues in Python itself, as well as thefind
walker reused here).
In addition, Grep must take care to catch and recover from
encoding errors, since some files with matching names that it
searches might still not be decodable per the input encoding, and in
fact might not be text files at all. For example, searches in
Python 3.1’s
standard library
(like the example Grep for%
described earlier) run into a handful of files which do not decode
properly on my Windows machine and would otherwise crash PyEdit.
Binary files which happen to match the filename patterns would fare
even worse.
In general, programs can avoid Unicode encoding errors by
either catching exceptions or opening files in binary mode; since
Grep might not be able to interpret some of the files it visits as
text at all, it takes the former approach. Really, opening even text
files in binary mode to read raw byte strings in 3.X mimics the
behavior of text files in 2.X, and underscores why forcing programs
to deal with Unicode is sometimes a good
thing—
binary mode avoids decoding
exceptions, but probably shouldn’t, because the still-encoded text
might not work as expected. In this case, it might yield invalid
comparison results.
For more details on Grep’s Unicode support, and a set of open
issues and options surrounding it, see the source code listed ahead.
For a suggested enhancement, see also there
pattern matching module in
Chapter 19
—a tool we could use to search for
patterns instead of specific
strings.
Also in this version,
text editor updates itself before inserting text into
its text widget at construction time when it is passed an initial file
name in itsloadFirst
argument.
Sometime after the third edition and Python 2.5, either Tk or tkinter
changed such that inserting text before an update call caused the
scroll position to be off by one—the text editor started with line 2
at its top in this mode instead of line 1. This also occurs in the
third edition’s version of this example under Python 2.6, but not 2.5;
adding anupdate
correctly
positions at line 1 initially. Obscure but true in
the real world of library dependencies!
[
39
]
Clients of the classes here should alsoupdate
before manually inserting text into a
newly created (or packed) text editor object for accurate positioning;
PyView later in this chapter as well as PyMailGUI in
Chapter 14
now do. PyEdit doesn’t update itself
on every construction, because it may be created early by, or even
hidden in, an enclosing GUI (for instance, this would show a
half-complete window in PyView). Moreover, PyEdit could automaticallyupdate
itself at the start ofsetAllText
instead of requiring
this step of clients, but forcedupdate
is required only once initially after
being packed (not before each text insertion), and this too might be
an undesirable side effect in some conceivable use cases. As a rule of
thumb, adding unrelated operations to methods this way tends to limit
their scope.
The Run Code option
in the Tools menu was fixed in three ways that make it
more useful for running code being edited from its external file,
rather than in-process:
After changing to the file’s directory in order to make any
relative filenames in its code accurate, PyEdit now strips off any
directory path prefix in the file’s name before launching it,
because its original directory path may no longer be valid if it
is
relative
instead of absolute. For
instance, paths of files opened manually are absolute, but file
paths in PyDemos’s Code pop ups are all relative to the example
package root and would fail after achdir
.
PyEdit now correctly uses launcher tools that support
command-line arguments for file mode on Windows.
PyEdit inherits a fix in the underlyinglaunchmodes
module that changes forward
slashes in script path names to backslashes (though this was later
made a moot point by stripping relative path prefixes). PyEdit
gets by with forward slashes on Windows becauseopen
allows them, but some Windows
launch tools do not.
Additionally, for both code run from files and code run in
memory, this version adds anupdate
call between pop-up dialogs to ensure that later dialogs appear in all
cases (the second occasionally failed to pop up in rare contexts).
Even with these fixes, Run Code is useful but still not fully robust.
For example, if the edited code is not run from a file, it is run
in-process and not spawned off in a thread, and so may block the GUI.
It’s also not clear how best to handle import paths and directories
for files run in nonfile mode, or whether this mode is worth retaining
in general. Improve as desired.