Let’s step back from coding details for just a moment to gain some design perspective. As we’ve seen, Python code, by and large, automatically lends itself to systems that are easy to read and maintain; it has a simple syntax that cuts much of the clutter of other tools. On the other hand, coding styles and program design can often affect maintainability as much as syntax. For example, the “Hello World” selector pages of the preceding section work as advertised and were very easy and fast to throw together. But as currently coded, the languages selector suffers from substantial maintainability flaws.
Imagine, for instance, that you actually take me up on that challenge posed at the end of the last section, and attempt to add another entry for COBOL. If you add COBOL to the CGI script’s table, you’re only half done: the list of supported languages lives redundantly in two places—in the HTML for the main page as well as in the script’s syntax dictionary. Changing one does not change the other. More generally, there are a handful of ways that this program might fail the scrutiny of a rigorous code review. These are described next.
As just mentioned, the list of languages supported by this program lives in two places: the HTML file and the CGI script’s table.
The field name of the input parameter, language
, is hardcoded into both files
as well. You might remember to change it in the other if you
change it in one, but you might not.
We’ve redundantly coded classes to mock-up form field inputs twice in this chapter already; the “dummy” class here is clearly a mechanism worth reusing.
HTML embedded in and generated by the script is sprinkled
throughout the program in print
statements, making it difficult
to implement broad web page layout changes or delegate web page
design to nonprogrammers.
This is a short example, of course, but issues of redundancy and reuse become more acute as your scripts grow larger. As a rule of thumb, if you find yourself changing multiple source files to modify a single behavior, or if you notice that you’ve taken to writing programs by cut-and-paste copying of existing code, it’s probably time to think about more rational program structures. To illustrate coding styles and practices that are friendlier to maintainers, let’s rewrite (that is, refactor) this example to fix all of these weaknesses in a single mutation.
We can remove the first two maintenance problems listed earlier with a simple transformation; the trick is to generate the main page dynamically, from an executable script, rather than from a precoded HTML file. Within a script, we can import the input field name and selection list values from a common Python module file, shared by the main and reply page generation scripts. Changing the selection list or field name in the common module changes both clients automatically. First, we move shared objects to a common module file, as shown in Example 16-19.
Example 16-19. PP3EInternetWebcgi-binlanguages2common.py
######################################################## # common objects shared by main and reply page scripts; # need change only this file to add a new language. ######################################################## inputkey = 'language' # input parameter name hellos = { 'Python': r" print 'Hello World' ", 'Perl': r' print "Hello World "; ', 'Tcl': r' puts "Hello World" ', 'Scheme': r' (display "Hello World") (newline) ', 'SmallTalk': r" 'Hello World' print. ", 'Java': r' System.out.println("Hello World"); ', 'C': r' printf("Hello World "); ', 'C++': r' cout << "Hello World" << endl; ', 'Basic': r' 10 PRINT "Hello World" ', 'Fortran': r" print *, 'Hello World' ", 'Pascal': r" WriteLn('Hello World'), " }
The module languages2common
contains all the data that needs to agree between pages: the field
name as well as the syntax dictionary. The hellos
syntax dictionary isn’t quite HTML
code, but its keys list can be used to generate HTML for the
selection list on the main page dynamically.
Notice that this module is stored in the same cgi-bin directory as the CGI scripts that will use it; this makes import search paths simple—the module will be found in the script’s current working directory, without path configuration. In general, external references in CGI scripts are resolved as follows:
Module imports will be relative to the CGI script’s current working directory (cgi-bin), plus any custom path setting in place when the script runs.
When using minimal URLs, referenced pages and scripts in links and form actions within generated HTML are relative to the prior page’s location as usual. For a CGI script, such minimal URLs are relative to the location of the generating script itself.
Filenames referenced in query parameters and passed into scripts are normally relative to the directory containing the CGI script (cgi-bin). However, on some platforms and servers they may be relative to the web server’s directory instead—see the note at the end of this section. For our local web server, the latter case applies.
Next, in Example 16-20, we recode the main page as an executable script, and populate the response HTML with values imported from the common module file in the previous example.
Example 16-20. PP3EInternetWebcgi-binlanguages2.py
#!/usr/bin/python ################################################################# # generate HTML for main page dynamically from an executable # Python script, not a precoded HTML file; this lets us # import the expected input field name and the selection table # values from a common Python module file; changes in either # now only have to be made in one place, the Python module file; ################################################################# REPLY = """Content-type: text/html <html><title>Languages2</title> <body> <h1>Hello World selector</h1> <P>Similar to file <a href="../languages.html">languages.html</a>, but this page is dynamically generated by a Python CGI script, using selection list and input field names imported from a common Python module on the server. Only the common module must be maintained as new languages are added, because it is shared with the reply script. To see the code that generates this page and the reply, click <a href="getfile.py?filename=cgi-bin/languages2.py">here</a>, <a href="getfile.py?filename=cgi-bin/languages2reply.py">here</a>, <a href="getfile.py?filename=cgi-bin/languages2common.py">here</a>, and <a href="getfile.py?filename=cgi-bin/formMockup.py">here</a>.</P> <hr> <form method=POST action="languages2reply.py"> <P><B>Select a programming language:</B> <P><select name=%s> <option>All %s <option>Other </select> <P><input type=Submit> </form> </body></html> """ from languages2common import hellos, inputkey options = [] for lang in hellos.keys( ): # we could sort this too options.append('<option>' + lang) # wrap table keys in HTML code options = ' '.join(options) print REPLY % (inputkey, options) # field name and values from module
Again, ignore the getfile
hyperlinks in this file for now; we’ll learn what they mean in a
later section. You should notice, though, that the HTML page
definition becomes a printed Python string here (named REPLY
), with %s
format targets where we plug in values
imported from the common module. It’s otherwise similar to the
original HTML file’s code; when we visit this script’s URL, we get a
similar page, shown in Figure
16-25. But this time, the page is generated by running a
script on the server that populates the pull-down selection list
from the keys list of the common syntax table. Use your browser’s
View Source option to see the HTML generated; it’s nearly identical
to the HTML file in Example
16-17.
One maintenance note here: the content of the REPLY
HTML code template string in Example 16-20 could be loaded
from an external text file so that it could be worked on
independently of the Python program logic. In general, though,
external text files are no more easily changed than Python scripts.
In fact, Python scripts are text files, and
this is a major feature of the language—it’s easy to change the
Python scripts of an installed system onsite, without recompile or
relink steps. However, external HTML files could be checked out
separately in a source-control system, if this matters in your
environment.
A subtle issue worth mentioning: on Windows, the locally running web server of Example 16-1 that we’re using in this chapter runs CGI scripts in the same process as the web server. Unfortunately, it fails to temporarily change to the home directory of a CGI script it runs, and this cannot be easily customized by subclassing. As a result, just on platforms where the server runs CGI scripts in-process, filenames passed as parameters to scripts are relative to the web server’s current working directory (which is one level up from the scripts’ cgi-bin directory).
This seems like a flaw in the Python CGI server classes,
because behavior will differ on other platforms. When CGI scripts
are launched elsewhere, they run in the directory in which they
are located, such that filename parameters will be relative to
cgi-bin, the current working directory. This
is also why we augmented Example 16-1 to insert the
cgi-bin directory on sys.path
: this step emulates the current
working directory path setting on platforms where scripts are
separate processes. This applies only to module imports, though,
not to relative file paths.
If you use Example
16-1 on a platform where CGI scripts are run in separate
processes, or if the Python web server classes are ever fixed to
change to the CGI script’s home directory, the filename
query parameters in Example 16-20 will need to
be changed to omit the cgi-bin component. Of
course, for other servers, URL paths may be arbitrarily different
from those you see in this book anyhow.
Moving the languages table and input field name to a module file solves the first two maintenance problems we noted. But if we want to avoid writing a dummy field mock-up class in every CGI script we write, we need to do something more. Again, it’s merely a matter of exploiting the Python module’s affinity for code reuse: let’s move the dummy class to a utility module, as in Example 16-21.
Example 16-21. PP3EInternetWebcgi-binformMockup.py
############################################################## # Tools for simulating the result of a cgi.FieldStorage( ) # call; useful for testing CGI scripts outside the Web ############################################################## class FieldMockup: # mocked-up input object def _ _init_ _(self, str): self.value = str def formMockup(**kwargs): # pass field=value args mockup = {} # multichoice: [value,...] for (key, value) in kwargs.items( ): if type(value) != list: # simple fields have .value mockup[key] = FieldMockup(str(value)) else: # multichoice have list mockup[key] = [] # to do: file upload fields for pick in value: mockup[key].append(FieldMockup(pick)) return mockup def selftest( ): # use this form if fields can be hardcoded form = formMockup(name='Bob', job='hacker', food=['Spam', 'eggs', 'ham']) print form['name'].value print form['job'].value for item in form['food']: print item.value, # use real dict if keys are in variables or computed print form = {'name':FieldMockup('Brian'), 'age':FieldMockup(38)} for key in form.keys( ): print form[key].value if _ _name_ _ == '_ _main_ _': selftest( )
When we place our mock-up class in the module formMockup.py
, it automatically becomes a
reusable tool and may be imported by any script we care to
write.[*] For readability, the dummy
field simulation class has been
renamed FieldMockup
here. For
convenience, we’ve also added a formMockup
utility function that builds up
an entire form dictionary from passed-in keyword arguments. Assuming
you can hardcode the names of the form to be faked, the mock-up can
be created in a single call. This module includes a self-test
function invoked when the file is run from the command line, which
demonstrates how its exports are used. Here is its test output,
generated by making and querying two form mock-up objects:
C:...PP3EInternetWebcgi-bin>python formMockup.py
Bob
hacker
Spam eggs ham
38
Brian
Since the mock-up now lives in a module, we can reuse it anytime we want to test a CGI script offline. To illustrate, the script in Example 16-22 is a rewrite of the tutor5.py example we saw earlier, using the form mock-up utility to simulate field inputs. If we had planned ahead, we could have tested the script like this without even needing to connect to the Net.
Example 16-22. PP3EInternetWebcgi-bin utor5_mockup.py
#!/usr/bin/python ################################################################## # run tutor5 logic with formMockup instead of cgi.FieldStorage( ) # to test: python tutor5_mockup.py > temp.html, and open temp.html ################################################################## from formMockup import formMockup form = formMockup(name='Bob', shoesize='Small', language=['Python', 'C++', 'HTML'], comment='ni, Ni, NI') # rest same as original, less form assignment
Running this script from a simple command line shows us what the HTML response stream will look like:
C:...PP3EInternetWebcgi-bin>python tutor5_mockup.py
Content-type: text/html
<TITLE>tutor5.py</TITLE>
<H1>Greetings</H1>
<HR>
<H4>Your name is Bob</H4>
<H4>You wear rather Small shoes</H4>
<H4>Your current job: (unknown)</H4>
<H4>You program in Python and C++ and HTML</H4>
<H4>You also said:</H4>
<P>ni, Ni, NI</P>
<HR>
Running it live yields the page in Figure 16-26. Field inputs are hardcoded, similar in spirit to the tutor5 extension that embedded input parameters at the end of hyperlink URLs. Here, they come from form mock-up objects created in the reply script that cannot be changed without editing the script. Because Python code runs immediately, though, modifying a Python script during the debug cycle goes as quickly as you can type.
There’s one last step on our path to software maintenance nirvana: we must recode the reply page script itself to import data that was factored out to the common module and import the reusable form mock-up module’s tools. While we’re at it, we move code into functions (in case we ever put things in this file that we’d like to import in another script), and all HTML code to triple-quoted string blocks. The result is Example 16-23. Changing HTML is generally easier when it has been isolated in single strings like this, instead of being sprinkled throughout a program.
Example 16-23. PP3EInternetWebcgi-binlanguages2reply.py
#!/usr/bin/python ######################################################### # for easier maintenance, use HTML template strings, get # the language table and input key from common module file, # and get reusable form field mockup utilities module. ######################################################### import cgi, sys from formMockup import FieldMockup # input field simulator from languages2common import hellos, inputkey # get common table, name debugme = False hdrhtml = """Content-type: text/html <TITLE>Languages</TITLE> <H1>Syntax</H1><HR>""" langhtml = """ <H3>%s</H3><P><PRE> %s </PRE></P><BR>""" def showHello(form): # HTML for one language choice = form[inputkey].value # escape lang name too try: print langhtml % (cgi.escape(choice), cgi.escape(hellos[choice])) except KeyError: print langhtml % (cgi.escape(choice), "Sorry--I don't know that language") def main( ): if debugme: form = {inputkey: FieldMockup(sys.argv[1])} # name on cmd line else: form = cgi.FieldStorage( ) # parse real inputs print hdrhtml if not form.has_key(inputkey) or form[inputkey].value == 'All': for lang in hellos.keys( ): mock = {inputkey: FieldMockup(lang)} showHello(mock) else: showHello(form) print '<HR>' if _ _name_ _ == '_ _main_ _': main( )
When global debugme
is set
to True
, the script can be tested
offline from a simple command line as before:
C:...PP3EInternetWebcgi-bin>python languages2reply.py Python
Content-type: text/html
<TITLE>Languages</TITLE>
<H1>Syntax</H1><HR>
<H3>Python</H3><P><PRE>
print 'Hello World'
</PRE></P><BR>
<HR>
When run online, we get the same reply pages we saw for the original version of this example (we won’t repeat them here again). This transformation changed the program’s architecture, not its user interface.
Most of the code changes in this version of the reply script are straightforward. If you test-drive these pages, the only differences you’ll find are the URLs at the top of your browser (they’re different files, after all), extra blank lines in the generated HTML (ignored by the browser), and a potentially different ordering of language names in the main page’s pull-down selection list.
This selection list ordering difference arises because this
version relies on the order of the Python dictionary’s keys list,
not on a hardcoded list in an HTML file. Dictionaries, you’ll
recall, arbitrarily order entries for fast fetches; if you want the
selection list to be more predictable, simply sort the keys list
before iterating over it using the list sort
method, or the sorted
function introduced in Python
2.4:
for lang in sorted(hellos): # dict iterator instead of .keys( ) mock = {inputkey: FieldMockup(lang)}
[*] Assuming, of course, that this module can be found on the
Python module search path when those scripts are run. See the
CGI search path discussion earlier in this chapter. Since Python
searches the current directory for imported modules by default,
this always works without sys.path
changes if all of our files
are in our main web directory. For other applications, we may
need to add this directory to PYTHONPATH
, or use package (directory
path) imports.
3.137.186.178