    import os, sys, types
    from UserDict import UserDict

    class WillNotRunError(Exception): pass

    class PropertiesObject(UserDict):
        """A Properties Object.

        A PropertiesObject represents, in a dictionary-like fashion, the values
        found in a Properties.py file. That file is always included with a Webware
        component to advertise its name, version, status, etc. Note that a Webware
        component is a Python package that follows additional conventions.
        Also, the top level Webware directory contains a Properties.py.

        Component properties are often used for:
            * generation of documentation
            * runtime examination of components, especially prior to loading

        PropertiesObject provides additional keys:
            * filename - the filename from which the properties were read
            * versionString - a nicely printable string of the version
            * requiredPyVersionString - like versionString,
              but for requiredPyVersion instead
            * willRun - 1 if the component will run.
              So far that means having the right Python version.
            * willNotRunReason - defined only if willRun is 0,
              contains a readable error message

        Using a PropertiesObject is better than investigating the Properties.py
        file directly, because the rules for determining derived keys and any
        future convenience methods will all be provided here.

        Usage example:
            from MiscUtils.PropertiesObject import PropertiesObject
            props = PropertiesObject(filename)
            for item in props.items():
                print '%s: %s' % item

        Note: We don't normally suffix a class name with "Object" as we have
        with this class, however, the name Properties.py is already used in
        our containing package and all other packages.

        """

        ## Init and reading ##

        def __init__(self, filename=None):
            UserDict.__init__(self)
            if filename:
                self.readFileNamed(filename)

        def loadValues(self, dict):
            self.update(dict)
            self.cleanPrivateItems()

        def readFileNamed(self, filename):
            self['filename'] = filename
            results = {}
            exec open(filename) in results
            # @@ 2001-01-20 ce: try "...in self"
            self.update(results)
            self.cleanPrivateItems()
            self.createDerivedItems()

        ## Self utility ##

        def cleanPrivateItems(self):
            """Remove items whose keys start with a double underscore, such as __builtins__."""
            for key in self.keys():
                if key[:2] == '__':
                    del self[key]

        def createDerivedItems(self):
            self.createVersionString()
            self.createRequiredPyVersionString()
            self.createWillRun()

        def _versionString(self, version):
            """Return the version number as a string.

            For a sequence containing version information such as (2, 0, 0, 'pre'),
            this returns a printable string such as '2.0pre'.
            The micro version number is only excluded from the string if it is zero.

            """
            ver = map(str, version)
            numbers, rest = ver[:ver[2] == '0' and 2 or 3], ver[3:]
            return '.'.join(numbers) + '-'.join(rest)

        def createVersionString(self):
            self['versionString'] = self._versionString(self['version'])

        def createRequiredPyVersionString(self):
            self['requiredPyVersionString'] = self._versionString(self['requiredPyVersion'])

        def createWillRun(self):
            self['willRun'] = 0
            try:
                # Invoke each of the checkFoo() methods
                for key in self.willRunKeys():
                    methodName = 'check' + key[0].upper() + key[1:]
                    method = getattr(self, methodName)
                    method()
            except WillNotRunError, msg:
                self['willNotRunReason'] = msg
                return
            self['willRun'] = 1 # we passed all the tests

        def willRunKeys(self):
            """Return keys to be examined before running the component.

            This returns a list of all keys whose values should be examined in
            order to determine if the component will run. Used by createWillRun().

            """
            return ['requiredPyVersion', 'requiredOpSys', 'deniedOpSys', 'willRunFunc']

        def checkRequiredPyVersion(self):
            pyVer = getattr(sys, 'version_info', None)
            if not pyVer:
                # Prior 2.0 there was no version_info
                # So we parse it out of .version which is a string
                pyVer = sys.version.split(' ', 1)[0].split('.')
                pyVer = map(int, pyVer)
            if tuple(pyVer) < tuple(self['requiredPyVersion']):
                raise WillNotRunError, 'Required Python ver is %s, but actual ver is %s.' % (
                    '.'.join(map(str, self['requiredPyVersion'])), '.'.join(map(str, pyVer)))

        def checkRequiredOpSys(self):
            requiredOpSys = self.get('requiredOpSys', None)
            if requiredOpSys:
                # We accept a string or list of strings
                if type(requiredOpSys) is types.StringType:
                    requiredOpSys = [requiredOpSys]
                if not os.name in requiredOpSys:
                    raise WillNotRunError, 'Required op sys is %s, but actual op sys is %s.' % (
                        '/'.join(requiredOpSys), os.name)

        def checkDeniedOpSys(self):
            deniedOpSys = self.get('deniedOpSys', None)
            if deniedOpSys:
                # We accept a string or list of strings
                if type(deniedOpSys) is types.StringType:
                    deniedOpSys = [deniedOpSys]
                if os.name in deniedOpSys:
                    raise WillNotRunError, 'Will not run on op sys %s and actual op sys is %s.' % (
                        '/'.join(deniedOpSys), os.name)

        def checkRequiredSoftware(self):
            """Not implemented. No op right now."""
            # Check required software
            # @@ 2001-01-24 ce: TBD
            # Issues include:
            #     - order of dependencies
            #     - circular dependencies
            #     - examining Properties and willRun of dependencies
            reqSoft = self.get('requiredSoftware', None)
            if reqSoft:
                for soft in reqSoft:
                    # type, name, version
                    pass

        def checkWillRunFunc(self):
            willRunFunc = self.get('willRunFunc', None)
            if willRunFunc:
                whyNotMsg = willRunFunc()
                if whyNotMsg:
                    raise WillNotRunError, whyNotMsg