diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e7d3a83b5923ce6d4801984872968c2c0b51b87..d0e3fe6915d9ef0699f595aefbe75f5abcb33a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ # v2.17.23 * un-deprecated the `cps` metric +* OmniHashableMixin will check for typing as well +* fixed a bug in OmniHashableMixin regarding attribute checks diff --git a/satella/__init__.py b/satella/__init__.py index 90911dcdd7cf1fc756016476dbfd86cde91bb810..300d2842b0d96d61306208499a0426c2f4966aa7 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.17.23a1' +__version__ = '2.17.23' diff --git a/satella/coding/structures/mixins/hashable.py b/satella/coding/structures/mixins/hashable.py index 930ca386cd1319a136686150a8777eb0dcf824c2..b53be462b258a643dd370bd9757f0c37329f78e8 100644 --- a/satella/coding/structures/mixins/hashable.py +++ b/satella/coding/structures/mixins/hashable.py @@ -143,6 +143,8 @@ class OmniHashableMixin(metaclass=ABCMeta): Do everything in your power to make specified fields immutable, as mutating them will result in a different hash. + This will also check if the other value is an instance of this instance's class. + _HASH_FIELDS_TO_USE can be also a single string, in this case a single field called by this name will be taken. @@ -178,29 +180,40 @@ class OmniHashableMixin(metaclass=ABCMeta): """ Note that this will only compare _HASH_FIELDS_TO_USE """ + if not isinstance(other, type(self)): + return False + if isinstance(other, OmniHashableMixin): cmpr_by = self._HASH_FIELDS_TO_USE - - if isinstance(cmpr_by, str): - return getattr(self, cmpr_by) == getattr(other, cmpr_by) - - for field_name in self._HASH_FIELDS_TO_USE: - if getattr(self, field_name) != getattr(other, field_name): - return False - return True + try: + if isinstance(cmpr_by, str): + return getattr(self, cmpr_by) == getattr(other, cmpr_by) + + for field_name in self._HASH_FIELDS_TO_USE: + if getattr(self, field_name) != getattr(other, field_name): + return False + return True + except AttributeError: + return False else: return super().__eq__(other) def __ne__(self, other) -> bool: + if not isinstance(other, type(self)): + return True + if isinstance(other, OmniHashableMixin): cmpr_by = self._HASH_FIELDS_TO_USE - if isinstance(cmpr_by, str): - return getattr(self, cmpr_by) != getattr(other, cmpr_by) + try: + if isinstance(cmpr_by, str): + return getattr(self, cmpr_by) != getattr(other, cmpr_by) - for field_name in cmpr_by: - if getattr(self, field_name) != getattr(other, field_name): - return True - return False + for field_name in cmpr_by: + if getattr(self, field_name) != getattr(other, field_name): + return True + return False + except AttributeError: + return True else: return super().__ne__(other) diff --git a/tests/test_coding/test_structures.py b/tests/test_coding/test_structures.py index c951204f73665e76d9fd898bf682e0bbe365de3d..b0fd4a7f40f16f3c081f763afb71e1f4c65ccdea 100644 --- a/tests/test_coding/test_structures.py +++ b/tests/test_coding/test_structures.py @@ -664,6 +664,33 @@ class TestMisc(unittest.TestCase): self.assertEqual(a[e1], '1') self.assertEqual(hash(e1), hash(2)) + def test_omni_not_filled_in_fields(self): + class Omni1(OmniHashableMixin): + _HASH_FIELDS_TO_USE = 'a' + + def __init__(self, a=None): + if a is not None: + self.a = a + + self.assertNotEqual(Omni1(3), Omni1()) + self.assertFalse(Omni1() == Omni1(3)) + + def test_omni_different_hierarchies(self): + class Omni1(OmniHashableMixin): + _HASH_FIELDS_TO_USE = 'a' + + def __init__(self, a): + self.a = a + + class Omni2(OmniHashableMixin): + _HASH_FIELDS_TO_USE = 'a' + + def __init__(self, a): + self.a = a + + self.assertNotEqual(Omni1(1), Omni2(1)) + self.assertFalse(Omni1(1) == Omni2(1)) + def test_omni_single_field(self): class Omni(OmniHashableMixin): _HASH_FIELDS_TO_USE = 'a'