Skip to content

Commit 6863ed1

Browse files
tobixenclaude
andcommitted
fix: add async support to get_object_by_uid and friends (issue #642)
get_object_by_uid() called self.search() which, for async clients, returns a coroutine rather than a list. The method then tried to iterate over the coroutine, raising TypeError. Fix by adding _async_get_object_by_uid() that awaits the search, and dispatching to it when is_async_client is True, following the same pattern used by other async-capable methods in the class. The get_todo_by_uid(), get_event_by_uid(), get_journal_by_uid() and the deprecated *_by_uid() aliases are fixed transitively because they all call get_object_by_uid(). Fixes #642 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 888ff8d commit 6863ed1

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

caldav/collection.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,8 @@ def get_object_by_uid(
14861486
Returns:
14871487
CalendarObjectResource (Event, Todo, or Journal)
14881488
"""
1489+
if self.is_async_client:
1490+
return self._async_get_object_by_uid(uid, comp_filter, comp_class)
14891491
## Use self.search() rather than CalDAVSearcher directly, so that any
14901492
## monkey-patching of Calendar.search (e.g. the search-cache delay for
14911493
## servers with lazy search indexes) is respected. This mirrors the
@@ -1504,6 +1506,23 @@ def get_object_by_uid(
15041506
error.assert_(len(items_found) == 1)
15051507
return items_found[0]
15061508

1509+
async def _async_get_object_by_uid(
1510+
self,
1511+
uid: str,
1512+
comp_filter: cdav.CompFilter | None = None,
1513+
comp_class: Optional["CalendarObjectResource"] = None,
1514+
) -> "Event":
1515+
"""Async helper for get_object_by_uid()."""
1516+
items_found = await self.search(
1517+
uid=uid, comp_class=comp_class, xml=comp_filter, post_filter=True, _hacks="insist"
1518+
)
1519+
items_found = [o for o in items_found if o.id == uid]
1520+
1521+
if not items_found:
1522+
raise error.NotFoundError("%s not found on server" % uid)
1523+
error.assert_(len(items_found) == 1)
1524+
return items_found[0]
1525+
15071526
def get_todo_by_uid(self, uid: str) -> "CalendarObjectResource":
15081527
"""
15091528
Get a task/todo from the calendar by UID.

tests/test_caldav_unit.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,6 +1837,71 @@ def testGetObjectByUidExactMatch(self):
18371837
calendar.get_object_by_uid("20010712T182145Z-123401@example.com-nope")
18381838

18391839

1840+
class TestAsyncGetObjectByUid:
1841+
"""Tests for async support in get_object_by_uid and friends (issue #642)."""
1842+
1843+
def _make_multistatus(self, ical_data, href="/calendar/ev1.ics"):
1844+
return f"""<d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
1845+
<d:response>
1846+
<d:href>{href}</d:href>
1847+
<d:propstat>
1848+
<d:prop>
1849+
<cal:calendar-data>{ical_data}</cal:calendar-data>
1850+
</d:prop>
1851+
<d:status>HTTP/1.1 200 OK</d:status>
1852+
</d:propstat>
1853+
</d:response>
1854+
</d:multistatus>"""
1855+
1856+
def test_get_object_by_uid_returns_coroutine_for_async_client(self):
1857+
"""get_object_by_uid() must return a coroutine (not a result) when the
1858+
client is an AsyncDAVClient, so that the caller can await it."""
1859+
import asyncio
1860+
1861+
from caldav.async_davclient import AsyncDAVClient
1862+
1863+
xml_response = self._make_multistatus(ev1)
1864+
client = MockedDAVClient(xml_response)
1865+
# Pretend the client is async by patching the type name
1866+
client.__class__ = type(
1867+
"AsyncDAVClient", (MockedDAVClient,), {"__module__": AsyncDAVClient.__module__}
1868+
)
1869+
calendar = Calendar(client, url="/calendar/")
1870+
assert calendar.is_async_client
1871+
uid = "20010712T182145Z-123401@example.com"
1872+
result = calendar.get_object_by_uid(uid)
1873+
# Must be a coroutine, not an Event/CalendarObjectResource
1874+
assert asyncio.iscoroutine(result), (
1875+
f"expected coroutine from async client, got {type(result)}"
1876+
)
1877+
result.close() # clean up to avoid ResourceWarning
1878+
1879+
def test_get_object_by_uid_async_returns_correct_object(self):
1880+
"""Awaiting the coroutine from get_object_by_uid() returns the right object."""
1881+
import asyncio
1882+
1883+
from caldav.async_davclient import AsyncDAVClient
1884+
1885+
client = MockedDAVClient("")
1886+
client.__class__ = type(
1887+
"AsyncDAVClient", (MockedDAVClient,), {"__module__": AsyncDAVClient.__module__}
1888+
)
1889+
calendar = Calendar(client, url="/calendar/")
1890+
uid = "20010712T182145Z-123401@example.com"
1891+
1892+
# Build a real Event object that the mocked search will return
1893+
fake_event = Event(client=client, url="/calendar/ev1.ics", data=ev1, parent=calendar)
1894+
1895+
# Mock calendar.search as an async function returning our fake event
1896+
async def fake_search(**kwargs):
1897+
return [fake_event]
1898+
1899+
calendar.search = fake_search
1900+
1901+
obj = asyncio.run(calendar.get_object_by_uid(uid))
1902+
assert obj.id == uid
1903+
1904+
18401905
class TestRateLimitHelpers:
18411906
"""Unit tests for the shared rate-limit helper functions in caldav.lib.error."""
18421907

0 commit comments

Comments
 (0)