Commits

Wojciech Malinowski committed a72f373

Imported from svn by Bitbucket

Comments (0)

Files changed (763)

+# .checksrc.config
+{
+    'DisableErrors': {
+        'UncapFN': ['clean', 'install'],
+    }
+}
+*.pyc
+*.pyo
+*.pyd
+*.config
+*.config.default
+install.log
+# lint Python modules using external checkers.
+#
+# This is the main checker controling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+#
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflict with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option conflict
+# with the disable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories.
+#enable-msg-cat=
+
+# Disable all messages in the listed categories.
+#disable-msg-cat=
+
+# Enable the message(s) with the given id(s).
+#enable-msg=
+
+# Disable the message(s) with the given id(s).
+#disable-msg=
+disable-msg=W0122,W0142,W0201,W0212,W0223,W0403,W0603,W0613,W0614,W0621,W0622,W0703
+
+
+[REPORTS]
+
+# set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=no
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells wether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note).You have access to the variables errors warning, statement which
+# respectivly contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# Enable the report(s) with the given id(s).
+#enable-report=
+
+# Disable the report(s) with the given id(s).
+#disable-report=
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branchs, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+#
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+#const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$
+const-rgx=(([a-z_][A-Za-z]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=_?[A-Z][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+#function-rgx=[a-z_][a-z0-9_]{2,30}$
+function-rgx=_?[a-z][a-zA-Z0-9]{2,30}$
+
+# Regular expression which should only match correct method names
+#method-rgx=[a-z_][a-z0-9_]{2,30}$
+method-rgx=(_?[a-z][a-zA-Z0-9]{2,30}|(__.*__))$
+
+# Regular expression which should only match correct instance attribute names
+#attr-rgx=[a-z_][a-z0-9_]{2,30}$
+attr-rgx=(_[a-z][a-zA-Z0-9]{2,30}$|(__.*__))$
+
+# Regular expression which should only match correct argument names
+#argument-rgx=[a-z_][a-z0-9_]{2,30}$
+argument-rgx=[a-z][a-zA-Z0-9]{2,30}$
+
+# Regular expression which should only match correct variable names
+#variable-rgx=[a-z_][a-z0-9_]{2,30}$
+variable-rgx=[a-z][a-zA-Z0-9]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+#good-names=i,j,k,ex,Run,_
+good-names=d,e,f,g,h,i,j,k,m,n,p,q,r,s,t,v,w,x,y,z,dt,ex,fp,fs,ht,id,ip,kv,kw,lm,pp,td,th,tm,tr,rv,wr,Run,_,_id
+
+# Bad variable names which should always be refused, separated by a comma
+#bad-names=foo,bar,baz,toto,tutu,tata
+bad-names=l,o,bla,foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+#bad-functions=map,filter,apply,input
+bad-functions=apply,input
+
+
+# try to find bugs in the code using type inference
+#
+[TYPECHECK]
+
+# Tells wether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# When zope mode is activated, consider the acquired-members option to ignore
+# access to some undefined attributes.
+zope=no
+
+# List of members which are usually get through zope's acquisition mecanism and
+# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
+acquired-members=REQUEST,acl_users,aq_parent
+
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assigment
+#
+[VARIABLES]
+
+# Tells wether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existant members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+#
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+#
+[DESIGN]
+
+# Maximum number of arguments for function / method
+#max-args=5
+max-args=10
+
+# Maximum number of locals for function / method body
+#max-locals=15
+max-locals=30
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+#max-branchs=12
+max-branchs=30
+
+# Maximum number of statements in function / method body
+#max-statements=50
+max-statements=150
+
+# Maximum number of parents for a class (see R0901).
+#max-parents=7
+max-parents=10
+
+# Maximum number of attributes for a class (see R0902).
+#max-attributes=7
+max-attributes=25
+
+# Minimum number of public methods for a class (see R0903).
+#min-public-methods=2
+min-public-methods=1
+
+# Maximum number of public methods for a class (see R0904).
+#max-public-methods=20
+max-public-methods=100
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+#
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+#
+[FORMAT]
+
+# Maximum number of characters on a single line.
+#max-line-length=80
+max-line-length=100
+
+# Maximum number of lines in a module
+#max-module-lines=1000
+max-module-lines=1500
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+#
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=@@,FIXME,TODO,XXX,
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+#
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+#!/usr/bin/env python
+
+"""AllTests.py - This module runs the automated tests in all the components.
+
+To run specific test cases, pass one or more names of package/module names
+on the command line which contain the test cases to be run.
+
+Usage:
+    python AllTests.py                  - Runs all the unittests
+    python AllTests.py mypackage.MyFile - Runs the tests in 'mypackage/MyFile'
+
+"""
+
+
+alltestnames = [
+
+    'WebUtils.Tests.TestFuncs',
+    'WebUtils.Tests.TestHTMLTag.makeTestSuite',
+
+    'MiscUtils.Tests.TestCSVParser',
+    'MiscUtils.Tests.TestNamedValueAccess.makeTestSuite',
+    'MiscUtils.Tests.TestError',
+    'MiscUtils.Tests.TestFuncs',
+    'MiscUtils.Tests.TestPickleCache',
+    'MiscUtils.Tests.TestDataTable',
+    'MiscUtils.Tests.TestDateInterval',
+    'MiscUtils.Tests.TestDateParser',
+    'MiscUtils.Tests.TestDictForArgs',
+
+    'WebKit.Tests.SessionStoreTest',
+    'WebKit.Tests.Basic.Test',
+
+    'TaskKit.Tests.Test.makeTestSuite',
+
+    'PSP.Tests.TestContext',
+    'PSP.Tests.TestUtils',
+    'PSP.Tests.TestBraceConverter',
+    'PSP.Tests.TestCompiler',
+
+    'UserKit.Tests.ExampleTest',
+    'UserKit.Tests.RoleTest',
+    'UserKit.Tests.UserManagerTest.makeTestSuite',
+
+]
+
+import site
+import sys
+import unittest
+import logging
+
+
+if __name__ == '__main__':
+    # Configure logging
+    logging.basicConfig() # default level is WARN
+    print
+    print
+    # If no arguments are given, all of the test cases are run.
+    if len(sys.argv) == 1:
+        testnames = alltestnames
+        verbosity = 2
+        logging.getLogger().setLevel(logging.INFO)
+        print 'Loading all Webware Tests...'
+    else:
+        testnames = sys.argv[1:]
+        # Turn up verbosity and logging level
+        verbosity = 3
+        logging.getLogger().setLevel(logging.DEBUG)
+        print 'Loading tests %s...' % testnames
+
+    tests = unittest.TestSuite()
+
+    # We could just use defaultTestLoader.loadTestsFromNames(),
+    # but it doesn't give a good error message when it cannot load a test.
+    # So we load all tests individually and raise appropriate exceptions.
+    for test in testnames:
+        try:
+            tests.addTest(unittest.defaultTestLoader.loadTestsFromName(test))
+        except Exception:
+            print 'ERROR: Skipping tests from "%s".' % test
+            try: # just try to import the test after loadig failed
+                __import__(test)
+            except ImportError:
+                print 'Could not import the test module.'
+            else:
+                print 'Could not load the test suite.'
+            from traceback import print_exc
+            print_exc()
+
+    print
+    print 'Running the tests...'
+    unittest.TextTestRunner(verbosity=verbosity).run(tests)

CGIWrapper/.cvsignore

+*.pyc *.pyo

CGIWrapper/AdminPage.py

+
+
+class AdminPage(object):
+    """AdminPage
+
+    This is the abstract superclass of all CGI Wrapper administration CGI
+    classes. Subclasses typically override title() and writeBody(), but may
+    customize other methods. Subclasses use self._var for the various vars
+    that are passed in from CGI Wrapper and self.write() and self.writeln().
+
+    """
+
+
+    ## Init ##
+
+    def __init__(self, vars):
+        for name in vars:
+            setattr(self, '_' + name, vars[name])
+        self._vars = vars
+
+
+    ## HTML ##
+
+    def html(self):
+        self._html = []
+        self.writeHeader()
+        self.writeBody()
+        self.writeFooter()
+        return ''.join(self._html)
+
+
+    ## Utility methods ##
+
+    def write(self, *args):
+        for arg in args:
+            self._html.append(str(arg))
+
+    def writeln(self, *args):
+        for arg in args:
+            self._html.append(str(arg))
+        self._html.append('\n')
+
+
+    ## Content methods ##
+
+    def writeHeader(self):
+        self.writeln('''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+    <title>%s</title>
+</head>
+<body %s>
+        <table align="center" bgcolor="white"><tr><td>''' % (
+            self.title(), self.bodyTags()))
+        self.writeBanner()
+        self.writeToolbar()
+
+    def writeBody(self):
+        raise NotImplementedError('Should be overridden in a subclass')
+
+    def writeFooter(self):
+        self.writeln('''
+        <hr>
+        <div align="center" style="font-size:small">Webware for Python</div>
+        </td></tr></table>
+</body>
+</html>''')
+
+    def title(self):
+        raise NotImplementedError('Should be overridden in a subclass')
+
+    def bodyTags(self):
+        return 'text="black" bgcolor="#555555"'
+
+    def writeBanner(self):
+        self.writeln('''
+        <table align="center" bgcolor="#202080" cellpadding="5" cellspacing="0" width="100%%">
+            <tr><td align="center" style="color:white;font-weight:bold;font-family:Tahoma,Verdana,Arial,Helvetica,sans-serif">
+                <div style="font-size:14pt">CGI Wrapper</div>
+                <div style="font-size:16pt">%s</div>
+            </td></tr>
+        </table>''' % self.title())
+
+    def writeToolbar(self):
+        pass

CGIWrapper/CGIWrapper.py

+#!/usr/bin/env python
+
+"""CGIWrapper.py
+
+Webware for Python
+
+See the CGIWrapper.html documentation for more information.
+
+"""
+
+# We first record the starting time, in case we're being run as a CGI script.
+from time import time, localtime, asctime
+serverStartTime = time()
+
+# Some imports
+import cgi, os, sys, traceback
+from random import randint
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+if '' not in sys.path:
+    sys.path.insert(0, '')
+
+from Properties import version
+
+try:
+    import WebUtils
+except ImportError:
+    sys.path.append(os.path.abspath('..'))
+    import WebUtils
+
+from WebUtils.HTMLForException import HTMLForException
+
+from MiscUtils.NamedValueAccess import valueForName
+
+
+class CGIWrapper(object):
+    """The CGI Wrapper class.
+
+    A CGI wrapper executes a target script and provides various services
+    for both the script and website developer and the administrator.
+
+    See the CGIWrapper.html documentation for full information.
+
+    """
+
+
+    ## Init ##
+
+    def __init__(self):
+        self._config = self.config()
+
+
+    ## Configuration ##
+
+    def defaultConfig(self):
+        """Return a dictionary with the default configuration.
+
+        Subclasses could override to customize the values
+        or where they're taken from.
+
+        """
+        return dict(
+            ScriptsHomeDir = 'Examples',
+            ChangeDir = True,
+            ExtraPaths = [],
+            ExtraPathsIndex = 1,
+            LogScripts = True,
+            ScriptLogFilename = 'Scripts.csv',
+            ScriptLogColumns = [
+                'environ.REMOTE_ADDR',
+                'environ.REQUEST_METHOD', 'environ.REQUEST_URI',
+                'responseSize', 'scriptName',
+                'serverStartTimeStamp', 'serverDuration',
+                'scriptDuration', 'errorOccurred'
+            ],
+            ClassNames = ['', 'Page'],
+            ShowDebugInfoOnErrors = True,
+            UserErrorMessage = 'The site is having technical difficulties'
+                ' with this page. An error has been logged, and the problem'
+                ' will be fixed as soon as possible. Sorry!',
+            LogErrors = True,
+            ErrorLogFilename = 'Errors.csv',
+            SaveErrorMessages = True,
+            ErrorMessagesDir = 'ErrorMsgs',
+            EmailErrors = False,
+            ErrorEmailServer = 'localhost',
+            ErrorEmailHeaders = {
+                'From': 'webware@mydomain',
+                'To': ['webware@mydomain'],
+                'Reply-To': 'webware@mydomain',
+                'Content-Type': 'text/html',
+                'Subject': 'Error'
+            },
+            AdminRemoteAddr = ['127.0.0.1']
+        )
+
+    def configFilename(self):
+        """Return the filename of the optional configuration file."""
+        return 'CGIWrapper.config'
+
+    def userConfig(self):
+        """Return a dictionary with the user configuration.
+
+        This are overrides found in the optional configuration file,
+        or {} if there is no such file. The config filename is taken
+        from configFilename().
+
+        """
+        try:
+            f = open(self.configFilename())
+        except IOError:
+            return {}
+        else:
+            config = f.read()
+            config = eval('dict(%s)' % config)
+            f.close()
+            assert isinstance(config, dict)
+            return config
+
+    def config(self):
+        """Return the configuration for the CGIWrapper.
+
+        This is a combination of defaultConfig() and userConfig().
+        This method does no caching.
+
+        """
+        config = self.defaultConfig()
+        config.update(self.userConfig())
+        return config
+
+    def setting(self, name):
+        """Return the value of a particular setting in the configuration."""
+        return self._config[name]
+
+
+    ## Utilities ##
+
+    def docType(self):
+        return docType()
+
+    def makeHeaders(self):
+        """Return a default header dictionary with Content-Type entry."""
+        return {'Content-Type': 'text/html'}
+
+    def makeFieldStorage(self):
+        """Return a default field storage object created from the cgi module."""
+        return cgi.FieldStorage()
+
+    def enhanceThePath(self):
+        """Enhance sys.path according to our configuration."""
+        extraPathsIndex = self.setting('ExtraPathsIndex')
+        sys.path[extraPathsIndex:extraPathsIndex] = self.setting('ExtraPaths')
+
+    def environ(self):
+        """Get the environment for the request."""
+        return self._environ
+
+    def requireEnvs(self, names):
+        """Check that given environment variable names exist.
+
+        If they don't, a basic HTML error message is printed and we exit.
+
+        """
+        badNames = [name for name in names if name not in self._environ]
+        if badNames:
+            print 'Content-Type: text/html'
+            print
+            print docType()
+            print '<html><head><title>Error</title></head><body>'
+            print '<p>ERROR: Missing %s</p>' % ', '.join(badNames)
+            print '</body></html>'
+            sys.exit(0)
+
+    def scriptPathname(self):
+        """Return the full pathname of the target script.
+
+        Scripts that start with an underscore are special -- they run
+        out of the same directory as the CGI Wrapper and are typically
+        CGI Wrapper support scripts.
+
+        """
+        # remove the CGI Wrapper's filename part
+        pathname = os.path.split(self._environ['SCRIPT_FILENAME'])[0]
+        filename = self._environ['PATH_INFO'][1:]
+        ext = os.path.splitext(filename)[1]
+        if ext:
+            # Hmmm, some kind of extension like maybe '.html'.
+            # Leave out the 'ScriptsHomeDir' and leave the extension alone.
+            filename = os.path.join(pathname, filename)
+            self._servingScript = False
+        else:
+            # No extension - we assume a Python CGI script.
+            if filename.startswith('_'):
+                # Underscores denote private scripts packaged with CGI Wrapper,
+                # such as '_admin.py'.
+                if self.setting('AdminRemoteAddr'):
+                    # Users with the wrong remote address are redirected
+                    # to the access denied script.
+                    self.requireEnvs(['REMOTE_ADDR'])
+                    remoteAddr = self._environ['REMOTE_ADDR'] + '.'
+                    for addr in self.setting('AdminRemoteAddr'):
+                        if remoteAddr.startswith(addr + '.'):
+                            break
+                    else:
+                        filename = '_accessDenied'
+                filename = os.path.join(pathname, filename + '.py')
+            else:
+                # All other scripts are based in the directory named
+                # by the 'ScriptsHomeDir' setting.
+                filename = os.path.join(pathname,
+                    self.setting('ScriptsHomeDir'), filename + '.py')
+            self._servingScript = True
+        return filename
+
+    def writeScriptLog(self):
+        """Write an entry to the script log file.
+
+        Uses settings ScriptLogFilename and ScriptLogColumns.
+
+        """
+        filename = self.setting('ScriptLogFilename')
+        if os.path.exists(filename):
+            f = open(filename, 'a')
+        else:
+            f = open(filename, 'w')
+            f.write(','.join(self.setting('ScriptLogColumns')) + '\n')
+        values = []
+        for column in self.setting('ScriptLogColumns'):
+            value = valueForName(self, column)
+            if isinstance(value, float):
+                # might need more flexibility in the future
+                value = '%0.4f' % value
+            else:
+                value = str(value)
+            values.append(value)
+        f.write(','.join(values) + '\n')
+        f.close()
+
+    def version(self):
+        return '.'.join(map(str, version))
+
+
+    ## Exception handling ##
+
+    def handleException(self, excInfo):
+        """Handle an exception in the target script.
+
+        Invoked by self when an exception occurs in the target script.
+        <code>excInfo</code> is a sys.exc_info()-style tuple of information
+        about the exception.
+
+        """
+        # Note the duration of the script and time of the exception
+        self._scriptEndTime = time()
+        self.logExceptionToConsole()
+        self.reset()
+        print self.htmlErrorPage(
+            showDebugInfo=self.setting('ShowDebugInfoOnErrors'))
+        fullErrorMsg = None
+        if self.setting('SaveErrorMessages'):
+            fullErrorMsg = self.htmlErrorPage(showDebugInfo=True)
+            filename = self.saveHTMLErrorPage(fullErrorMsg)
+        else:
+            filename = ''
+        self.logExceptionToDisk(filename)
+        if self.setting('EmailErrors'):
+            if fullErrorMsg is None:
+                fullErrorMsg = self.htmlErrorPage(showDebugInfo=True)
+            self.emailException(fullErrorMsg)
+
+    def logExceptionToConsole(self, stderr=sys.stderr):
+        """Log an exception in the target script.
+
+        Logs the time, script name and traceback to the console
+        (typically stderr). This usually results in the information
+        appearing in the web server's error log. Used by handleException().
+
+        """
+        # stderr logging
+        stderr.write('[%s] [error] CGI Wrapper:'
+            ' Error while executing script %s\n' % (
+            asctime(localtime(self._scriptEndTime)), self._scriptPathname))
+        traceback.print_exc(file=stderr)
+
+    def reset(self):
+        """Reset CGI output.
+
+        Used by handleException() to clear out the current CGI output results
+        in preparation of delivering an HTML error message page.
+        Currently resets headers and deletes cookies, if present.
+
+        """
+        # Set headers to basic text/html. We don't want stray headers
+        # from a script that failed.
+        self._headers = self.makeHeaders()
+        # Get rid of cookies, too
+        if 'cookies' in self._namespace:
+            del self._namespace['cookies']
+
+    def htmlErrorPage(self, showDebugInfo=True):
+        """Return an HTML page explaining that there is an error.
+
+        There could be more options in the future, so using named arguments
+        (e.g. showDebugInfo=False) is recommended. Invoked by handleException().
+
+        """
+        html = ['''%s
+<html>
+<title>Error</title>
+<body text="black" bgcolor="white">
+%s<p>%s</p>
+''' % (docType(), htTitle('Error'), self.setting('UserErrorMessage'))]
+
+        if self.setting('ShowDebugInfoOnErrors'):
+            html.append(self.htmlDebugInfo())
+
+        html.append('</body></html>')
+        return ''.join(html)
+
+    def htmlDebugInfo(self):
+        """Return an HTML page with debugging info on the current exception.
+
+        Used by handleException().
+
+        """
+        html = ['''
+%s<p><i>%s</i></p>
+''' % (htTitle('Traceback'), self._scriptPathname)]
+        html.append(HTMLForException())
+        html.extend([
+            htTitle('Misc Info'),
+            htDictionary({
+                'time': asctime(localtime(self._scriptEndTime)),
+                'filename': self._scriptPathname,
+                'os.getcwd()': os.getcwd(),
+                'sys.path': sys.path
+            }),
+            htTitle('Fields'), htDictionary(self._fields),
+            htTitle('Headers'), htDictionary(self._headers),
+            htTitle('Environment'), htDictionary(self._environ, {'PATH': ';'}),
+            htTitle('Ids'), htTable(osIdTable(), ['name', 'value'])])
+        return ''.join(html)
+
+    def saveHTMLErrorPage(self, html):
+        """Save the given HTML error page for later viewing by the developer.
+
+        Returns the filename used. Invoked by handleException().
+
+        """
+        dir = self.setting('ErrorMessagesDir')
+        if not os.path.exists(dir):
+            os.makedirs(dir)
+        filename = os.path.join(dir, self.htmlErrorPageFilename())
+        try:
+            f = open(filename, 'w')
+            try:
+                f.write(html)
+            finally:
+                f.close()
+        except IOError:
+            sys.stderr.write('[%s] [error] CGI Wrapper: Cannot save error page (%s)\n'
+                % (asctime(localtime(time())), filename))
+        else:
+            return filename
+
+    def htmlErrorPageFilename(self):
+        """Construct a filename for an HTML error page.
+
+        This filename does not include the 'ErrorMessagesDir' setting.
+
+        """
+        # Note: Using the timestamp and a random number is a poor technique
+        # for filename uniqueness, but it is fast and good enough in practice.
+        return 'Error-%s-%s-%06d.html' % (os.path.split(self._scriptPathname)[1],
+            '-'.join(map(lambda x: '%02d' % x, localtime(self._scriptEndTime)[:6])),
+            randint(0, 999999))
+
+    def logExceptionToDisk(self, errorMsgFilename=None, excInfo=None):
+        """Write exception info to the log file.
+
+        Writes a tuple containing (date-time, filename, pathname,
+        exception-name, exception-data, error report filename)
+        to the errors file (typically 'Errors.csv') in CSV format.
+        Invoked by handleException().
+
+        """
+        if not excInfo:
+            excInfo = sys.exc_info()
+        err, msg = excInfo[:2]
+        if isinstance(err, basestring): # string exception
+            err, msg = '', str(msg or err)
+        else:
+            err, msg = err.__name__, str(msg)
+        logline = (asctime(localtime(self._scriptEndTime)),
+            os.path.split(self._scriptPathname)[1], self._scriptPathname,
+            err, msg, errorMsgFilename or '')
+        def fixElement(element):
+            element = str(element)
+            if ',' in element or '"' in element:
+                element = element.replace('"', '""')
+                element = '"%s"' % element
+            return element
+        logline = map(fixElement, logline)
+        filename = self.setting('ErrorLogFilename')
+        if os.path.exists(filename):
+            f = open(filename, 'a')
+        else:
+            f = open(filename, 'w')
+            f.write('time,filename,pathname,exception name,'
+                'exception data,error report filename\n')
+        f.write(','.join(logline) + '\n')
+        f.close()
+
+    def emailException(self, html, excInfo=None):
+        """Email an exception."""
+        # Construct the message
+        if not excInfo:
+            excInfo = sys.exc_info()
+        headers = self.setting('ErrorEmailHeaders')
+        msg = []
+        for key in headers:
+            if key != 'From' and key != 'To':
+                msg.append('%s: %s\n' % (key, headers[key]))
+        msg.append('\n')
+        msg.append(html)
+        msg = ''.join(msg)
+        # Send the message
+        import smtplib
+        server = smtplib.SMTP(self.setting('ErrorEmailServer'))
+        server.set_debuglevel(0)
+        server.sendmail(headers['From'], headers['To'], msg)
+        server.quit()
+
+
+    ## Serve ##
+
+    def serve(self, environ=os.environ):
+        """Serve a request."""
+        # Record the time
+        if 'isMain' in globals():
+            self._serverStartTime = serverStartTime
+        else:
+            self._serverStartTime = time()
+        self._serverStartTimeStamp = asctime(localtime(self._serverStartTime))
+
+        # Set up environment
+        self._environ = environ
+
+        # Ensure that filenames and paths have been provided
+        self.requireEnvs(['SCRIPT_FILENAME', 'PATH_INFO'])
+
+        # Set up the namespace
+        self._headers = self.makeHeaders()
+        self._fields = self.makeFieldStorage()
+        self._scriptPathname = self.scriptPathname()
+        self._scriptName = os.path.split(self._scriptPathname)[1]
+
+        self._namespace = dict(
+            headers=self._headers, fields=self._fields,
+            environ=self._environ, wrapper=self)
+        info = self._namespace.copy()
+
+        # Set up sys.stdout to be captured as a string. This allows scripts
+        # to set CGI headers at any time, which we then print prior to
+        # printing the main output. This also allows us to skip on writing
+        # any of the script's output if there was an error.
+        #
+        # This technique was taken from Andrew M. Kuchling's Feb 1998
+        # WebTechniques article.
+        #
+        self._realStdout = sys.stdout
+        sys.stdout = StringIO()
+
+        # Change directories if needed
+        if self.setting('ChangeDir'):
+            origDir = os.getcwd()
+            os.chdir(os.path.split(self._scriptPathname)[0])
+        else:
+            origDir = None
+
+        # A little more setup
+        self._errorOccurred = False
+        self._scriptStartTime = time()
+
+        # Run the target script
+        try:
+            if self._servingScript:
+                execfile(self._scriptPathname, self._namespace)
+                for name in self.setting('ClassNames'):
+                    if not name:
+                        name = os.path.splitext(self._scriptName)[0]
+                    if name in self._namespace:
+                        # our hook for class-oriented scripts
+                        print self._namespace[name](info).html()
+                        break
+            else:
+                self._headers = {'Location':
+                    os.path.split(self._environ['SCRIPT_NAME'])[0]
+                    + self._environ['PATH_INFO']}
+
+            # Note the end time of the script
+            self._scriptEndTime = time()
+            self._scriptDuration = self._scriptEndTime - self._scriptStartTime
+        except:
+            # Note the end time of the script
+            self._scriptEndTime = time()
+            self._scriptDuration = self._scriptEndTime - self._scriptStartTime
+
+            self._errorOccurred = True
+
+            # Not really an error, if it was sys.exit(0)
+            excInfo = sys.exc_info()
+            if excInfo[0] == SystemExit:
+                code = excInfo[1].code
+                if not code:
+                    self._errorOccurred = False
+
+            # Clean up
+            if self._errorOccurred:
+                if origDir:
+                    os.chdir(origDir)
+                    origDir = None
+
+                # Handle exception
+                self.handleException(sys.exc_info())
+
+        self.deliver()
+
+        # Restore original directory
+        if origDir:
+            os.chdir(origDir)
+
+        # Note the duration of server processing (as late as we possibly can)
+        self._serverDuration = time() - self._serverStartTime
+
+        # Log it
+        if self.setting('LogScripts'):
+            self.writeScriptLog()
+
+    def deliver(self):
+        """Deliver the HTML.
+
+        This is used for the output that came from the script being served,
+        or from our own error reporting.
+
+        """
+        # Compile the headers & cookies
+        headers = StringIO()
+        for header, value in self._headers.items():
+            headers.write("%s: %s\n" % (header, value))
+        if 'cookies' in self._namespace:
+            headers.write(str(self._namespace['cookies']))
+        headers.write('\n')
+
+        # Get the string buffer values
+        headersOut = headers.getvalue()
+        stdoutOut = sys.stdout.getvalue()
+
+        # Compute size
+        self._responseSize = len(headersOut) + len(stdoutOut)
+
+        # Send to the real stdout
+        self._realStdout.write(headersOut)
+        self._realStdout.write(stdoutOut)
+
+
+## Misc functions ##
+
+def docType():
+    """Return a standard HTML document type"""
+    return ('<!DOCTYPE HTML PUBLIC'
+        ' "-//W3C//DTD HTML 4.01 Transitional//EN"'
+        ' "http://www.w3.org/TR/html4/loose.dtd">')
+
+def htTitle(name):
+    """Return an HTML section title."""
+    return ('<h2 style="color:white;background-color:#993333;'