diff --git a/CHANGELOG.md b/CHANGELOG.md index fc364fde..69cdb5e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,25 +8,43 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ## [2.0.0] - [Unreleased] -In version 2.0, the requests library will be replaced with niquests or httpx. See https://github.com/python-caldav/caldav/issues/457. Master branch is currently running niquests. +In version 2.0, the requests library will be replaced with niquests or httpx. See https://github.com/python-caldav/caldav/issues/457. Master branch is currently running niquests. Work by @ArtemIsmagilov, https://github.com/python-caldav/caldav/pull/455 -In version 2.0, support for python 3.7 will be dropped, possibly also python 3.8. Master branch supports both as for now. +In version 2.0, support for python 3.7 and python 3.8 will be officially dropped. Master branch *should* support both as for now, but Python 3.7 is no longer tested. ## [1.5.0] - [Unreleased] ### Deprecated -Python 3.7 is no longer tested - but it should work. Please file a bug report if it doesn't work. (Note that the caldav library pulls in many dependencies, and not all of them supports dead snakes). +Python 3.7 is no longer tested (dependency problems) - but it should work. Please file a bug report if it doesn't work. (Note that the caldav library pulls in many dependencies, and not all of them supports dead snakes). -### Changed +### Fixed +* Servers that return a quoted URL in their path will now be parsed correctly by @edel-macias-cubix in https://github.com/python-caldav/caldav/pull/473 +* Compatibility workaround: If `event.load()` fails, it will retry the load by doing a multiget - https://github.com/python-caldav/caldav/pull/460 and https://github.com/python-caldav/caldav/pull/475 - https://github.com/python-caldav/caldav/issues/459 +* Compatibility workaround: A problem with a wiki calendar fixed by @soundstorm in https://github.com/python-caldav/caldav/pull/469 +* Blank passwords should be acceptable - https://github.com/python-caldav/caldav/pull/481 +* Compatibility workaround: Accept XML content from calendar server even if it's marked up with content-type text/plain by @niccokunzmann in https://github.com/python-caldav/caldav/pull/465 +* Bugfix for saving component failing on multi-component recurrence objects - https://github.com/python-caldav/caldav/pull/467 * Some exotic servers may return object URLs on search, but it does not work out to fetch the calendar data. Now it will log an error instead of raising an error in such cases. +* Some workarounds and fixes for getting tests passing on all the test servers I had at hand in https://github.com/python-caldav/caldav/pull/492 + +### Changed + * The `tests/compatibility_issues.py` has been moved to `caldav/compatibility_hints.py`, this to make it available for a caldav-server-tester-tool that I'm splitting off to a separate project/repository, and also to make https://github.com/python-caldav/caldav/issues/402 possible. #### Refactoring * Minor code cleanups by github user @ArtemIsmagilov in https://github.com/python-caldav/caldav/pull/456 -* The very much overgrown `objects.py`-file has been split into three. +* The very much overgrown `objects.py`-file has been split into three - https://github.com/python-caldav/caldav/pull/483 +* Refactor compatibility issues by @tobixen in https://github.com/python-caldav/caldav/pull/484 +* Refactoring of `multiget` in https://github.com/python-caldav/caldav/pull/492 + +### Documentation + +* Add more project links to PyPI by @niccokunzmann in https://github.com/python-caldav/caldav/pull/464 +* Document how to use tox for testing by @niccokunzmann in https://github.com/python-caldav/caldav/pull/466 +* Readthedocs integration has been repaired (https://github.com/python-caldav/caldav/pull/453 - but eventually the fix was introduced directly in the master branch) #### Test framework @@ -39,20 +57,18 @@ Python 3.7 is no longer tested - but it should work. Please file a bug report i * Allows offline testing of my upcoming `check_server_compatibility`-script * Also added the possibility to tag test servers with a name * Many changes done to the compatibility flag list (due to work on the server-checker project) +* Functional tests for multiget in https://github.com/python-caldav/caldav/pull/489 ### Added +* support easy search for journals by @tobixen in https://github.com/python-caldav/caldav/pull/486 +* Methods for verifying and adding reverse relations - https://github.com/python-caldav/caldav/pull/336 +* Easy creation of events and tasks with alarms, search for alarms - https://github.com/python-caldav/caldav/pull/221 * Work in progress: `auto_conn`, `auto_calendar` and `auto_calendars` may read caldav connection and calendar configuration from a config file, environmental variables or other sources. Currently I've made the minimal possible work to be able to test the caldav-server-tester script. * By now `calendar.search(..., sort_keys=("DTSTART")` will work. Sort keys expects a list or a tuple, but it's easy to send an attribute by mistake. https://github.com/python-caldav/caldav/issues/448 https://github.com/python-caldav/caldav/pull/449 -* Compatibility workaround: If `event.load()` fails, it will retry the load by doing a multiget - https://github.com/python-caldav/caldav/pull/475 - https://github.com/python-caldav/caldav/issues/459 * The `class_`-parameter now works when sending data to `save_event()` etc. * Search method now takes parameter `journal=True`. ref https://github.com/python-caldav/caldav/issues/237 and https://github.com/python-caldav/caldav/pull/486 -### Fixed - -* Bugfix for saving component failing on multi-component recurrence objects - https://github.com/python-caldav/caldav/pull/467 -* Readthedocs integration has been repaired (https://github.com/python-caldav/caldav/pull/453 - but eventually the fix was introduced directly in the master branch) - ## [1.4.0] - 2024-11-05 @@ -86,8 +102,6 @@ Python 3.7 is no longer tested - but it should work. Please file a bug report i ### Added -* Methods for verifying and adding reverse relations - https://github.com/python-caldav/caldav/pull/336 -* Easy creation of events and tasks with alarms, search for alarms - https://github.com/python-caldav/caldav/pull/221 * Allow to reverse the sorting order on search function by @twissell- in https://github.com/python-caldav/caldav/pull/433 * Work on integrating typing information. Details in https://github.com/python-caldav/caldav/pull/358 * Remove dependency on pytz. Details in https://github.com/python-caldav/caldav/issues/231 and https://github.com/python-caldav/caldav/pull/363 diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index 0205576f..5a660912 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -61,7 +61,9 @@ from typing import Self from .davobject import DAVObject -from .elements import cdav, dav +from .elements.cdav import CalendarData +from .elements import cdav +from .elements import dav from .lib import error from .lib import vcal from .lib.error import errmsg @@ -396,11 +398,6 @@ def fix_reverse_relations(self, pdb: bool = False) -> list: """ return self._handle_reverse_relations(verify=True, fix=True, pdb=pdb) - ## TODO: fix this (and consolidate with _handle_relations / set_relation?) - # def ensure_reverse_relations(self): - # missing_relations = self.check_reverse_relations() - # ... - def _get_icalendar_component(self, assert_one=False): """Returns the icalendar subcomponent - which should be an Event, Journal, Todo or FreeBusy from the icalendar class @@ -606,10 +603,10 @@ def load(self, only_if_unloaded: bool = False) -> Self: try: r = self.client.request(str(self.url)) + if r.status == 404: + raise error.NotFoundError(errmsg(r)) except: - return self.load_by_multiget() - if r.status == 404: - raise error.NotFoundError(errmsg(r)) + self.load_by_multiget() self.data = vcal.fix(r.raw) if "Etag" in r.headers: self.props[dav.GetEtag.tag] = r.headers["Etag"] @@ -623,15 +620,17 @@ def load_by_multiget(self) -> Self: with a multiget query """ error.assert_(self.url) - href = self.url.path - prop = dav.Prop() + CalendarData() - root = cdav.CalendarMultiGet() + prop + dav.Href(value=href) - response = self.parent._query(root, 1, "report") - results = response.expand_simple_props([CalendarData()]) - error.assert_(len(results) == 1) - data = results[href][CalendarData.tag] - error.assert_(data) - self.data = data + mydata = self.parent._multiget(event_urls=[self.url], raise_notfound=True) + try: + url, self.data = next(mydata) + except StopIteration: + ## We shouldn't come here. Something is wrong. + ## TODO: research it + ## As of 2025-05-20, this code section is used by + ## TestForServerECloud::testCreateOverwriteDeleteEvent + raise error.NotFoundError(self.url) + assert_(self.data) + assert_(next(mydata, None) is None) return self ## TODO: self.id should either always be available or never diff --git a/caldav/collection.py b/caldav/collection.py index 8d0a7fad..5f2b8ccf 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -59,7 +59,9 @@ from .calendarobjectresource import Journal from .calendarobjectresource import Todo from .davobject import DAVObject -from .elements import cdav, dav +from .elements.cdav import CalendarData +from .elements import cdav +from .elements import dav from .lib import error from .lib import vcal from .lib.python_utilities import to_wire @@ -558,15 +560,14 @@ def save(self): self._create(id=self.id, name=self.name, **self.extra_init_options) return self - ## TODO: this is missing test code. - ## TODO: needs refactoring: - ## Objects found may be Todo and Journal, not only Event. - ## Replace the last lines with _request_report_build_resultlist method - def calendar_multiget(self, event_urls: Iterable[URL]) -> List[_CC]: + # def data2object_class + + def _multiget( + self, event_urls: Iterable[URL], raise_notfound: bool = False + ) -> Iterable[str]: """ - get multiple events' data - @author mtorange@gmail.com - @type events list of Event + get multiple events' data. + TODO: Does it overlap the _request_report_build_resultlist method """ if self.url is None: raise ValueError("Unexpected value None for self.url") @@ -580,16 +581,40 @@ def calendar_multiget(self, event_urls: Iterable[URL]) -> List[_CC]: ) response = self._query(root, 1, "report") results = response.expand_simple_props([cdav.CalendarData()]) - rv = [ - Event( + if raise_notfound: + for href in response.statuses: + status = response.statuses[href] + if "404" in status: + raise error.NotFoundError(f"Status {status} in {href}") + for r in results: + yield (r, results[r][cdav.CalendarData.tag]) + + ## Replace the last lines with + def multiget( + self, event_urls: Iterable[URL], raise_notfound: bool = False + ) -> Iterable[_CC]: + """ + get multiple events' data + TODO: Does it overlap the _request_report_build_resultlist method? + @author mtorange@gmail.com (refactored by Tobias) + """ + results = self._multiget(event_urls, raise_notfound=raise_notfound) + for url, data in results: + yield self._calendar_comp_class_by_data(data)( self.client, - url=self.url.join(r), - data=results[r][cdav.CalendarData.tag], + url=self.url.join(url), + data=data, parent=self, ) - for r in results - ] - return rv + + def calendar_multiget(self, *largs, **kwargs): + """ + get multiple events' data + @author mtorange@gmail.com + (refactored by Tobias) + This is for backward compatibility. It may be removed in 3.0 or later release. + """ + return list(self.multiget(*largs, **kwargs)) ## TODO: Upgrade the warning to an error (and perhaps critical) in future ## releases, and then finally remove this method completely. diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 9b427b6c..87330ac3 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -77,6 +77,9 @@ 'no_scheduling_mailbox': """Parts of RFC6833 is supported, but not the existence of inbox/mailbox""", + 'no_scheduling_calendar_user_address_set': + """Parts of RFC6833 is supported, but not getting the calendar users addresses""", + 'no_default_calendar': """The given user starts without an assigned default calendar """ """(or without pre-defined calendars at all)""", @@ -268,6 +271,9 @@ 'no_search': """Apparently the calendar server does not support search at all (this often implies that 'object_by_uid_is_broken' has to be set as well)""", + 'no_search_openended': + """An open-ended search will not work""", + 'no_events_and_tasks_on_same_calendar': """Zimbra has the concept of task lists ... a calendar must either be a calendar with only events, or it can be a task list, but those must never be mixed""" } @@ -419,10 +425,11 @@ 'fragile_sync_tokens', ## no issue raised yet 'vtodo_datesearch_nodtstart_task_is_skipped', ## no issue raised yet 'broken_expand_on_exceptions', ## no issue raised yet - 'date_todo_search_ignores_duration' + 'date_todo_search_ignores_duration', 'calendar_color', 'calendar_order', - 'vtodo_datesearch_notime_task_is_skipped' + 'vtodo_datesearch_notime_task_is_skipped', + "no_alarmsearch", ] google = [ @@ -448,6 +455,9 @@ nextcloud = [ 'date_search_ignores_duration', + 'unique_calendar_ids', + 'broken_expand', + 'no_delete_calendar', 'sync_breaks_on_delete', 'no_recurring_todo', 'combined_search_not_working', @@ -488,7 +498,8 @@ 'text_search_not_working', 'no_relships', 'isnotdefined_not_working', - 'robur_rrule_freq_yearly_expands_monthly' + 'no_alarmsearch', + 'broken_expand', ] posteo = [ @@ -498,6 +509,7 @@ 'no_recurring_todo', 'no_sync_token', 'combined_search_not_working', + 'no_alarmsearch', 'broken_expand', ] @@ -527,7 +539,22 @@ ## Purelymail claims that the search indexes are "lazily" populated, ## so search works some minutes after the event was created/edited. - 'search_delay' + 'search_delay', + + ## I haven't raised this one with them yet + 'no_alarmsearch', +] + +gmx = [ + "no_scheduling_mailbox", + "no_mkcalendar", + "search_needs_comptype", + #"text_search_is_case_insensitive", + "no_freebusy_rfc4791", + "no_expand", + "no_search_openended", + "no_sync_token", + "no_scheduling_calendar_user_address_set", ] # fmt: on diff --git a/caldav/davclient.py b/caldav/davclient.py index 0f5d60b2..ef81acb3 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -283,6 +283,7 @@ def find_objects_and_props(self) -> Dict[str, Dict[str, _Element]]: self.sync_token will be populated if found, self.objects will be populated. """ self.objects: Dict[str, Dict[str, _Element]] = {} + self.statuses: Dict[str, str] = {} if "Schedule-Tag" in self.headers: self.schedule_tag = self.headers["Schedule-Tag"] @@ -300,6 +301,7 @@ def find_objects_and_props(self) -> Dict[str, Dict[str, _Element]]: ## but then there was https://github.com/python-caldav/caldav/issues/136 if href not in self.objects: self.objects[href] = {} + self.statuses[href] = status ## The properties may be delivered either in one ## propstat with multiple props or in multiple @@ -358,7 +360,7 @@ def _expand_simple_prop( error.assert_(len(values) == 1) return values[0] - ## TODO: "expand" does not feel quite right. + ## TODO: word "expand" does not feel quite right. def expand_simple_props( self, props: Iterable[BaseElement] = None, @@ -897,6 +899,7 @@ def auto_conn( try: idx = int(name) + name = None except ValueError: idx = None try: diff --git a/caldav/davobject.py b/caldav/davobject.py index 01cca546..d4e7309a 100644 --- a/caldav/davobject.py +++ b/caldav/davobject.py @@ -216,11 +216,11 @@ def _query( body = to_wire(body) if ( ret.status == 500 - and b"getetag" not in body - and b"" in body + and b"D:getetag" not in body + and b"", b"" + b" None: raise -ERR_FRAGMENT: str = "Please consider raising an issue at https://github.com/python-caldav/caldav/issues or reach out to t-caldav@tobixen.no, include this error and the traceback and tell what server you are using" +ERR_FRAGMENT: str = "Please consider raising an issue at https://github.com/python-caldav/caldav/issues or reach out to t-caldav@tobixen.no, include this error and the traceback (if any) and tell what server you are using" class DAVError(Exception): diff --git a/tests/conf.py b/tests/conf.py index bcfba3ae..2204132a 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -33,7 +33,13 @@ try: from .conf_private import caldav_servers except ImportError: - caldav_servers = [] + try: + from conf_private import caldav_servers + except ImportError: + try: + from tests.conf_private import caldav_servers + except ImportError: + caldav_servers = [] try: from .conf_private import test_private_test_servers @@ -121,7 +127,7 @@ def setup_radicale(self): i = 0 while True: try: - niquests.get(self.url) + niquests.get(str(self.url)) break except: time.sleep(0.05) @@ -201,7 +207,7 @@ def teardown_xandikos(self): ## ... but the thread may be stuck waiting for a request ... def silly_request(): try: - niquests.get(self.url) + niquests.get(str(self.url)) except: pass @@ -241,7 +247,7 @@ def client( ): kwargs_ = kwargs.copy() no_args = not any(x for x in kwargs if kwargs[x] is not None) - if idx is None and no_args and caldav_servers: + if idx is None and name is None and no_args and caldav_servers: ## No parameters given - find the first server in caldav_servers list return client(idx=0) elif idx is not None and no_args and caldav_servers: diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 76054913..54e54c2b 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -136,6 +136,8 @@ "test4", "test5", "test6", + "c26921f4-0653-11ef-b756-58ce2a14e2e5", + "e2a2e13e-34f2-11f0-ae12-1c1bb5134174", ) ## TODO: todo7 is an item without uid. Should be taken care of somehow. @@ -554,7 +556,7 @@ def testInviteAndRespond(self): ## inbox/outbox? -def _delay_decorator(f, t=60): +def _delay_decorator(f, t=20): def foo(*a, **kwa): time.sleep(t) return f(*a, **kwa) @@ -751,6 +753,7 @@ def testSupport(self): def testSchedulingInfo(self): self.skip_on_compatibility_flag("no_scheduling") + self.skip_on_compatibility_flag("no_scheduling_calendar_user_address_set") calendar_user_address_set = self.principal.calendar_user_address_set() me_a_participant = self.principal.get_vcal_address() @@ -967,6 +970,8 @@ def testCreateEvent(self): c = self._fixCalendar() existing_events = c.events() + existing_urls = {x.url for x in existing_events} + cleanse = lambda events: [x for x in events if x.url not in existing_urls] if not self.check_compatibility_flag("no_mkcalendar"): ## we're supposed to be working towards a brand new calendar @@ -976,13 +981,13 @@ def testCreateEvent(self): c.save_event(broken_ev1) # c.events() should give a full list of events - events = c.events() - assert len(events) == len(existing_events) + 1 + events = cleanse(c.events()) + assert len(events) == 1 # We should be able to access the calender through the URL c2 = self.caldav.calendar(url=c.url) - events2 = c2.events() - assert len(events2) == len(existing_events) + 1 + events2 = cleanse(c2.events()) + assert len(events2) == 1 assert events2[0].url == events[0].url if not self.check_compatibility_flag( @@ -993,7 +998,7 @@ def testCreateEvent(self): ## may break if we have multiple calendars with the same name if not self.check_compatibility_flag("no_delete_calendar"): assert c2.url == c.url - events2 = c2.events() + events2 = cleanse(c2.events()) assert len(events2) == 1 assert events2[0].url == events[0].url @@ -1014,6 +1019,7 @@ def testAlarm(self): ev = c.save_event( dtstart=datetime(2015, 10, 10, 8, 0, 0), summary="This is a test event", + uid="test1", dtend=datetime(2016, 10, 10, 9, 0, 0), alarm_trigger=timedelta(minutes=-15), alarm_action="AUDIO", @@ -1410,6 +1416,8 @@ def testSearchEvent(self): self.skip_on_compatibility_flag("no_search") c = self._fixCalendar() + num_existing = len(c.events()) + c.save_event(ev1) c.save_event(ev3) c.save_event(evr) @@ -1417,13 +1425,13 @@ def testSearchEvent(self): ## Search without any parameters should yield everything on calendar all_events = c.search() if self.check_compatibility_flag("search_needs_comptype"): - assert len(all_events) <= 3 + assert len(all_events) <= 3 + num_existing else: - assert len(all_events) == 3 + assert len(all_events) == 3 + num_existing ## Search with comp_class set to Event should yield all events on calendar all_events = c.search(comp_class=Event) - assert len(all_events) == 3 + assert len(all_events) == 3 + num_existing ## Search with todo flag set should yield no events try: @@ -1589,6 +1597,11 @@ def testSearchSortTodo(self): self.skip_on_compatibility_flag("no_todo") self.skip_on_compatibility_flag("no_search") c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) + pre_todos = c.todos() + pre_todo_uid_map = {x.icalendar_component["uid"] for x in pre_todos} + cleanse = lambda tasks: [ + x for x in tasks if x.icalendar_component["uid"] not in pre_todo_uid_map + ] t1 = c.save_todo( summary="1 task overdue", due=date(2022, 12, 12), @@ -1628,14 +1641,22 @@ def check_order(tasks, order): "test" + str(x) for x in order ] - all_tasks = c.search(todo=True, sort_keys=("uid",)) + all_tasks = cleanse(c.search(todo=True, sort_keys=("uid",))) check_order(all_tasks, (1, 2, 3, 4, 6)) - all_tasks = c.search(sort_keys=("summary",)) + all_tasks = cleanse(c.search(sort_keys=("summary",))) check_order(all_tasks, (1, 2, 3, 4, 5, 6)) - all_tasks = c.search( - sort_keys=("isnt_overdue", "categories", "dtstart", "priority", "status") + all_tasks = cleanse( + c.search( + sort_keys=( + "isnt_overdue", + "categories", + "dtstart", + "priority", + "status", + ) + ) ) ## This is difficult ... ## * 1 is the only one that is overdue, and False sorts before True, so 1 comes first @@ -1651,6 +1672,8 @@ def testSearchTodos(self): self.skip_on_compatibility_flag("no_search") c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) + pre_cnt = len(c.todos()) + t1 = c.save_todo(todo) t2 = c.save_todo(todo2) t3 = c.save_todo(todo3) @@ -1661,13 +1684,13 @@ def testSearchTodos(self): ## Search without any parameters should yield everything on calendar all_todos = c.search() if self.check_compatibility_flag("search_needs_comptype"): - assert len(all_todos) <= 6 + assert len(all_todos) <= 6 + pre_cnt else: - assert len(all_todos) == 6 + assert len(all_todos) == 6 + pre_cnt ## Search with comp_class set to Event should yield all events on calendar all_todos = c.search(comp_class=Event) - assert len(all_todos) == 0 + assert len(all_todos) == 0 + pre_cnt ## Search with todo flag set should yield all 6 tasks ## (Except, if the calendar server does not support is-not-defined very @@ -1675,20 +1698,20 @@ def testSearchTodos(self): ## https://gitlab.com/davical-project/davical/-/issues/281 ) all_todos = c.search(todo=True) if self.check_compatibility_flag("isnotdefined_not_working"): - assert len(all_todos) in (3, 6) + assert len(all_todos) - pre_cnt in (3, 6) else: - assert len(all_todos) == 6 + assert len(all_todos) == 6 + pre_cnt ## Search for misc text fields ## UID is a special case, supported by almost all servers some_todos = c.search(comp_class=Todo, uid="19970901T130000Z-123404@host.com") if not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) == 1 + assert len(some_todos) == 1 + pre_cnt ## class ... hm, all 6 example todos are 'CONFIDENTIAL' ... some_todos = c.search(comp_class=Todo, class_="CONFIDENTIAL") if not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) == 6 + assert len(some_todos) == 6 + pre_cnt ## category ## Too much copying of the examples ... @@ -1696,34 +1719,34 @@ def testSearchTodos(self): if not self.check_compatibility_flag( "category_search_yields_nothing" ) and not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) == 6 + assert len(some_todos) == 6 + pre_cnt some_todos = c.search(comp_class=Todo, category="finance") if not self.check_compatibility_flag( "category_search_yields_nothing" ) and not self.check_compatibility_flag("text_search_not_working"): if self.check_compatibility_flag("text_search_is_case_insensitive"): - assert len(some_todos) == 6 + assert len(some_todos) == 6 + pre_cnt else: - assert len(some_todos) == 0 + assert len(some_todos) == 0 + pre_cnt ## This is not a very useful search, and it's sort of a client side bug that we allow it at all. ## It will not match if categories field is set to "PERSONAL,ANNIVERSARY,SPECIAL OCCASION" ## It may not match since the above is to be considered equivalent to the raw data entered. some_todos = c.search(comp_class=Todo, category="FAMILY,FINANCE") if not self.check_compatibility_flag("text_search_not_working"): - assert len(some_todos) in (0, 6) + assert len(some_todos) - pre_cnt in (0, 6) ## TODO: We should consider to do client side filtering to ensure exact ## match only on components having MIL as a category (and not FAMILY) some_todos = c.search(comp_class=Todo, category="MIL") if self.check_compatibility_flag("text_search_is_exact_match_sometimes"): - assert len(some_todos) in (0, 6) + assert len(some_todos) - pre_cnt in (0, 6) elif self.check_compatibility_flag("text_search_is_exact_match_only"): - assert len(some_todos) == 0 + assert len(some_todos) - pre_cnt == 0 elif not self.check_compatibility_flag( "category_search_yields_nothing" ) and not self.check_compatibility_flag("text_search_not_working"): ## This is the correct thing, according to the letter of the RFC - assert len(some_todos) == 6 + assert len(some_todos) - pre_cnt == 6 ## completing events, and it should not show up anymore t3.complete() @@ -1731,11 +1754,11 @@ def testSearchTodos(self): t6.complete() some_todos = c.search(todo=True) - assert len(some_todos) == 3 + assert len(some_todos) == 3 + pre_cnt ## unless we specifically ask for completed tasks all_todos = c.search(todo=True, include_completed=True) - assert len(all_todos) == 6 + assert len(all_todos) == 6 + pre_cnt def testWrongPassword(self): if ( @@ -1909,6 +1932,8 @@ def testSetDue(self): child=[some_todo.id], ) + assert not parent.check_reverse_relations() + ## The above updates the some_todo object on the server side, but the local object is not ## updated ... until we reload it some_todo.load() @@ -2828,6 +2853,7 @@ def testRecurringDateWithExceptionSearch(self): c = self._fixCalendar() # evr2 is a bi-weekly event starting 2024-04-11 + ## It has an exception, edited summary for recurrence id 20240425T123000Z e = c.save_event(evr2) r = c.search( diff --git a/tests/test_caldav_unit.py b/tests/test_caldav_unit.py index c45d682c..48ff3a63 100755 --- a/tests/test_caldav_unit.py +++ b/tests/test_caldav_unit.py @@ -272,6 +272,20 @@ def testRequestNonAscii(self, mocked): assert response.status == 200 assert response.tree is None + def testLoadByMultiGet404(self): + xml = """ + + + /calendars/pythoncaldav-test/20010712T182145Z-123401%40example.com.ics + HTTP/1.1 404 Not Found + +""" + client = MockedDAVClient(xml) + calendar = Calendar(client, url="/calendar/issue491/") + object = Event(url="/calendar/issue491/notfound.ics", parent=calendar) + with pytest.raises(error.NotFoundError): + object.load_by_multiget() + @mock.patch("caldav.davclient.niquests.Session.request") def testRequestCustomHeaders(self, mocked): """