=== modified file 'twisted/python/test/test_util.py' --- twisted/python/test/test_util.py 2010-08-02 17:16:05 +0000 +++ twisted/python/test/test_util.py 2010-10-06 14:31:30 +0000 @@ -616,6 +615,256 @@ +class Hashable(util.FancyHashMixin): + """ + Trivial class that uses L{util.FancyHashMixin}. + """ + compareAttributes = hashAttributes = ("value",) + def __init__(self, value): + self.value = value + + + +class DifferentHashable(util.FancyHashMixin): + """ + Trivial class that uses L{util.FancyHashMixin}, but is unrelated to + L{Hashable}. + """ + compareAttributes = hashAttributes = ("value",) + def __init__(self, value): + self.value = value + + + +class PartialHashable(util.FancyHashMixin): + """ + A class that uses L{util.FancyHashMixin}, but only hashes on part of its + attributes. + """ + compareAttributes = ("valueOne", "valueTwo") + hashAttributes = ("valueOne",) + + def __init__(self, valueOne, valueTwo): + self.valueOne, self.valueTwo = valueOne, valueTwo + + + +class DifferentPartialHashable(util.FancyHashMixin): + """ + A class that isn't L{PartialHashable}, but behaves like it. + """ + compareAttributes = ("valueOne", "valueTwo") + hashAttributes = ("valueOne",) + + def __init__(self, valueOne, valueTwo): + self.valueOne, self.valueTwo = valueOne, valueTwo + + + +class FancyHashMixinTests(unittest.TestCase): + """ + Tests for hashable objects that use L{util.FancyHashMixin}. + + This tests reasonably simple implementations where all attributes used for + testing equality are also used in producing hash values. + """ + def test_equality(self): + """ + Tests that a hashable object compares equal to itself, and that two + such objects that should compare equal compare equal. + """ + h1, h2 = Hashable(1), Hashable(1) + self.assertEquals(h1, h1) + self.assertEquals(h1, h2) + + + def test_inequality(self): + """ + Tests that two unequal hashables compare unequal. + """ + h1, h2 = Hashable(1), Hashable(2) + self.assertNotEquals(h1, h2) + + + def test_inequalityDifferentClasses(self): + """ + Tests that two unequal hashables of different type compare unequal. + """ + h1, h2 = Hashable(1), DifferentHashable(2) + self.assertNotEquals(h1, h2) + + + def test_hashConsistent(self): + """ + Tests that hashes are consistent when recomputed. + """ + h = Hashable(1) + self.assertEquals(hash(h), hash(h)) + + + def test_hashEqual(self): + """ + Tests that two equal hashables hash equal. + """ + h1, h2 = Hashable(1), Hashable(1) + self.assertEquals(hash(h1), hash(h2)) + + + def test_hashEqualDifferentClasses(self): + """ + Tests that two hashables of different type (but with the same hash + attributes) hash equal. + """ + h1, h2 = Hashable(1), DifferentHashable(1) + self.assertEquals(hash(h1), hash(h2)) + + + def test_hashNotEqual(self): + """ + Tests that two unequal hashables hash unequal. + + Although this is not a strict requirement of hashes, it is necessary + to prevent hash tables from performing pathologically. + + Since L{util.FancyHashMixin} uses a tuple internally, the hash value + of the class that uses it is dependant on the hash attributes. Hence, + it suffices to use immutable objects that have unique hashes. + """ + h1, h2 = Hashable(1), Hashable(2) + self.assertNotEquals(hash(h1), hash(h2)) + + + def test_hashNotEqualDifferentClasses(self): + """ + Tests that two unequal hashables of different type hash unequal. + """ + h1, h2 = Hashable(1), DifferentHashable(2) + self.assertNotEquals(hash(h1), hash(h2)) + + + def test_set(self): + """ + Tests that two equal hashables, when added to a set, produce a set of + size 1. + """ + s = set([Hashable(1), Hashable(1)]) + self.assertEquals(len(s), 1) + + + +class PartialHashableTests(unittest.TestCase): + """ + Tests for hashable objects that use L{util.FancyHashMixin}. + + This tests reasonably simple implementations where all attributes used for + testing equality are also used in producing hash values. + """ + def test_equality(self): + """ + Tests that a partially hashable object compares equal to itself, and + that two such objects that should compare equal compare equal. + """ + h1, h2 = PartialHashable(1, 2), PartialHashable(1, 2) + self.assertEquals(h1, h1) + self.assertEquals(h1, h2) + + + def test_inequality(self): + """ + Tests that two unequal partial hashables compare unequal. + """ + h1, h2 = PartialHashable(1, 2), PartialHashable(2, 1) + self.assertNotEquals(h1, h2) + + + def test_hashConsistent(self): + """ + Tests that hashes are consistent when recomputed. + """ + h = PartialHashable(1, 2) + self.assertEquals(hash(h), hash(h)) + + + def test_hashEqual(self): + """ + Tests that two equal partial hashables hash equal. + """ + h1, h2 = PartialHashable(1, 2), PartialHashable(1, 2) + self.assertEquals(hash(h1), hash(h2)) + + + def test_hashEqualDifferentClasses(self): + """ + Tests that equal partial hashables of different classes hash equal. + """ + h1, h2 = PartialHashable(1, 2), DifferentPartialHashable(1, 2) + self.assertEquals(hash(h1), hash(h2)) + + + def test_hashEqualPartial(self): + """ + Tests that partial hashables with equal hash attirbutes hash equal. + """ + h1, h2 = PartialHashable(1, 1), PartialHashable(1, 2) + self.assertEquals(hash(h1), hash(h2)) + + + def test_hashEqualPartialDifferentClasses(self): + """ + Tests that partial hashables of different type with equal hash + attirbutes hash equal. + """ + h1, h2 = PartialHashable(1, 1), DifferentPartialHashable(1, 2) + self.assertEquals(hash(h1), hash(h2)) + + + def test_hashNotEqual(self): + """ + Tests that unequal partial hashables hash unequal. + """ + h1, h2 = PartialHashable(1, 2), PartialHashable(2, 1) + self.assertNotEquals(hash(h1), hash(h2)) + + + def test_hashNotEqualDifferentClasses(self): + """ + Tests that unequal partial hashables of different classes hash + unequal. + """ + h1, h2 = PartialHashable(1, 2), DifferentPartialHashable(2, 1) + self.assertNotEquals(hash(h1), hash(h2)) + + + def test_set(self): + """ + Tests equal partial hasable behavior in a set. + + Only one such object can exist in a set at the same time. + """ + s = set([PartialHashable(1, 2), PartialHashable(1, 2)]) + self.assertEquals(len(s), 1) + + + def test_setEqualHashes(self): + """ + Tests unequal partial hashable behavior in a set for partially + hashables that hash equal. + """ + s = set([PartialHashable(1, 1), PartialHashable(1, 2)]) + self.assertEquals(len(s), 2) + + + def test_setEqualHashesDifferentClasses(self): + """ + Test equally hashing partial hashables of different class. + + Because they are unequal (different class), these can coexist in sets. + """ + s = set([PartialHashable(1, 2), DifferentPartialHashable(1, 2)]) + self.assertEquals(len(s), 2) + + + class RunAsEffectiveUserTests(unittest.TestCase): """ Test for the L{util.runAsEffectiveUser} function. === modified file 'twisted/python/util.py' --- twisted/python/util.py 2010-08-02 17:16:05 +0000 +++ twisted/python/util.py 2010-10-06 14:55:21 +0000 @@ -545,8 +545,33 @@ class FancyEqMixin: + """ + A mixin for objects that implement (in-)equality. + + Classes that use this mixin are assumed to be mutable. As a result, they + are not hashable. For creating hashable types, look at L{FancyHashMixin}. + + For objects that do not have C{compareAttributes} set, this class will + compare on identity, as usual. + + Otherwise, classes will compare equal iff they are of the same type and + all of their C{compareAttributes} are equal. + + This class correctly cooperates with other types implementing equality + checks using the C{NotImplemented} sentinel value. + + @cvar compareAttributes: The attributes used in comparing equality. + @type compareAttributes: C{tuple} of attribute names + """ compareAttributes = () + def __eq__(self, other): + """ + Tests if this object is equal to another object. + + @param other: The object this object will be compared to. + @return: C{True} if equal to this object, C{False} otherwise. + """ if not self.compareAttributes: return self is other if isinstance(self, other.__class__): @@ -557,12 +582,54 @@ def __ne__(self, other): + """ + Tests if this object is unequal to another object. + + @param other: The object this object will be compared to. + @return: C{True} if not equal to this object, C{False} otherwise. + """ result = self.__eq__(other) if result is NotImplemented: return result return not result + __hash__ = None + + + +class FancyHashMixin(FancyEqMixin): + """ + A mixin for hashable objects. + + Hashable objects must hash consistently during their lifetime. This means + that any attribute used in computing the hash must be immutable. + + For practicality reasons, this object also implements equality as a + L{FancyEqMixin}. C{__eq__} may still be overridden for classes requiring + custom equality testing. Bear in mind that objects that compare equal must + also hash equal. + + Most implementors will simply want to make C{compareAttributes} and + C{hashAttributes} the same. Provided these attributes are all immutable, + this class will guarantee all hashability invariants. + + @cvar hashAttributes: The attributes used in computing the hash. + @type hashAttributes: C{tuple} of attribute names + """ + compareAttributes = hashAttributes = () + + def __hash__(self): + """ + Computes the hash for this object. + + @return: The hash value for this object. + @rtype: C{int} + """ + values = tuple(getattr(self, name) for name in self.hashAttributes) + return hash(values) + + def dsu(list, key): """ @@ -973,4 +1040,5 @@ "dsu", "switchUID", "SubclassableCStringIO", "moduleMovedForSplit", "unsignedID", "mergeFunctionMetadata", "nameToLabel", "uidFromString", "gidFromString", "runAsEffectiveUser", "moduleMovedForSplit", + "FancyHashMixin" ]