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