Issue #8188: Introduce a new scheme for computing hashes of numbers
(instances of int, float, complex, decimal.Decimal and
fractions.Fraction) that makes it easy to maintain the invariant that
hash(x) == hash(y) whenever x and y have equal value.
diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py
index b52b1db..cabeb16 100644
--- a/Lib/test/test_float.py
+++ b/Lib/test/test_float.py
@@ -914,15 +914,6 @@
         self.assertFalse(NAN.is_inf())
         self.assertFalse((0.).is_inf())
 
-    def test_hash_inf(self):
-        # the actual values here should be regarded as an
-        # implementation detail, but they need to be
-        # identical to those used in the Decimal module.
-        self.assertEqual(hash(float('inf')), 314159)
-        self.assertEqual(hash(float('-inf')), -271828)
-        self.assertEqual(hash(float('nan')), 0)
-
-
 fromHex = float.fromhex
 toHex = float.hex
 class HexFloatTestCase(unittest.TestCase):
diff --git a/Lib/test/test_numeric_tower.py b/Lib/test/test_numeric_tower.py
new file mode 100644
index 0000000..eafdb0f
--- /dev/null
+++ b/Lib/test/test_numeric_tower.py
@@ -0,0 +1,151 @@
+# test interactions betwen int, float, Decimal and Fraction
+
+import unittest
+import random
+import math
+import sys
+import operator
+from test.support import run_unittest
+
+from decimal import Decimal as D
+from fractions import Fraction as F
+
+# Constants related to the hash implementation;  hash(x) is based
+# on the reduction of x modulo the prime _PyHASH_MODULUS.
+_PyHASH_MODULUS = sys.hash_info.modulus
+_PyHASH_INF = sys.hash_info.inf
+
+class HashTest(unittest.TestCase):
+    def check_equal_hash(self, x, y):
+        # check both that x and y are equal and that their hashes are equal
+        self.assertEqual(hash(x), hash(y),
+                         "got different hashes for {!r} and {!r}".format(x, y))
+        self.assertEqual(x, y)
+
+    def test_bools(self):
+        self.check_equal_hash(False, 0)
+        self.check_equal_hash(True, 1)
+
+    def test_integers(self):
+        # check that equal values hash equal
+
+        # exact integers
+        for i in range(-1000, 1000):
+            self.check_equal_hash(i, float(i))
+            self.check_equal_hash(i, D(i))
+            self.check_equal_hash(i, F(i))
+
+        # the current hash is based on reduction modulo 2**n-1 for some
+        # n, so pay special attention to numbers of the form 2**n and 2**n-1.
+        for i in range(100):
+            n = 2**i - 1
+            if n == int(float(n)):
+                self.check_equal_hash(n, float(n))
+                self.check_equal_hash(-n, -float(n))
+            self.check_equal_hash(n, D(n))
+            self.check_equal_hash(n, F(n))
+            self.check_equal_hash(-n, D(-n))
+            self.check_equal_hash(-n, F(-n))
+
+            n = 2**i
+            self.check_equal_hash(n, float(n))
+            self.check_equal_hash(-n, -float(n))
+            self.check_equal_hash(n, D(n))
+            self.check_equal_hash(n, F(n))
+            self.check_equal_hash(-n, D(-n))
+            self.check_equal_hash(-n, F(-n))
+
+        # random values of various sizes
+        for _ in range(1000):
+            e = random.randrange(300)
+            n = random.randrange(-10**e, 10**e)
+            self.check_equal_hash(n, D(n))
+            self.check_equal_hash(n, F(n))
+            if n == int(float(n)):
+                self.check_equal_hash(n, float(n))
+
+    def test_binary_floats(self):
+        # check that floats hash equal to corresponding Fractions and Decimals
+
+        # floats that are distinct but numerically equal should hash the same
+        self.check_equal_hash(0.0, -0.0)
+
+        # zeros
+        self.check_equal_hash(0.0, D(0))
+        self.check_equal_hash(-0.0, D(0))
+        self.check_equal_hash(-0.0, D('-0.0'))
+        self.check_equal_hash(0.0, F(0))
+
+        # infinities and nans
+        self.check_equal_hash(float('inf'), D('inf'))
+        self.check_equal_hash(float('-inf'), D('-inf'))
+
+        for _ in range(1000):
+            x = random.random() * math.exp(random.random()*200.0 - 100.0)
+            self.check_equal_hash(x, D.from_float(x))
+            self.check_equal_hash(x, F.from_float(x))
+
+    def test_complex(self):
+        # complex numbers with zero imaginary part should hash equal to
+        # the corresponding float
+
+        test_values = [0.0, -0.0, 1.0, -1.0, 0.40625, -5136.5,
+                       float('inf'), float('-inf')]
+
+        for zero in -0.0, 0.0:
+            for value in test_values:
+                self.check_equal_hash(value, complex(value, zero))
+
+    def test_decimals(self):
+        # check that Decimal instances that have different representations
+        # but equal values give the same hash
+        zeros = ['0', '-0', '0.0', '-0.0e10', '000e-10']
+        for zero in zeros:
+            self.check_equal_hash(D(zero), D(0))
+
+        self.check_equal_hash(D('1.00'), D(1))
+        self.check_equal_hash(D('1.00000'), D(1))
+        self.check_equal_hash(D('-1.00'), D(-1))
+        self.check_equal_hash(D('-1.00000'), D(-1))
+        self.check_equal_hash(D('123e2'), D(12300))
+        self.check_equal_hash(D('1230e1'), D(12300))
+        self.check_equal_hash(D('12300'), D(12300))
+        self.check_equal_hash(D('12300.0'), D(12300))
+        self.check_equal_hash(D('12300.00'), D(12300))
+        self.check_equal_hash(D('12300.000'), D(12300))
+
+    def test_fractions(self):
+        # check special case for fractions where either the numerator
+        # or the denominator is a multiple of _PyHASH_MODULUS
+        self.assertEqual(hash(F(1, _PyHASH_MODULUS)), _PyHASH_INF)
+        self.assertEqual(hash(F(-1, 3*_PyHASH_MODULUS)), -_PyHASH_INF)
+        self.assertEqual(hash(F(7*_PyHASH_MODULUS, 1)), 0)
+        self.assertEqual(hash(F(-_PyHASH_MODULUS, 1)), 0)
+
+    def test_hash_normalization(self):
+        # Test for a bug encountered while changing long_hash.
+        #
+        # Given objects x and y, it should be possible for y's
+        # __hash__ method to return hash(x) in order to ensure that
+        # hash(x) == hash(y).  But hash(x) is not exactly equal to the
+        # result of x.__hash__(): there's some internal normalization
+        # to make sure that the result fits in a C long, and is not
+        # equal to the invalid hash value -1.  This internal
+        # normalization must therefore not change the result of
+        # hash(x) for any x.
+
+        class HalibutProxy:
+            def __hash__(self):
+                return hash('halibut')
+            def __eq__(self, other):
+                return other == 'halibut'
+
+        x = {'halibut', HalibutProxy()}
+        self.assertEqual(len(x), 1)
+
+
+def test_main():
+    run_unittest(HashTest)
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 2caf09f..c056f9a 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -426,6 +426,23 @@
         self.assertEqual(type(sys.int_info.bits_per_digit), int)
         self.assertEqual(type(sys.int_info.sizeof_digit), int)
         self.assertIsInstance(sys.hexversion, int)
+
+        self.assertEqual(len(sys.hash_info), 5)
+        self.assertLess(sys.hash_info.modulus, 2**sys.hash_info.width)
+        # sys.hash_info.modulus should be a prime; we do a quick
+        # probable primality test (doesn't exclude the possibility of
+        # a Carmichael number)
+        for x in range(1, 100):
+            self.assertEqual(
+                pow(x, sys.hash_info.modulus-1, sys.hash_info.modulus),
+                1,
+                "sys.hash_info.modulus {} is a non-prime".format(
+                    sys.hash_info.modulus)
+                )
+        self.assertIsInstance(sys.hash_info.inf, int)
+        self.assertIsInstance(sys.hash_info.nan, int)
+        self.assertIsInstance(sys.hash_info.imag, int)
+
         self.assertIsInstance(sys.maxsize, int)
         self.assertIsInstance(sys.maxunicode, int)
         self.assertIsInstance(sys.platform, str)