from django.utils.datetime_safe import datetime


class DeepMatcher(object):
    ANY_INT = 'AnyInt'
    ANY_STRING = 'AnyString'
    DATETIME_AROUND_NOW = 'DatetimeAroundNow'

    @classmethod
    def _fuzzy_match_dicts(cls, actual, expected, reason):
        for key, expected_value in expected.items():
            actual_value = actual.get(key)
            reason += key+"."
            cls.fuzzy_match(actual_value, expected_value, reason)

    @classmethod
    def _fuzzy_match_lists(cls, actual, expected, reason):
        for expected_item, actual_item in zip(expected, actual):
            reason += "[]"
            cls.fuzzy_match(actual_item, expected_item, reason)

    @classmethod
    def _fuzzy_match_leaves(cls, actual, expected):
        if expected == cls.ANY_INT:
            if not isinstance(actual, int):
                raise ValueError('expected any integer, got {}'.format(actual))
        elif expected == cls.ANY_STRING:
            if not isinstance(actual, str):
                raise ValueError('expected any string, got {}'.format(actual))
        elif expected == cls.DATETIME_AROUND_NOW:
            actual_time = datetime.strptime(actual.split(".")[0], "%Y-%m-%dT%H:%M:%S")
            now = datetime.utcnow()
            difference = now - actual_time
            if abs(difference.total_seconds()) > 60:
                raise ValueError('expected an UTC time string within 60 seconds of now, got {}'.format(actual))
        else:
            if actual != expected:
                raise ValueError('expected {}, got {}'.format(expected, actual))

    @classmethod
    def fuzzy_match(cls, actual, expected, reason=""):
        """
        :raises ValueError: if the arguments do not match
        """
        if isinstance(expected, dict) and isinstance(actual, dict):
            cls._fuzzy_match_dicts(actual, expected, reason)
        elif isinstance(expected, list) and isinstance(actual, list):
            cls._fuzzy_match_lists(actual, expected, reason)
        else:
            try:
                cls._fuzzy_match_leaves(actual, expected)
            except ValueError as e:
                raise ValueError("Reason: "+reason+", "+e.args[0])

@@ -169,16 +169,281 @@
 ndNow'%0A%0A
+    @classmethod%0A    def _raise_not_matching(cls, actual, expected, reason):%0A        raise ValueError('got %22%7Bactual%7D%22, expected %22%7Bexpected%7D%22, trace %22%7Breason%7D%22'.format(%0A            actual=actual,%0A            expected=expected,%0A            reason=reason,%0A        ))%0A%0A
     @cla
@@ -610,38 +610,8 @@
 ey)%0A
-            reason += key+%22.%22%0A
     
@@ -664,22 +664,63 @@
 _value, 
+'%7B
 reason
+%7D.%7Bkey%7D'.format(reason=reason, key=key)
 )%0A%0A    @
@@ -802,16 +802,20 @@
     for 
+i, (
 expected
@@ -832,20 +832,31 @@
 ual_item
+)
  in 
+enumerate(
 zip(expe
@@ -872,37 +872,11 @@
 ual)
+)
 :%0A
-            reason += %22%5B%5D%22%0A
     
@@ -927,22 +927,58 @@
 d_item, 
+'%7B
 reason
+%7D%5B%7Bi%7D%5D'.format(reason=reason, i=i)
 )%0A%0A    @
@@ -1038,16 +1038,24 @@
 expected
+, reason
 ):%0A     
@@ -1149,35 +1149,41 @@
     
-raise ValueError('expected 
+cls._raise_not_matching(actual, '
 any 
@@ -1193,32 +1193,17 @@
 eger
+'
 , 
-got %7B%7D'.format(actual)
+reason
 )%0A  
@@ -1305,35 +1305,41 @@
     
-raise ValueError('expected 
+cls._raise_not_matching(actual, '
 any 
@@ -1348,32 +1348,17 @@
 ring
+'
 , 
-got %7B%7D'.format(actual)
+reason
 )%0A  
@@ -1644,41 +1644,44 @@
     
-raise ValueError('expected an UTC
+cls._raise_not_matching(actual, 'utc
  tim
@@ -1717,145 +1717,96 @@
  now
+'
 , 
-got %7B%7D'.format(actual))%0A        else:%0A            if actual != expected:%0A                raise ValueError('expected %7B%7D, got %7B%7D'.format(
+reason)%0A        elif actual != expected:%0A            cls._raise_not_matching(actual, 
 expe
@@ -1811,23 +1811,22 @@
 pected, 
-actual)
+reason
 )%0A%0A    @
@@ -2252,29 +2252,8 @@
 se:%0A
-            try:%0A    
     
@@ -2304,109 +2304,14 @@
 cted
-)%0A            except ValueError as e:%0A                raise ValueError(%22Reason: %22+reason+%22, %22+e.args%5B0%5D
+, reason
 )%0A

Refactor fuzzy matching reason building