From 088eea77e18fb893026468212110433de9724dc2 Mon Sep 17 00:00:00 2001 From: jaca Date: Fri, 1 Sep 2023 15:15:09 +0200 Subject: [PATCH 1/3] fix #526 --- CHANGES.rst | 5 ++- src/icalendar/cal.py | 16 +++++++++ ...sue_526_calendar_with_different_events.ics | 18 ++++++++++ .../issue_526_calendar_with_events.ics | 18 ++++++++++ ...sue_526_calendar_with_shuffeled_events.ics | 18 ++++++++++ src/icalendar/tests/test_unit_cal.py | 36 +++++++++++++++++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_events.ics create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics diff --git a/CHANGES.rst b/CHANGES.rst index 861ff360..edc5cc05 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,10 @@ New features: Bug fixes: -- ... +- Calendar components are now properly compared + Ref: #550 + Fixes: #526 + [jacadzaca] 5.0.7 (2023-05-29) ------------------ diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 35a288bf..b718765b 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -436,6 +436,22 @@ def __repr__(self): subs = ', '.join(str(it) for it in self.subcomponents) return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})" + def __eq__(self, other): + properties_equal = super().__eq__(other) + if not properties_equal: + return False + + # The subcomponents might not be in the same order, + # neither there's a natural key we can sort the subcomponents by nor + # are the subcomponent types hashable, so we cant put them in a set to + # check for set equivalence. We have to iterate over the subcomponents + # and look for each of them in the list. + for subcomponent in self.subcomponents: + if subcomponent not in other.subcomponents: + return False + + return True + ####################################### # components defined in RFC 5545 diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics new file mode 100644 index 00000000..67b32f54 --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:ical-jacadzaca-3 +SUMMARY: Some very different event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +BEGIN:VEVENT +UID:ical-jacadzaca-4 +SUMMARY: Some very different other event +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics new file mode 100644 index 00000000..2406497a --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:1 +SUMMARY: Some event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +BEGIN:VEVENT +UID:2 +SUMMARY: Some other event +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics new file mode 100644 index 00000000..6833bdab --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:2 +SUMMARY: Some other event +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 +DTSTAMP:20211004T150245Z +END:VEVENT +BEGIN:VEVENT +UID:1 +SUMMARY: Some event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 3d09f046..9d9a1100 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -2,6 +2,8 @@ from datetime import timedelta import unittest +import pytest + import icalendar import pytz import re @@ -455,3 +457,37 @@ def test_cal_strict_parsing(self): icalendar.vUTCOffset.ignore_exceptions = True self.assertEqual(icalendar.Calendar.from_ical(cal_str).to_ical(), cal_str) icalendar.vUTCOffset.ignore_exceptions = False + + +@pytest.mark.parametrize('calendar', [ + 'issue_156_RDATE_with_PERIOD_TZID_khal', + 'issue_156_RDATE_with_PERIOD_TZID_khal_2', + 'issue_178_custom_component_contains_other', + 'issue_178_custom_component_inside_other', + 'issue_526_calendar_with_events', + 'issue_526_calendar_with_different_events', +]) +@pytest.mark.parametrize('other_calendar', [ + 'issue_156_RDATE_with_PERIOD_TZID_khal', + 'issue_156_RDATE_with_PERIOD_TZID_khal_2', + 'issue_178_custom_component_contains_other', + 'issue_178_custom_component_inside_other', + 'issue_526_calendar_with_events', + 'issue_526_calendar_with_different_events', +]) +def test_comparing_calendars(calendars, calendar, other_calendar): + are_calendars_equal = calendars[calendar] == calendars[other_calendar] + are_calendars_actually_equal = calendar == other_calendar + assert are_calendars_equal == are_calendars_actually_equal + + +@pytest.mark.parametrize('calendar, shuffeled_calendar', [ + ( + 'issue_526_calendar_with_events', + 'issue_526_calendar_with_shuffeled_events', + ), +]) +def test_calendars_with_same_subcomponents_in_different_order_are_equal(calendars, calendar, shuffeled_calendar): + assert not calendars[calendar].subcomponents == calendars[shuffeled_calendar].subcomponents + assert calendars[calendar] == calendars[shuffeled_calendar] + From 4f0028663e0ef8aa3bd420bb0e12f4940d631881 Mon Sep 17 00:00:00 2001 From: jaca Date: Fri, 1 Sep 2023 20:15:57 +0200 Subject: [PATCH 2/3] dont repeat parameter list in test_comparing_calendars --- src/icalendar/tests/test_unit_cal.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 9d9a1100..e3a9ce3a 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -1,3 +1,4 @@ +import itertools from datetime import datetime from datetime import timedelta import unittest @@ -459,22 +460,17 @@ def test_cal_strict_parsing(self): icalendar.vUTCOffset.ignore_exceptions = False -@pytest.mark.parametrize('calendar', [ - 'issue_156_RDATE_with_PERIOD_TZID_khal', - 'issue_156_RDATE_with_PERIOD_TZID_khal_2', - 'issue_178_custom_component_contains_other', - 'issue_178_custom_component_inside_other', - 'issue_526_calendar_with_events', - 'issue_526_calendar_with_different_events', -]) -@pytest.mark.parametrize('other_calendar', [ - 'issue_156_RDATE_with_PERIOD_TZID_khal', - 'issue_156_RDATE_with_PERIOD_TZID_khal_2', - 'issue_178_custom_component_contains_other', - 'issue_178_custom_component_inside_other', - 'issue_526_calendar_with_events', - 'issue_526_calendar_with_different_events', -]) +@pytest.mark.parametrize( + 'calendar, other_calendar', + itertools.product([ + 'issue_156_RDATE_with_PERIOD_TZID_khal', + 'issue_156_RDATE_with_PERIOD_TZID_khal_2', + 'issue_178_custom_component_contains_other', + 'issue_178_custom_component_inside_other', + 'issue_526_calendar_with_events', + 'issue_526_calendar_with_different_events', + ], repeat=2) +) def test_comparing_calendars(calendars, calendar, other_calendar): are_calendars_equal = calendars[calendar] == calendars[other_calendar] are_calendars_actually_equal = calendar == other_calendar From 2dfd51cf59f7ab3a1f2572b04f2b76a198201a33 Mon Sep 17 00:00:00 2001 From: jaca Date: Fri, 1 Sep 2023 20:34:32 +0200 Subject: [PATCH 3/3] dont treat components with a subset of other a larger components' subcomponents as equal to the bigger component --- src/icalendar/cal.py | 3 +++ .../issue_526_calendar_with_event_subset.ics | 11 +++++++++++ src/icalendar/tests/test_unit_cal.py | 1 + 3 files changed, 15 insertions(+) create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index b718765b..40889717 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -437,6 +437,9 @@ def __repr__(self): return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})" def __eq__(self, other): + if not len(self.subcomponents) == len(other.subcomponents): + return False + properties_equal = super().__eq__(other) if not properties_equal: return False diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics new file mode 100644 index 00000000..4624f2f5 --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:1 +SUMMARY: Some event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index e3a9ce3a..410abce5 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -469,6 +469,7 @@ def test_cal_strict_parsing(self): 'issue_178_custom_component_inside_other', 'issue_526_calendar_with_events', 'issue_526_calendar_with_different_events', + 'issue_526_calendar_with_event_subset', ], repeat=2) ) def test_comparing_calendars(calendars, calendar, other_calendar):