From b8e619a2e3830d5e1b811536765b92d6d9ad675d Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Fri, 16 Sep 2022 23:30:44 +0200 Subject: [PATCH 1/9] relative delta keyword added to assertAlmostEqual and assertNotAlmostEqual --- Lib/unittest/case.py | 60 +++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index af8303333d4087..6f3d98af9bc6a7 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -882,15 +882,22 @@ def assertNotEqual(self, first, second, msg=None): raise self.failureException(msg) def assertAlmostEqual(self, first, second, places=None, msg=None, - delta=None): + delta=None, rel_delta=None): """Fail if the two objects are unequal as determined by their - difference rounded to the given number of decimal places - (default 7) and comparing to zero, or by comparing that the - difference between the two objects is more than the given - delta. + difference by one of the following methods: - Note that decimal places (from zero) are usually not the same - as significant digits (measured from the most significant digit). + 1) Given number of decimal places. This is the default method + (default value 7 digits). Note that decimal places (from + zero) are usually not the same as significant digits + (measured from the most significant digit). + + 2) Comparing that the absolute difference between the two + objects is more than the given delta. + + 3) Comparing that the relative difference between the two + objects is more than the given rel_delta. The relative + difference is applied with respect to the first object, + testing abs(first - second) < rel_delta*abs(first). If the two objects compare equal then they will automatically compare almost equal. @@ -898,8 +905,9 @@ def assertAlmostEqual(self, first, second, places=None, msg=None, if first == second: # shortcut return - if delta is not None and places is not None: - raise TypeError("specify delta or places not both") + if int(delta is not None) + int(places is not None) + \ + int(rel_delta is not None) > 1: + raise TypeError("specify maximally one: delta, places or rel_delta") diff = abs(first - second) if delta is not None: @@ -911,6 +919,15 @@ def assertAlmostEqual(self, first, second, places=None, msg=None, safe_repr(second), safe_repr(delta), safe_repr(diff)) + elif rel_delta is not None: + if diff < rel_delta*abs(first): + return + + standardMsg = '%s != %s within relatively %s (%s rel. difference)' % ( + safe_repr(first), + safe_repr(second), + safe_repr(rel_delta), + safe_repr(diff/abs(first))) else: if places is None: places = 7 @@ -927,19 +944,17 @@ def assertAlmostEqual(self, first, second, places=None, msg=None, raise self.failureException(msg) def assertNotAlmostEqual(self, first, second, places=None, msg=None, - delta=None): + delta=None, rel_delta=None): """Fail if the two objects are equal as determined by their - difference rounded to the given number of decimal places - (default 7) and comparing to zero, or by comparing that the - difference between the two objects is less than the given delta. - - Note that decimal places (from zero) are usually not the same - as significant digits (measured from the most significant digit). + difference by one of the methods analougsly described for + assertAlmostEqual(). Objects that are equal automatically fail. """ - if delta is not None and places is not None: - raise TypeError("specify delta or places not both") + if int(delta is not None) + int(places is not None) + \ + int(rel_delta is not None) > 1: + raise TypeError("specify maximally one: delta, places or rel_delta") + diff = abs(first - second) if delta is not None: if not (first == second) and diff > delta: @@ -949,6 +964,15 @@ def assertNotAlmostEqual(self, first, second, places=None, msg=None, safe_repr(second), safe_repr(delta), safe_repr(diff)) + elif rel_delta is not None: + if diff > rel_delta*abs(first): + return + + standardMsg = '%s == %s within relatively %s (%s rel. difference)' % ( + safe_repr(first), + safe_repr(second), + safe_repr(rel_delta), + safe_repr(diff/abs(first))) else: if places is None: places = 7 From 6eeb2b9de2434117c2408a5255c945b46d5ee165 Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Fri, 16 Sep 2022 23:31:12 +0200 Subject: [PATCH 2/9] unittest added to relative delta keyword in assertAlmostEqual and assertNotAlmostEqual --- Lib/test/test_unittest/test_assertions.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 6557104b81fc0f..e639f1a62cd584 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -57,6 +57,43 @@ def test_AmostEqualWithDelta(self): self.assertNotAlmostEqual(first, second, delta=datetime.timedelta(seconds=5)) + def test_AlmostEqualWithRelativeDelta(self): + self.assertAlmostEqual(1.1, 1.0, rel_delta=0.5) + self.assertAlmostEqual(1.0, 1.1, rel_delta=0.5) + with self.assertRaises(self.failureException): + self.assertAlmostEqual(1.1, 1.0, rel_delta=0.05) + with self.assertRaises(self.failureException): + self.assertAlmostEqual(1.0, 1.1, rel_delta=0.05) + + self.assertNotAlmostEqual(1.1, 1.0, rel_delta=0.05) + self.assertNotAlmostEqual(1.0, 1.1, rel_delta=0.05) + with self.assertRaises(self.failureException): + self.assertNotAlmostEqual(1.1, 1.0, rel_delta=0.5) + with self.assertRaises(self.failureException): + self.assertNotAlmostEqual(1.0, 1.1, rel_delta=0.5) + + self.assertAlmostEqual(1.0, 0.6, rel_delta=0.5) + self.assertNotAlmostEqual(0.6, 1.0, rel_delta=0.5) + with self.assertRaises(self.failureException): + self.assertAlmostEqual(0.6, 1.0, rel_delta=0.5) + with self.assertRaises(self.failureException): + self.assertNotAlmostEqual(1.0, 0.6, rel_delta=0.5) + + with self.assertRaises(TypeError): + self.assertAlmostEqual(1.0, 1.1, places=0, rel_delta=0.5) + with self.assertRaises(TypeError): + self.assertAlmostEqual(1.0, 1.1, delta=1, rel_delta=0.5) + with self.assertRaises(TypeError): + self.assertNotAlmostEqual(1.0, 1.1, places=0, rel_delta=0.05) + with self.assertRaises(TypeError): + self.assertNotAlmostEqual(1.0, 1.1, delta=0.05, rel_delta=0.05) + + first_dt = datetime.timedelta(seconds=20) + second_dt = datetime.timedelta(seconds=15) + + self.assertAlmostEqual(first_dt, second_dt, rel_delta=0.5) + self.assertNotAlmostEqual(first_dt, second_dt, rel_delta=0.1) + def test_assertRaises(self): def _raise(e): raise e From 0797a24f29350493af22763ba5b9a331ec8a0943 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 16 Sep 2022 21:51:41 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst diff --git a/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst b/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst new file mode 100644 index 00000000000000..7b543834b7cfbb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst @@ -0,0 +1 @@ +A new keyword "rel_delta" was added to the assertAlmostEqual and assertNotAlmostEqual methods of the unittest.TestCase class. It enables to compare two numbers and objects supporting addition, subtraction and scalar number multiplication to be compared with a maximum relative difference to each other. From 6151d7e2a335d78270c626bde7803a0c7a6e3783 Mon Sep 17 00:00:00 2001 From: wehlgrundspitze Date: Sat, 8 Oct 2022 20:57:41 +0200 Subject: [PATCH 4/9] Clarify news message and add internal rereferences. Co-authored-by: Nikita Sobolev --- .../next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst b/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst index 7b543834b7cfbb..2f9412929ac299 100644 --- a/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst +++ b/Misc/NEWS.d/next/Library/2022-09-16-21-51-41.gh-issue-71385.r5XUzt.rst @@ -1 +1 @@ -A new keyword "rel_delta" was added to the assertAlmostEqual and assertNotAlmostEqual methods of the unittest.TestCase class. It enables to compare two numbers and objects supporting addition, subtraction and scalar number multiplication to be compared with a maximum relative difference to each other. +A new parameter ``rel_delta`` was added to the :meth:`unittest.TestCase.assertAlmostEqual` and :meth:`unittest.TestCase.assertNotAlmostEqual`. It allows comparing two objects with a maximum relative difference to each other. From fc545ec184c685f4bd5eaffc83ee37b805f3918a Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Sat, 8 Oct 2022 23:39:00 +0200 Subject: [PATCH 5/9] rel_delta algorithm symmetrized in analogy to math.isclose --- Lib/unittest/case.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 6f3d98af9bc6a7..0c6f7f1fcc551d 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -895,9 +895,8 @@ def assertAlmostEqual(self, first, second, places=None, msg=None, objects is more than the given delta. 3) Comparing that the relative difference between the two - objects is more than the given rel_delta. The relative - difference is applied with respect to the first object, - testing abs(first - second) < rel_delta*abs(first). + objects is more than the given rel_delta, using the same + algorithm as :meth:`math.isclose`. If the two objects compare equal then they will automatically compare almost equal. @@ -920,7 +919,7 @@ def assertAlmostEqual(self, first, second, places=None, msg=None, safe_repr(delta), safe_repr(diff)) elif rel_delta is not None: - if diff < rel_delta*abs(first): + if (diff < rel_delta*abs(first)) or (diff < rel_delta*abs(second)): return standardMsg = '%s != %s within relatively %s (%s rel. difference)' % ( @@ -965,7 +964,7 @@ def assertNotAlmostEqual(self, first, second, places=None, msg=None, safe_repr(delta), safe_repr(diff)) elif rel_delta is not None: - if diff > rel_delta*abs(first): + if (diff > rel_delta*abs(first)) and (diff > rel_delta*abs(second)): return standardMsg = '%s == %s within relatively %s (%s rel. difference)' % ( From ca09f2ecae9ad56b36099117c8d43409c1f431ea Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Sat, 8 Oct 2022 23:40:18 +0200 Subject: [PATCH 6/9] unittests adapted to symmetrization of rel_delta argument of unittest.TestCase.assertAlmostEqual --- Lib/test/test_unittest/test_assertions.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index e639f1a62cd584..481f98ccf0b22a 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -58,26 +58,19 @@ def test_AmostEqualWithDelta(self): delta=datetime.timedelta(seconds=5)) def test_AlmostEqualWithRelativeDelta(self): - self.assertAlmostEqual(1.1, 1.0, rel_delta=0.5) - self.assertAlmostEqual(1.0, 1.1, rel_delta=0.5) + self.assertAlmostEqual(1.0, 0.6, rel_delta=0.5) + self.assertAlmostEqual(0.6, 1.0, rel_delta=0.5) with self.assertRaises(self.failureException): - self.assertAlmostEqual(1.1, 1.0, rel_delta=0.05) + self.assertAlmostEqual(1.0, 0.6, rel_delta=0.05) with self.assertRaises(self.failureException): - self.assertAlmostEqual(1.0, 1.1, rel_delta=0.05) + self.assertAlmostEqual(0.6, 1.0, rel_delta=0.05) - self.assertNotAlmostEqual(1.1, 1.0, rel_delta=0.05) - self.assertNotAlmostEqual(1.0, 1.1, rel_delta=0.05) - with self.assertRaises(self.failureException): - self.assertNotAlmostEqual(1.1, 1.0, rel_delta=0.5) - with self.assertRaises(self.failureException): - self.assertNotAlmostEqual(1.0, 1.1, rel_delta=0.5) - - self.assertAlmostEqual(1.0, 0.6, rel_delta=0.5) - self.assertNotAlmostEqual(0.6, 1.0, rel_delta=0.5) - with self.assertRaises(self.failureException): - self.assertAlmostEqual(0.6, 1.0, rel_delta=0.5) + self.assertNotAlmostEqual(1.0, 0.6, rel_delta=0.05) + self.assertNotAlmostEqual(0.6, 1.0, rel_delta=0.05) with self.assertRaises(self.failureException): self.assertNotAlmostEqual(1.0, 0.6, rel_delta=0.5) + with self.assertRaises(self.failureException): + self.assertNotAlmostEqual(0.6, 1.0, rel_delta=0.5) with self.assertRaises(TypeError): self.assertAlmostEqual(1.0, 1.1, places=0, rel_delta=0.5) From 1b01829e9ce826d4359f28fbd5614c6ee6b094ad Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Sun, 16 Oct 2022 22:30:54 +0200 Subject: [PATCH 7/9] unittest.TestCase.assertAlmostEqual and assertNotAlmostEqual modified to handle +-inf and NaN according to IEEE 754 standard --- Lib/unittest/case.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 0c6f7f1fcc551d..eecb3f281e9312 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -899,7 +899,9 @@ def assertAlmostEqual(self, first, second, places=None, msg=None, algorithm as :meth:`math.isclose`. If the two objects compare equal then they will automatically - compare almost equal. + compare almost equal. Comparison of floating point numbers + (espaicially inf, -inf, and NaN) is applied in the same way as + in :meth:`math.isclose` according to IEEE standard 754. """ if first == second: # shortcut @@ -955,8 +957,9 @@ def assertNotAlmostEqual(self, first, second, places=None, msg=None, raise TypeError("specify maximally one: delta, places or rel_delta") diff = abs(first - second) + if delta is not None: - if not (first == second) and diff > delta: + if not (first == second) and ((diff > delta) or diff != diff): return standardMsg = '%s == %s within %s delta (%s difference)' % ( safe_repr(first), @@ -964,7 +967,9 @@ def assertNotAlmostEqual(self, first, second, places=None, msg=None, safe_repr(delta), safe_repr(diff)) elif rel_delta is not None: - if (diff > rel_delta*abs(first)) and (diff > rel_delta*abs(second)): + if not (first == second) and \ + (((diff > rel_delta*abs(first)) and (diff > rel_delta*abs(second))) + or (diff != diff) or (diff/2 == diff)): return standardMsg = '%s == %s within relatively %s (%s rel. difference)' % ( From 640ba3b69aa860e66e37243752a646554907ef12 Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Sun, 16 Oct 2022 22:32:17 +0200 Subject: [PATCH 8/9] unittest added to check +-inf and nan handling of unittest.TestCase.assert(Not)AlmostEqual --- Lib/test/test_unittest/test_assertions.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 481f98ccf0b22a..fbf8e1863e2ede 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -6,6 +6,11 @@ from itertools import product +NAN = float('nan') +INF = float('inf') +NINF = float('-inf') + + class Test_Assertions(unittest.TestCase): def test_AlmostEqual(self): self.assertAlmostEqual(1.00000001, 1.0) @@ -87,6 +92,25 @@ def test_AlmostEqualWithRelativeDelta(self): self.assertAlmostEqual(first_dt, second_dt, rel_delta=0.5) self.assertNotAlmostEqual(first_dt, second_dt, rel_delta=0.1) + def test_AlmostEqual_inf_nan(self): + for criterion in [{"places": 2}, {"delta": 0.5}, {"rel_delta": 0.2}]: + for n1, n2 in [(INF, INF), (NINF, NINF)]: + self.assertAlmostEqual(n1, n2, **criterion) + with self.assertRaises(self.failureException, + msg="%s %s should be equal with %s" % (n1, n2, criterion)): + self.assertNotAlmostEqual(n1, n2, **criterion) + + for n1, n2 in [(INF, NINF), (NINF, INF), + (INF, 1), (1, INF), + (NINF, 1), (1, NINF), + (NAN, NAN), + (NAN, 1), (1, NAN), + (NAN, INF), (INF, NAN), + (NAN, NINF), (NINF, NAN)]: + self.assertNotAlmostEqual(n1, n2, **criterion) + with self.assertRaises(self.failureException): + self.assertAlmostEqual(n1, n2, **criterion) + def test_assertRaises(self): def _raise(e): raise e From 2d115d15f218e82595a16c58dd0203bc4dd3854f Mon Sep 17 00:00:00 2001 From: Manuel Zahn Date: Sun, 16 Oct 2022 22:36:04 +0200 Subject: [PATCH 9/9] test duplication and typo removed --- Lib/test/test_unittest/test_assertions.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index fbf8e1863e2ede..cb81f6b3178204 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -31,11 +31,7 @@ def test_AlmostEqual(self): self.assertRaises(self.failureException, self.assertNotAlmostEqual, 0, .1+.1j, places=0) - self.assertAlmostEqual(float('inf'), float('inf')) - self.assertRaises(self.failureException, self.assertNotAlmostEqual, - float('inf'), float('inf')) - - def test_AmostEqualWithDelta(self): + def test_AlmostEqualWithDelta(self): self.assertAlmostEqual(1.1, 1.0, delta=0.5) self.assertAlmostEqual(1.0, 1.1, delta=0.5) self.assertNotAlmostEqual(1.1, 1.0, delta=0.05)