Section 16.7. Refactoring Code for Maintainability


16.7. Refactoring Code for Maintainability

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 placesin 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.


Selection list

As just mentioned, the list of languages supported by this program lives in two places: the HTML file and the CGI script's table.


Field name

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.


Form mock-ups

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 code

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.

16.7.1. Step 1: Sharing Objects Between PagesA New Input Form

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. PP3E\Internet\Web\cgi-bin\languages2common.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\n";            ',     '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\n");          ',     '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 simplethe 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 insteadsee 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. PP3E\Internet\Web\cgi-bin\languages2.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 = '\n\t'.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.

Figure 16-25. Alternative main page made by languages2.py


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 languageit'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.


16.7.2. Step 2: A Reusable Form Mock-Up Utility

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. PP3E\Internet\Web\cgi-bin\formMockup.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:

[*] 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.

 C:\...\PP3E\Internet\Web\cgi-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. PP3E\Internet\Web\cgi-bin\tutor5_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:\...\PP3E\Internet\Web\cgi-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.

Figure 16-26. A response page with simulated inputs


16.7.3. Step 3: Putting It All TogetherA New Reply Script

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. PP3E\Internet\Web\cgi-bin\languages2reply.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\n <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:\...\PP3E\Internet\Web\cgi-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)} 

Faking Inputs with Shell Variables

If you know what you're doing, you can also test CGI scripts from the command line on some platforms by setting the same environment variables that HTTP servers set, and then launching your script. For example, we might be able to pretend to be a web server by storing input parameters in the QUERY_STRING environment variable, using the same syntax we employ at the end of a URL string after the ?:

 $ setenv QUERY_STRING "name=Mel&job=trainer,+writer" $ python tutor5.py Content-type: text/html <TITLE>tutor5.py<?TITLE> <H1>Greetings</H1> <HR> <H4>Your name is Mel</H4> <H4>You wear rather (unknown) shoes</H4> <H4>Your current job: trainer, writer</H4> <H4>You program in (unknown)</H4> <H4>You also said:</H4> <P>(unknown)</P> <HR> 

Here, we mimic the effects of a GET style form submission or explicit URL. HTTP servers place the query string (parameters) in the shell variable QUERY_STRING. Python's cgi module finds them there as though they were sent by a browser. POST-style inputs can be simulated with shell variables too, but it's more complexso much so that you may be better off not bothering to learn how. In fact, it may be more robust in general to mock up inputs with Python objects (e.g., as in formMockup.py). But some CGI scripts may have additional environment or testing constraints that merit unique treatment.





Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net