From 8ee680596b41ffd3932586b0a1a5dd41b0d3341c Mon Sep 17 00:00:00 2001 From: David Brakman Date: Fri, 29 Mar 2024 19:00:12 -0400 Subject: [PATCH] diff: allow tolerance only between floats * Enable and require absolute equality between large integers not possible with math.isclose * Replace num_types usage with consistent behavior among subclasses of numbers.Number --- dictdiffer/__init__.py | 14 ++++--- dictdiffer/utils.py | 9 ++-- dictdiffer/version.py | 2 +- tests/test_dictdiffer.py | 88 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/dictdiffer/__init__.py b/dictdiffer/__init__.py index cbabf43..7c4761e 100644 --- a/dictdiffer/__init__.py +++ b/dictdiffer/__init__.py @@ -76,8 +76,9 @@ def diff(first, second, node=None, ignore=None, path_limit=None, expand=False, ... path_limit=PathLimit([('a', 'b')]))) [('add', '', [('a', {})]), ('add', 'a', [('b', 'c')])] - >>> from dictdiffer.utils import PathLimit - >>> list(diff({'a': {'b': 'c'}}, {'a': {'b': 'c'}}, path_limit=PathLimit([('a',)]))) + >>> from dictdiffer.utils import PathLimit + >>> list(diff({'a': {'b': 'c'}}, {'a': {'b': 'c'}}, + ... path_limit=PathLimit([('a',)]))) [] The patch can be expanded to small units e.g. when adding multiple values: @@ -102,11 +103,14 @@ def diff(first, second, node=None, ignore=None, path_limit=None, expand=False, :param path_limit: List of path limit tuples or dictdiffer.utils.Pathlimit object to limit the diff recursion depth. A diff is still performed beyond the path_limit, - but individual differences will be aggregated up to the path_limit. + but individual differences will be aggregated up to the + path_limit. :param expand: Expand the patches. - :param tolerance: Threshold to consider when comparing two float numbers. + :param tolerance: Relative threshold for comparing two floating-point + numbers as equal, expressed as a proportion of the + maximum of the two :param absolute_tolerance: Absolute threshold to consider when comparing - two float numbers. + two floating-point numbers. :param dot_notation: Boolean to toggle dot notation on and off. .. versionchanged:: 0.3 diff --git a/dictdiffer/utils.py b/dictdiffer/utils.py index 5d59c92..79e14c8 100644 --- a/dictdiffer/utils.py +++ b/dictdiffer/utils.py @@ -13,7 +13,6 @@ import sys from itertools import zip_longest -num_types = int, float EPSILON = sys.float_info.epsilon @@ -256,8 +255,8 @@ def dot_lookup(source, lookup, parent=False): def are_different(first, second, tolerance, absolute_tolerance=None): """Check if 2 values are different. - In case of numerical values, the tolerance is used to check if the values - are different. + In case of 2 floating-point values, the tolerance is used to check if the + values are different. In all other cases, the difference is straight forward. """ if first == second: @@ -269,8 +268,8 @@ def are_different(first, second, tolerance, absolute_tolerance=None): if first_is_nan or second_is_nan: # two 'NaN' values are not different (see issue #114) return not (first_is_nan and second_is_nan) - elif isinstance(first, num_types) and isinstance(second, num_types): - # two numerical values are compared with tolerance + elif isinstance(first, float) and isinstance(second, float): + # two floating-precision values are compared with tolerance return not math.isclose( first, second, diff --git a/dictdiffer/version.py b/dictdiffer/version.py index 3149802..9bfbbd6 100644 --- a/dictdiffer/version.py +++ b/dictdiffer/version.py @@ -3,4 +3,4 @@ # setup.py and docs/conf.py """Version information for dictdiffer package.""" -__version__ = '0.9.0' +__version__ = '0.1.dev160' diff --git a/tests/test_dictdiffer.py b/tests/test_dictdiffer.py index 0af3e3d..7e45999 100644 --- a/tests/test_dictdiffer.py +++ b/tests/test_dictdiffer.py @@ -5,6 +5,7 @@ # Copyright (C) 2013 Fatih Erikli. # Copyright (C) 2013, 2014, 2015, 2016 CERN. # Copyright (C) 2017-2019 ETH Zurich, Swiss Data Science Center, Jiri Kuncar. +# Copyright (C) 2024 Kohl's. # # Dictdiffer is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more @@ -13,6 +14,8 @@ import unittest from collections import OrderedDict from collections.abc import MutableMapping, MutableSequence +from decimal import Decimal +from fractions import Fraction import pytest @@ -152,6 +155,91 @@ def test_tolerance(self): )) assert [] == diffed + first = {'a': 22570409781991170591038650551} + second = {'a': 22570409781991170591038650552} + diffed = list(diff(first, second, tolerance=None)) + assert [ + ( + 'change', + 'a', + (22570409781991170591038650551, 22570409781991170591038650552) + ) + ] == diffed + + diffed = list(diff(first, second, tolerance=0)) + assert [ + ( + 'change', + 'a', + (22570409781991170591038650551, 22570409781991170591038650552) + ) + ] == diffed + + diffed = list(diff(first, second, tolerance=1e-9)) + assert [ + ( + 'change', + 'a', + (22570409781991170591038650551, 22570409781991170591038650552) + ) + ] == diffed + + first = {'a': Decimal("1.0e-15")} + second = {'a': Decimal("2.5e-15")} + diffed = list(diff(first, second, tolerance=1e-20)) + assert [ + ('change', 'a', (Decimal("1.0e-15"), Decimal("2.5e-15"))) + ] == diffed + + first = {'a': complex(1.0e-15, 1.0e-15)} + second = {'a': complex(2.5e-15, 2.5e-15)} + diffed = list( + diff(first, second, tolerance=1e-3, absolute_tolerance=1e-3) + ) + assert [ + ( + 'change', + 'a', + (complex(1.0e-15, 1.0e-15), complex(2.5e-15, 2.5e-15)) + ) + ] == diffed + + first = {'a': Fraction(10, 7)} + second = {'a': Fraction(11, 7)} + diffed = list( + diff(first, second, tolerance=1e-3, absolute_tolerance=1e-3) + ) + assert [ + ('change', 'a', (Fraction(10, 7), Fraction(11, 7))) + ] == diffed + + first = {'a': 2 - 1e-15} + second = {'a': complex(2, 0)} + diffed = list( + diff(first, second, tolerance=1e-3, absolute_tolerance=1e-3) + ) + assert [ + ('change', 'a', (2 - 1e-15, complex(2, 0))) + ] == diffed + + first = {'a': 2 - 1e-15} + second = {'a': 2} + diffed = list( + diff(first, second, tolerance=1e-3, absolute_tolerance=1e-3) + ) + assert [ + ('change', 'a', (2 - 1e-15, 2)) + ] == diffed + + first = {'a': 2 - 1e-15} + second = {'a': Decimal("2.0")} + diffed = list( + diff(first, second, tolerance=1e-3, absolute_tolerance=1e-3) + ) + assert [ + ('change', 'a', (2 - 1e-15, Decimal("2.0"))) + ] == diffed + def test_path_limit_as_list(self): first = {} second = {'author': {'last_name': 'Doe', 'first_name': 'John'}}