Repository: collective/icalendar

Issue:
[REGRESSION] DDDType not hashable in icalendar 5
Compare this:

```
>>> import icalendar
>>> import datetime
>>> icalendar.__version__
'4.0.9'
>>> icalendar.vDDDTypes(datetime(2022,10,10)).__hash__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'module' object is not callable
>>> icalendar.vDDDTypes(datetime.datetime(2022,10,10)).__hash__()
8729746249635
>>> icalendar.vDDDTypes(datetime.datetime(2022,10,10)).__hash__
<method-wrapper '__hash__' of vDDDTypes object at 0x7f08d6c97cd0>
>>>
```

with this:

```
>>> import icalendar
>>> import datetime
>>> icalendar.__version__
'5.0.1'
>>> icalendar.vDDDTypes(datetime.datetime(2022,10,10)).__hash__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
>>> icalendar.vDDDTypes(datetime.datetime(2022,10,10)).__hash__
```

I noticed tests failing because of this in another project - https://github.com/niccokunzmann/python-recurring-ical-events/issues/99

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index b46dae7..d0682ba 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -14,11 +14,11 @@ Breaking changes:
 
 New features:
 
-- ...
+- vDDDTypes is hashable #487 #492 [niccokunzmann]
 
 Bug fixes:
 
-- ...
+- vDDDTypes' equality also checks the dt attribute #497 #492 [niccokunzmann]
 
 5.0.2 (2022-11-03)
 ------------------
diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py
index f6c261c..dcbb926 100644
--- a/src/icalendar/prop.py
+++ b/src/icalendar/prop.py
@@ -320,9 +320,12 @@ class vDDDTypes:
 
     def __eq__(self, other):
         if isinstance(other, vDDDTypes):
-            return self.params == other.params
+            return self.params == other.params and self.dt == other.dt
         return False
 
+    def __hash__(self):
+        return hash(self.dt)
+
     @classmethod
     def from_ical(cls, ical, timezone=None):
         if isinstance(ical, cls):


Test patch:
diff --git a/src/icalendar/tests/test_unit_prop.py b/src/icalendar/tests/test_unit_prop.py
index 0549320..b732b23 100644
--- a/src/icalendar/tests/test_unit_prop.py
+++ b/src/icalendar/tests/test_unit_prop.py
@@ -4,10 +4,12 @@ from datetime import time
 from datetime import timedelta
 from icalendar.parser import Parameters
 import unittest
-from icalendar.prop import vDatetime
+from icalendar.prop import vDatetime, vDDDTypes
 from icalendar.windows_to_olson import WINDOWS_TO_OLSON
-
+import pytest
 import pytz
+from copy import deepcopy
+from dateutil import tz
 
 
 class TestProp(unittest.TestCase):
@@ -112,27 +114,6 @@ class TestProp(unittest.TestCase):
         # Bad input
         self.assertRaises(ValueError, vDDDTypes, 42)
 
-    def test_vDDDTypes_equivalance(self):
-        from ..prop import vDDDTypes
-        from copy import deepcopy
-
-        vDDDTypes = [
-                vDDDTypes(pytz.timezone('US/Eastern').localize(datetime(year=2022, month=7, day=22, hour=12, minute=7))),
-                vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7)),
-                vDDDTypes(date(year=2022, month=7, day=22)),
-                vDDDTypes(time(hour=22, minute=7, second=2))]
-
-        for v_type in vDDDTypes:
-            self.assertEqual(v_type, deepcopy(v_type))
-            for other in vDDDTypes:
-                if other is v_type:
-                    continue
-                self.assertNotEqual(v_type, other)
-
-            # see if equivalnce fails for other types
-            self.assertNotEqual(v_type, 42)
-            self.assertNotEqual(v_type, 'test')
-
     def test_prop_vDate(self):
         from ..prop import vDate
 
@@ -518,6 +499,39 @@ class TestProp(unittest.TestCase):
         )
 
 
+
+vDDDTypes_list = [
+    vDDDTypes(pytz.timezone('US/Eastern').localize(datetime(year=2022, month=7, day=22, hour=12, minute=7))),
+    vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7)),
+    vDDDTypes(datetime(year=2022, month=7, day=22, hour=12, minute=7, tzinfo=tz.UTC)),
+    vDDDTypes(date(year=2022, month=7, day=22)),
+    vDDDTypes(date(year=2022, month=7, day=23)),
+    vDDDTypes(time(hour=22, minute=7, second=2))
+]
+
+def identity(x):
+    return x
+
+@pytest.mark.parametrize("map", [
+    deepcopy,
+    identity,
+    hash,
+])
+@pytest.mark.parametrize("v_type", vDDDTypes_list)
+@pytest.mark.parametrize("other", vDDDTypes_list)
+def test_vDDDTypes_equivalance(map, v_type, other):
+    if v_type is other:
+        assert map(v_type) == map(other), f"identity implies equality: {map.__name__}()"
+        assert not (map(v_type) != map(other)), f"identity implies equality: {map.__name__}()"
+    else:
+        assert map(v_type) != map(other), f"expected inequality: {map.__name__}()"
+        assert not (map(v_type) == map(other)), f"expected inequality: {map.__name__}()"
+
+@pytest.mark.parametrize("v_type", vDDDTypes_list)
+def test_inequality_with_different_types(v_type):
+    assert v_type != 42
+    assert v_type != 'test'
+
 class TestPropertyValues(unittest.TestCase):
 
     def test_vDDDLists_timezone(self):