diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index 43a47a06..12390671 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -79,9 +79,17 @@ class CalendarObjectResource(DAVObject): - """ - Ref RFC 4791, section 4.1, a "Calendar Object Resource" can be an + """Ref RFC 4791, section 4.1, a "Calendar Object Resource" can be an event, a todo-item, a journal entry, or a free/busy entry + + As per the RFC, a CalendarObjectResource can at most contain one + calendar component, with the exception of recurrence components. + Meaning that event.data typically contains one VCALENDAR with one + VEVENT and possibly one VTIMEZONE. + + In the case of expanded calendar date searches, each recurrence + will (by default) wrapped in a distinct CalendarObjectResource + object. This is a deviation from the definition given in the RFC. """ ## There is also STARTTOFINISH, STARTTOSTART and FINISHTOFINISH in RFC9253, @@ -473,7 +481,7 @@ def _set_icalendar_component(self, value) -> None: icalendar_component = property( _get_icalendar_component, _set_icalendar_component, - doc="icalendar component - should not be used with recurrence sets", + doc="icalendar component - this is the simplest way to access the event/task - it will give you the first component that isn't a timezone component. For recurrence sets, the master component will be returned. For any non-recurring event/task/journal, there should be only one calendar component in the object. For results from an expanded search, there should be only one calendar component in the object", ) component = icalendar_component diff --git a/tests/test_caldav.py b/tests/test_caldav.py index c0e2b945..99ae37bd 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -843,6 +843,51 @@ def testSchedulingMailboxes(self): inbox = self.principal.schedule_inbox() outbox = self.principal.schedule_outbox() + def testIssue397(self): + cal = self._fixCalendar() + cal.save_event( + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//PeterB//caldav//en_DK +BEGIN:VEVENT +SUMMARY:recurrence with attendee one single item +DTSTART;TZID=Europe/Zurich:20240101T090000 +DTEND;TZID=Europe/Zurich:20240101T180000 +UID:test1 +DESCRIPTION:this is the recurrent series +TRANSP:OPAQUE +RRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH +END:VEVENT +BEGIN:VEVENT +SUMMARY:single item +DTSTART;TZID=Europe/Zurich:20240605T090000 +DTEND;TZID=Europe/Zurich:20240605T170000 +UID:test1 +DESCRIPTION:this is the single item assigning a attendee to just one event +ATTENDEE:foo.bar@corge.baz +RECURRENCE-ID:20240605T070000Z +END:VEVENT +END:VCALENDAR +""" + ) + + object_by_id = cal.object_by_uid("test1", comp_class=Event) + instance = object_by_id.icalendar_instance + events = [ + event + for event in instance.subcomponents + if isinstance(event, icalendar.Event) + ] + assert len(events) == 2 + object_by_id = cal.object_by_uid("test1", comp_class=None) + instance = object_by_id.icalendar_instance + events = [ + event + for event in instance.subcomponents + if isinstance(event, icalendar.Event) + ] + assert len(events) == 2 + def testPropfind(self): """ Test of the propfind methods. (This is sort of redundant, since @@ -3001,7 +3046,7 @@ def search(month): event=True, start=datetime(2015, month, 1), end=datetime(2015, month, 2), - expand="client", ## client will be default from 2.0 + expand=True, ) assert len(recurrence) == 1 return recurrence[0]