#!/usr/bin/python
################################################################################
# Name: pyTools.py                                                             #
# Version: 1.0.0                                                               #
# Date: 2012-08-22                                                             #
# Author: VA ESE Enterprise Testing Services                                   #
#                                                                              #
# Commonly Used Python Objects                                                 #
#                                                                              #
# Classes                                                                      #
#     dictDiffer - extends the Python object class                             #
#        dictDiffer requires two Python Dictionary objects when it is created  #
#        (a "current" version and a "past" version. Object initialization      #
#        copies the two dictionaries to class properties, creates set objects  #
#        from the keys of the two dictionaries and creates a set intersection  #
#        object based the two sets of keys. Methods are provided to return the #
#        set of dictionary keys only in the current dictionary, the set of     #
#        dictionary keys only in the past dictionary, the set of dictionary    #
#        keys in both dictionaries with values that have changed, and the set  #
#        of dictionary keys in both dictionaries with values that have not     #
#        changed.                                                              #
#                                                                              #
# Functions                                                                    #
#     extractDict - extracts a subset of a dictionary object                   #
#         extractDict requires a Python Dictionary and a Python List of Keys   #
#         to extract from that Dictionary. It returns a Dictionary with only   #
#         the keys that were both in the Dictionary and the List passed in.    #
#                                                                              #
################################################################################

#
# Declare class dictDiffer as a subclass of object
#
class dictDiffer(object):
    """
    Calculate the difference between two dictionaries as:
    (1) keys added
    (2) keys removed
    (3) keys same in both but changed values
    (4) keys same in both and unchanged values
    """
    #
    # Class initialization method
    #
    def __init__(self, newDict, oldDict):                                       # Initialize the class with two dictionary objects
        self.newDict, self.oldDict = newDict, oldDict                           # Copy the dictionaries to internal class objects
                                                                                # Create set objects from the two dictionaries keys
        self.setNew, self.setOld = set(newDict.keys()), set(oldDict.keys())
        self.intersect = self.setNew.intersection(self.setOld)                  # Create a set object of the intersection of the two sets of keys

    #
    # Method to return a set of dictionary items that have been added to the 
    # "current" dictionary
    #
    def added(self):
        return self.setNew - self.intersect                                     # Return the set of keys unique to the "current" dictionary

    #
    # Method to return a set of dictionary items that have been removed from
    # the "current" dictionary
    #
    def removed(self):
        return self.setOld - self.intersect                                     # Return the set of keys unique to the "past"  dictionary

    #
    # Method to return the set of dictionary items that are in both the "current"
    # and "past" dictionary, but have been changed
    #
    def changed(self):                                                          # Return the set of common keys with values that do not match
        return set(o for o in self.intersect if self.oldDict[o] != self.newDict[o])

    #
    # Class initialization method
    #
    def unchanged(self):                                                        # Return the set of common keys with values that match
        return set(o for o in self.intersect if self.oldDict[o] == self.newDict[o])


#
# Class listDiffer is a subclass of object, that compares elements of two lists
#
class listDiffer(object):
    """
        Class to find differences betwen two list objects
        \trequires two list objects when an instance of the class is created
        This class provides methods to return:
        \titems that have been added to the new list
        \titems that have been removed from the new list
        \titems that are in both lists
    """
    def __init__(self, newList, oldList):
        """
            When an object of this class is created,
            check to ensure both input objects are lists
            and save the two lists for comparison
        """
        if not isinstance(newList, list):                                       # If the new object is not a list
                                                                                # Raise a typeError exception
            raise TypeError('Type of newList argument must be list, not %s' % type(newList))

        if not isinstance(oldList, list):                                       # If the old object is not a list
                                                                                # Raise a TypeError exception
            raise TypeError('Type of oldList argument must be list, not %s' % type(oldList))
        self.newList = newList                                                  # Save the new object
        self.oldList = oldList                                                  # Save the old object

    def added(self):
        """
            Method to return items in the new list, but not in the old list
        """
        return [item for item in self.newList if not item in self.oldList]      # Return all items in new list, but not old list

    def removed(self):
        """
            Method to return items in the old list, but not in the new list
        """
        return [item for item in self.oldList if not item in self.newList]      # Return all items in old list, but not new list

    def unchanged(self):
        """
            Method to return items in both the new list and the old list
        """
        return [item for item in self.newList if item in self.oldList]          # Return all items in both lists


#
# Function to compare two strings
#
def strDiffer(newString, oldString, lines = 2):
    """
        Function to return differences between lines in two strings
        returns diferences as a list of lines
        \toptional parameter 'lines' may be passed to specify the
        \t         number of lines to show before and after
        \t         each difference
    """
    import difflib                                                              # Import Python difflib
    return list(difflib.unified_diff(newStr, oldStr, n=lines))                  # Return a list of diff lines



#
# Function to extract a subset of keys from a dictionary
#
def extractDict(d, keys):
    """
        extractDict requires two arguments, a Dictionary, and a List of Keys.
        \treturns a dictionary with the keys that are in
        \tboth the dictionary and the list.
    """
    return dict((k, d[k]) for k in keys if k in d)                              # Return the keys, values from d that have keys in the list


#
# Function to Print Object Details
#
def printDetails(detail, indent = 1):
    """
        Function to Print Object Detail Information
        Calls itself if detail contains dictionaries or lists,
        Increasing indentation each time it is called
        \trequires an object(dictionary, list, tuple, string, boolean, integer or float)
        \taccepts an optional multiplier for indentation
    """
    if isinstance(detail, dict):                                                # If this detail is a dictionary
        for key, value in sorted(detail.iteritems()):                           # For each key, value pair, sorted by key
                                                                                # If the value is a dictionary or a list, with items in it
            if (isinstance(value, dict) or isinstance(value, list)) \
                and len(value) > 0:
                print '%s%s :' % ('\t' * indent, key)                           # Print, indented from any previous call, the key
                printDetails(value, indent + 1)                                 # And call ourselves, incrementing the indent
            elif isinstance(value, str) and '\n' in value:                      # If value is a string and has NL characters
                print '%s%s:' % ('\t' * indent, key)                            # Print, indented from any previous call, the key
                printDetails(value.split('\n'), indent + 1)                     # And call ourselves with the string split into a list of lines, incrementing the indent
            else:                                                               # If the value is any other type
                print '%s%s = %s' % ('\t' * indent, key, value)                 # Print, indented from any previous call, the key and value
        print '%s%s' % ('\t' * indent, '-' * 30)                        # Print a separator line
    elif isinstance(detail, list):                                              # If this detail is a list
        for item in detail:                                                     # For each item in the list, sorted by item
            if isinstance(item, dict) or isinstance(item, list):                # If the item is a dictionary or list, incrementing the indent
                printDetails(item, indent + 1)                                  # Call ourselves, incrementing the indent counter
            elif isinstance(item, str) and '\n' in item:                        # If item is a string and has NL characters
                printDetails(item.split('\n'), indent + 1)                      # Call ourselves with the string split into a list of lines, incrementing the indent
            else:                                                               # If the Item is any other type
                print '%s%s' % ('\t' * indent, item)                            # Print, indented from any previous call, the item
        print '%s%s' % ('\t' * indent, '-' * 30)                                # Print a separator line
    elif isinstance(detail, str) and '\n' in detail:                            # If detail is a string and has NL characters
        printDetails(detail.split('\n'), indent + 1)                            # Call ourselves with the string split into a list of lines, incrementing the indent
    else:                                                                       # If the detail is any other type
        print '%s%s' % ('\t' * indent, detail)                                  # Print, indented from any previous call, the detail
    

#
# Function to compare two sets of details
#
def diffDetails(newDetail, oldDetail, indent = 1):
    """
    Function to compare details of two objects
    the two objects must be of the same type
    supported objects include dictionary, list, str, boolean, int and float
    \taccepts an optional integer specifying the level of indentation
    """
    if type(newDetail) != type(oldDetail):                                      # If the two objects passed in are not the same type
        raise TypeError('Both arguments must be the same type.\n' +             # Raise a TypeError exception
                        '\toldDetail type: %s\n' +
                        '\tnewDetail type: %s' % (type(oldDetail),
                                                  type(newDetail)))

    if isinstance(newDetail, dict):                                             # If the objects are dictionaries
        dictCompare = dictDiffer(newDetail, oldDetail)                          # Create a dictDiffer object from them

        for addedKey in dictCompare.added():                                    # For each key in the new dictionary, but not the old dictionary
            print '%s%s :' % ('\t' * indent, addedKey)                          # Print the key, indented
            printDetails(newDetail[addedKey], indent + 1)                       # And call printDetails with an additional level of indentation

        for removedKey in dictCompare.removed():                                # For each key in the old dictionary, but not the new dictionary
            print '%s%s :' % ('\t' * indent, addedKey)                          # Print the key, indented
            printDetails(oldDetail[addedKey], indent + 1)                       # And call printDetails with an additional level of indentation

        for changedKey in dictCompare.changed():                                # For each key that is in both dictionaries, but has changed
            print '%s%s :' % ('\t' * indent, changedKey)                        # Print the key, indented
            diffDetails(newDetail[changedKey],                                  # And call ourselves, with an additional level of indentation
                        oldDetail[changedKey],
                        indent + 1)

    elif isinstance(newDetail, list):                                           # If the objects are lists
        listCompare = listDiffer(newDetail, oldDetail)                          # Create a listDiffer object from them

        addedItems = listCompare.added()                                        # Get a list of items that are in the new list, but not the old list
        if len(addedItems) > 0:                                                 # If the list has more than 0 items
            print '%sadded:' % ('\t' * indent)                                  # Print an indented 'added' header
            printDetails(addedItems, indent + 1)                                # And call printDetails with an additional level of indentation

        removedItems = listCompare.removed()                                    # Get a list of items that are in the old list, but not the new list
        if len(removedItems) > 0:                                               # If the list has more than 0 items
            print '%sremoved:' % ('\t' * indent)                                # Print an indented 'removed' header
            printDetails(removedItems, indent + 1)                              # And call printDetails with an additional level of indentation

    elif isinstance(newDetail, str):                                            # If the objects are strings
        strCompare = strDiff(newDetail, oldDetail)                              # Call the strCompare function, to get a list of diff lines
        if len(strCompare) > 0:                                                 # If the list of diff lines is greater than 0
            for line in strCompare:                                             # For each line
                print '%s%s' % ('\t' * indent, line)                            # Print the line, using the current indentation level

    else:                                                                       # If the object is any other type
        if newDetail != oldDetail:                                              # If the new and old objects are different
            print '%sold:\t%s\n%snew:\t%s' % ('\t' * indent,                    # Print the old and new objects
                                              oldDetail,
                                              '\t' * indent,
                                              newDetail)

