Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions caldav/async_davclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ async def sync_collection(
url: str | None = None,
sync_token: str | None = None,
props: list[str] | None = None,
depth: int = 1,
depth: int = 0,
headers: Mapping[str, str] | None = None,
) -> DAVResponse:
"""
Expand All @@ -852,7 +852,7 @@ async def sync_collection(
url: Target calendar URL (defaults to self.url).
sync_token: Previous sync token (None for initial sync).
props: Properties to include in response.
depth: Search depth.
depth: Depth header; RFC 6578 defines sync-collection for 0 only.
headers: Additional headers.

Returns:
Expand Down
17 changes: 10 additions & 7 deletions caldav/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1796,23 +1796,26 @@ def _href_to_url(self, href: str) -> "URL | str":
return url

def _request_report_build_resultlist(
self, xml, comp_class=None, props=None, no_calendardata=False
self, xml, comp_class=None, props=None, no_calendardata=False, depth=1
) -> "tuple[Any, list[CalendarObjectResource]] | Coroutine[Any, Any, tuple[Any, list[CalendarObjectResource]]]":
"""
Takes some input XML, does a report query on a calendar object
and returns the resource objects found.

``depth`` is 1 for calendar-query; sync-collection needs 0
(RFC 6578 section 3.2).

For async clients, returns a coroutine that must be awaited.
"""
props_ = [cdav.CalendarData()] if props is None else [cdav.CalendarData()] + props
if self.is_async_client:
return self._async_request_report_build_resultlist(xml, comp_class, props_)
response = self._query(xml, 1, "report")
return self._async_request_report_build_resultlist(xml, comp_class, props_, depth)
response = self._query(xml, depth, "report")
return self._post_request_report_build_resultlist(response, comp_class, props_)

async def _async_request_report_build_resultlist(self, xml, comp_class, props_):
async def _async_request_report_build_resultlist(self, xml, comp_class, props_, depth=1):
"""Async implementation of _request_report_build_resultlist."""
response = await self._query(xml, 1, "report")
response = await self._query(xml, depth, "report")
return self._post_request_report_build_resultlist(response, comp_class, props_)

def _populate_searcher(self, my_searcher, searchargs: dict, sort_reverse: bool) -> None:
Expand Down Expand Up @@ -2541,7 +2544,7 @@ def get_objects_by_sync_token(
sync_token=token, props=["getetag"]
)
(response, objects) = self._request_report_build_resultlist(
root, props=[dav.GetEtag()], no_calendardata=True
root, props=[dav.GetEtag()], no_calendardata=True, depth=0
)
(token, truncated, again) = self._absorb_sync_page(
response, objects, collected, token, deleted
Expand Down Expand Up @@ -2642,7 +2645,7 @@ async def _async_get_objects_by_sync_token(
sync_token=token, props=["getetag"]
)
(response, objects) = await self._request_report_build_resultlist(
root, props=[dav.GetEtag()], no_calendardata=True
root, props=[dav.GetEtag()], no_calendardata=True, depth=0
)
(token, truncated, again) = self._absorb_sync_page(
response, objects, collected, token, deleted
Expand Down
1 change: 1 addition & 0 deletions changes/+sync-collection-depth-0.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The sync-collection REPORT (`Calendar.get_objects_by_sync_token()`, `AsyncDAVClient.sync_collection()`) is now sent with `Depth: 0`, as [RFC 6578 section 3.2](https://datatracker.ietf.org/doc/html/rfc6578#section-3.2) requires, instead of `Depth: 1`; the `depth` parameter of `AsyncDAVClient.sync_collection()` now defaults to 0. None of the servers in the test suite behaves differently, but a strict server may reject the old request with a 400.
3 changes: 3 additions & 0 deletions tests/test_async_davclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,8 @@ async def test_follows_the_token_until_complete(self) -> None:

assert client.report.await_count == 2
bodies = [to_normal_str(c.args[1]) for c in client.report.call_args_list]
## RFC 6578 section 3.2: sync-collection is only defined for Depth: 0
assert [c.args[2] for c in client.report.call_args_list] == [0, 0]
assert "tok-1" in bodies[0]
assert "tok-2" in bodies[1]
assert result.sync_token == "tok-3"
Expand Down Expand Up @@ -2434,3 +2436,4 @@ async def test_sync_collection_reports_truncation(self) -> None:
assert response.sync_truncated is True
assert response.sync_token == "tok-2"
assert [r.href for r in response.results] == ["/dav/cal/a.ics"]
assert client.request.call_args.args[3]["Depth"] == "0"
7 changes: 7 additions & 0 deletions tests/test_caldav_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4382,10 +4382,12 @@ class CapturingClient(MockedDAVClient):
def __init__(self):
super().__init__("<multistatus xmlns='DAV:'/>")
self.report_bodies = []
self.report_depths = []

def request(self, url=None, method="GET", body=None, headers=None, *args, **kwargs):
if method == "REPORT" and body:
self.report_bodies.append(body)
self.report_depths.append((headers or {}).get("Depth"))
return super().request(url, method, body, headers, *args, **kwargs)

client = CapturingClient()
Expand All @@ -4402,6 +4404,9 @@ def request(self, url=None, method="GET", body=None, headers=None, *args, **kwar
# Must NOT contain bare date strings without time component
assert "20260501Z" not in body
assert "20260601Z" not in body
## calendar-query is a Depth: 1 REPORT (RFC 4791 section 7.8),
## unlike sync-collection, which shares the helper
assert client.report_depths[0] == "1"

def test_searcher_coerces_date_to_datetime(self):
"""calendar.searcher(start=date(...)) must store datetime on the searcher,
Expand Down Expand Up @@ -7596,6 +7601,8 @@ def test_untruncated_reply_is_a_single_request(self):
client, calendar = self._calendar(sync_page({"a.ics": '"a1"'}, "tok-2", truncated=False))
result = calendar.get_objects_by_sync_token("tok-1")
assert client.report.call_count == 1
## RFC 6578 section 3.2: sync-collection is only defined for Depth: 0
assert client.report.call_args.args[2] == 0
assert result.truncated is False
assert result.sync_token == "tok-2"
assert len(result) == 1
Expand Down
Loading