-
-
Save samuraisam/901117 to your computer and use it in GitHub Desktop.
#Copyright (c) 2010-2013 Samuel Sutch [[email protected]] | |
# | |
#Permission is hereby granted, free of charge, to any person obtaining a copy | |
#of this software and associated documentation files (the "Software"), to deal | |
#in the Software without restriction, including without limitation the rights | |
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
#copies of the Software, and to permit persons to whom the Software is | |
#furnished to do so, subject to the following conditions: | |
# | |
#The above copyright notice and this permission notice shall be included in | |
#all copies or substantial portions of the Software. | |
# | |
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
#THE SOFTWARE. | |
import datetime, time, functools, operator, types | |
default_fudge = datetime.timedelta(seconds=0, microseconds=0, days=0) | |
def deep_eq(_v1, _v2, datetime_fudge=default_fudge, _assert=False): | |
""" | |
Tests for deep equality between two python data structures recursing | |
into sub-structures if necessary. Works with all python types including | |
iterators and generators. This function was dreampt up to test API responses | |
but could be used for anything. Be careful. With deeply nested structures | |
you may blow the stack. | |
Options: | |
datetime_fudge => this is a datetime.timedelta object which, when | |
comparing dates, will accept values that differ | |
by the number of seconds specified | |
_assert => passing yes for this will raise an assertion error | |
when values do not match, instead of returning | |
false (very useful in combination with pdb) | |
Doctests included: | |
>>> x1, y1 = ({'a': 'b'}, {'a': 'b'}) | |
>>> deep_eq(x1, y1) | |
True | |
>>> x2, y2 = ({'a': 'b'}, {'b': 'a'}) | |
>>> deep_eq(x2, y2) | |
False | |
>>> x3, y3 = ({'a': {'b': 'c'}}, {'a': {'b': 'c'}}) | |
>>> deep_eq(x3, y3) | |
True | |
>>> x4, y4 = ({'c': 't', 'a': {'b': 'c'}}, {'a': {'b': 'n'}, 'c': 't'}) | |
>>> deep_eq(x4, y4) | |
False | |
>>> x5, y5 = ({'a': [1,2,3]}, {'a': [1,2,3]}) | |
>>> deep_eq(x5, y5) | |
True | |
>>> x6, y6 = ({'a': [1,'b',8]}, {'a': [2,'b',8]}) | |
>>> deep_eq(x6, y6) | |
False | |
>>> x7, y7 = ('a', 'a') | |
>>> deep_eq(x7, y7) | |
True | |
>>> x8, y8 = (['p','n',['asdf']], ['p','n',['asdf']]) | |
>>> deep_eq(x8, y8) | |
True | |
>>> x9, y9 = (['p','n',['asdf',['omg']]], ['p', 'n', ['asdf',['nowai']]]) | |
>>> deep_eq(x9, y9) | |
False | |
>>> x10, y10 = (1, 2) | |
>>> deep_eq(x10, y10) | |
False | |
>>> deep_eq((str(p) for p in xrange(10)), (str(p) for p in xrange(10))) | |
True | |
>>> str(deep_eq(range(4), range(4))) | |
'True' | |
>>> deep_eq(xrange(100), xrange(100)) | |
True | |
>>> deep_eq(xrange(2), xrange(5)) | |
False | |
>>> import datetime | |
>>> from datetime import datetime as dt | |
>>> d1, d2 = (dt.now(), dt.now() + datetime.timedelta(seconds=4)) | |
>>> deep_eq(d1, d2) | |
False | |
>>> deep_eq(d1, d2, datetime_fudge=datetime.timedelta(seconds=5)) | |
True | |
""" | |
_deep_eq = functools.partial(deep_eq, datetime_fudge=datetime_fudge, | |
_assert=_assert) | |
def _check_assert(R, a, b, reason=''): | |
if _assert and not R: | |
assert 0, "an assertion has failed in deep_eq (%s) %s != %s" % ( | |
reason, str(a), str(b)) | |
return R | |
def _deep_dict_eq(d1, d2): | |
k1, k2 = (sorted(d1.keys()), sorted(d2.keys())) | |
if k1 != k2: # keys should be exactly equal | |
return _check_assert(False, k1, k2, "keys") | |
return _check_assert(operator.eq(sum(_deep_eq(d1[k], d2[k]) | |
for k in k1), | |
len(k1)), d1, d2, "dictionaries") | |
def _deep_iter_eq(l1, l2): | |
if len(l1) != len(l2): | |
return _check_assert(False, l1, l2, "lengths") | |
return _check_assert(operator.eq(sum(_deep_eq(v1, v2) | |
for v1, v2 in zip(l1, l2)), | |
len(l1)), l1, l2, "iterables") | |
def op(a, b): | |
_op = operator.eq | |
if type(a) == datetime.datetime and type(b) == datetime.datetime: | |
s = datetime_fudge.seconds | |
t1, t2 = (time.mktime(a.timetuple()), time.mktime(b.timetuple())) | |
l = t1 - t2 | |
l = -l if l > 0 else l | |
return _check_assert((-s if s > 0 else s) <= l, a, b, "dates") | |
return _check_assert(_op(a, b), a, b, "values") | |
c1, c2 = (_v1, _v2) | |
# guard against strings because they are iterable and their | |
# elements yield iterables infinitely. | |
# I N C E P T I O N | |
for t in types.StringTypes: | |
if isinstance(_v1, t): | |
break | |
else: | |
if isinstance(_v1, types.DictType): | |
op = _deep_dict_eq | |
else: | |
try: | |
c1, c2 = (list(iter(_v1)), list(iter(_v2))) | |
except TypeError: | |
c1, c2 = _v1, _v2 | |
else: | |
op = _deep_iter_eq | |
return op(c1, c2) |
Yes, I'm agree with Yuando, package on pypi is great idea. If you wish I can help you with wrapping your code to package.
Has somebody forked this yet to enable NumPy support?
I tried using it with python3 and types
don't have StringTypes
anymore and while looking for a fix, I came across StackOverflow conversations and blogs mentioning that comparing type is not a good idea and instead we should try duck typing to figure out type of those variables, something like:
try:
_v1.title()
except TypeError:
try:
_v1.items()
op = _deep_dict_eq
except TypeError:
try:
c1, c2 = (list(iter(_v1)), list(iter(_v2)))
except TypeError:
c1, c2 = _v1, _v2
else:
op = _deep_iter_eq
If you want it to make it work on Python 3 with the minimum amount of changes, change line 129 such that
for t in [str]:
if isinstance(_v1, t):
break
else:
Is it something like deepdiff?
Thanks much!
If someone having trouble with these:
AttributeError: module 'types' has no attribute 'StringTypes'
AttributeError: module 'types' has no attribute 'DictType'
Just change these lines for the fix:
for t in [str]: **// this - replace types.StringTypes to [str]**
if isinstance(_v1, t):
break
else:
if isinstance(_v1, dict): **// this - replace types.DictType to dict**
Cheers!
Lol so many changes - anybody have the final version?
Only a couple changes needed:
s/types.StringTypes/[str]/
s/types.DictType/dict/
The original version overcomplicated the types and broke in python3 as a result.
Love this Gist.
Can you build it to a package and upload to PyPI? Then people who also love it do not need to copy it for their code.