Skip to content

Commit 4b6db95

Browse files
tobixenclaude
andcommitted
Consolidate _fixCalendar_ into get_or_create_test_calendar
Make fixture_helpers.py the single source of truth for the core create-or-find-calendar logic. _fixCalendar_ now delegates to the sync get_or_create_test_calendar, keeping only test-infrastructure concerns (caching, cleanup regime, cal_id defaults). - Add sync get_or_create_test_calendar with consolidated logic - Rename async version to aget_or_create_test_calendar - Extract shared helpers: _build_make_calendar_kwargs, _filter_calendars_by_component_set, _find_test_calendar - Update test_async_integration.py imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d8c107 commit 4b6db95

3 files changed

Lines changed: 225 additions & 80 deletions

File tree

tests/fixture_helpers.py

Lines changed: 181 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,111 @@ async def _maybe_await(result: Any) -> Any:
1616
return result
1717

1818

19-
async def get_or_create_test_calendar(
19+
def _build_make_calendar_kwargs(
20+
calendar_name: str | None,
21+
cal_id: str | None,
22+
supported_calendar_component_set: list[str] | None,
23+
) -> dict[str, Any]:
24+
"""Build kwargs dict for principal.make_calendar()."""
25+
kwargs: dict[str, Any] = {}
26+
if calendar_name is not None:
27+
kwargs["name"] = calendar_name
28+
if cal_id:
29+
kwargs["cal_id"] = cal_id
30+
if supported_calendar_component_set:
31+
kwargs["supported_calendar_component_set"] = supported_calendar_component_set
32+
return kwargs
33+
34+
35+
def _filter_calendars_by_component_set(
36+
calendars: list[Any],
37+
supported_calendar_component_set: list[str],
38+
get_properties_fn: Any = None,
39+
) -> list[Any] | None:
40+
"""Filter calendars by supported component set.
41+
42+
Uses property lookup first, then URL-based heuristics as fallback.
43+
Returns None if no matching calendars found (caller should skip test).
44+
45+
Args:
46+
calendars: List of calendar objects to filter
47+
supported_calendar_component_set: Required component types
48+
get_properties_fn: Callable that takes (calendar, keys) and returns
49+
properties dict. If None, uses calendar.get_properties() directly.
50+
"""
51+
comp_set_key = "{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set"
52+
53+
matching_calendars = []
54+
for c in calendars:
55+
try:
56+
if get_properties_fn:
57+
props = get_properties_fn(c, [comp_set_key])
58+
else:
59+
props = c.get_properties([comp_set_key])
60+
cal_components = props.get(comp_set_key, [])
61+
if cal_components and all(
62+
comp in cal_components for comp in supported_calendar_component_set
63+
):
64+
matching_calendars.append(c)
65+
except Exception:
66+
pass
67+
68+
# Fallback: URL/name pattern heuristics (some servers like Zimbra don't
69+
# return the supported-calendar-component-set property)
70+
if not matching_calendars:
71+
for c in calendars:
72+
url_path = str(c.url).lower()
73+
if "VTODO" in supported_calendar_component_set:
74+
if "/tasks/" in url_path or "_tasks/" in url_path:
75+
matching_calendars.append(c)
76+
elif "VJOURNAL" in supported_calendar_component_set:
77+
if "/journal" in url_path or "_journal" in url_path:
78+
matching_calendars.append(c)
79+
80+
return matching_calendars or None
81+
82+
83+
def _find_test_calendar(
84+
calendars: list[Any],
85+
get_properties_fn: Any = None,
86+
) -> Any:
87+
"""Find a dedicated test calendar by display name, or return first calendar.
88+
89+
Args:
90+
calendars: List of calendar objects to search
91+
get_properties_fn: Callable that takes (calendar, keys) and returns
92+
properties dict. If None, uses calendar.get_properties() directly.
93+
"""
94+
for c in calendars:
95+
try:
96+
if get_properties_fn:
97+
props = get_properties_fn(c, [])
98+
else:
99+
props = c.get_properties([])
100+
display_name = props.get("{DAV:}displayname", "")
101+
if "pythoncaldav-test" in str(display_name):
102+
return c
103+
except Exception:
104+
pass
105+
return calendars[0] if calendars else None
106+
107+
108+
def get_or_create_test_calendar(
20109
client: Any,
21110
principal: Any,
22-
calendar_name: str = "pythoncaldav-test",
111+
calendar_name: str | None = "pythoncaldav-test",
23112
cal_id: str | None = None,
24113
supported_calendar_component_set: list[str] | None = None,
25114
) -> tuple[Any, bool]:
26115
"""
27-
Get or create a test calendar, with fallback to existing calendars.
28-
29-
This implements the same logic as the sync _fixCalendar_ method,
30-
providing safeguards against accidentally overwriting user data.
116+
Get or create a test calendar (sync version), with fallback to existing calendars.
31117
32118
Args:
33-
client: The DAV client (sync or async)
119+
client: The DAV client
34120
principal: The principal object (or None to skip principal-based creation)
35-
calendar_name: Name for the test calendar
121+
calendar_name: Name for the test calendar, or None to skip setting name
36122
cal_id: Optional calendar ID
37123
supported_calendar_component_set: Component types this calendar should support
38-
(e.g., ["VTODO"] for task lists, ["VEVENT"] for event calendars).
39-
Important for servers like Zimbra that don't support mixed calendars.
40124
41125
Returns:
42126
Tuple of (calendar, was_created) where was_created indicates if
@@ -54,18 +138,90 @@ async def get_or_create_test_calendar(
54138
supports_create = client.features.is_supported("create-calendar")
55139

56140
if supports_create and principal is not None:
57-
# Try to create a new calendar
58141
try:
59-
kwargs: dict[str, Any] = {"name": calendar_name}
142+
kwargs = _build_make_calendar_kwargs(
143+
calendar_name, cal_id, supported_calendar_component_set
144+
)
145+
calendar = principal.make_calendar(**kwargs)
146+
created = True
147+
except (error.MkcalendarError, error.AuthorizationError, error.NotFoundError):
148+
# Creation failed - try to get by cal_id if available
60149
if cal_id:
61-
kwargs["cal_id"] = cal_id
150+
try:
151+
calendar = principal.calendar(cal_id=cal_id)
152+
except Exception:
153+
pass
154+
155+
if calendar is None:
156+
# Fall back to finding an existing calendar
157+
calendars = None
158+
159+
if principal is not None:
160+
try:
161+
calendars = principal.get_calendars()
162+
except (error.NotFoundError, error.AuthorizationError):
163+
pass
164+
165+
if calendars:
62166
if supported_calendar_component_set:
63-
kwargs["supported_calendar_component_set"] = supported_calendar_component_set
167+
filtered = _filter_calendars_by_component_set(
168+
calendars, supported_calendar_component_set
169+
)
170+
if filtered is None:
171+
return None, False
172+
calendars = filtered
173+
174+
calendar = _find_test_calendar(calendars)
175+
176+
return calendar, created
177+
178+
179+
async def aget_or_create_test_calendar(
180+
client: Any,
181+
principal: Any,
182+
calendar_name: str | None = "pythoncaldav-test",
183+
cal_id: str | None = None,
184+
supported_calendar_component_set: list[str] | None = None,
185+
) -> tuple[Any, bool]:
186+
"""
187+
Get or create a test calendar (async version), with fallback to existing calendars.
188+
189+
Args:
190+
client: The DAV client (sync or async)
191+
principal: The principal object (or None to skip principal-based creation)
192+
calendar_name: Name for the test calendar, or None to skip setting name
193+
cal_id: Optional calendar ID
194+
supported_calendar_component_set: Component types this calendar should support
195+
196+
Returns:
197+
Tuple of (calendar, was_created) where was_created indicates if
198+
we created the calendar (and should clean it up) or are using
199+
an existing one.
200+
"""
201+
from caldav.lib import error
202+
203+
calendar = None
204+
created = False
205+
206+
# Check if server supports calendar creation via features
207+
supports_create = True
208+
if hasattr(client, "features") and client.features:
209+
supports_create = client.features.is_supported("create-calendar")
210+
211+
if supports_create and principal is not None:
212+
try:
213+
kwargs = _build_make_calendar_kwargs(
214+
calendar_name, cal_id, supported_calendar_component_set
215+
)
64216
calendar = await _maybe_await(principal.make_calendar(**kwargs))
65217
created = True
66218
except (error.MkcalendarError, error.AuthorizationError, error.NotFoundError):
67-
# Creation failed - fall back to finding existing calendar
68-
pass
219+
# Creation failed - try to get by cal_id if available
220+
if cal_id:
221+
try:
222+
calendar = await _maybe_await(principal.calendar(cal_id=cal_id))
223+
except Exception:
224+
pass
69225

70226
if calendar is None:
71227
# Fall back to finding an existing calendar
@@ -78,41 +234,36 @@ async def get_or_create_test_calendar(
78234
pass
79235

80236
if calendars:
81-
# Property key for supported component set
82-
comp_set_key = "{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set"
83-
84-
# If we need specific component support, filter calendars
85237
if supported_calendar_component_set:
86-
matching_calendars = []
238+
239+
async def async_get_props(cal: Any, keys: list[str]) -> dict:
240+
return await _maybe_await(cal.get_properties(keys))
241+
242+
# Can't use the sync helper with async get_properties,
243+
# so inline the filtering
244+
comp_set_key = "{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set"
245+
matching_calendars: list[Any] = []
87246
for c in calendars:
88247
try:
89-
props = await _maybe_await(c.get_properties([comp_set_key]))
248+
props = await async_get_props(c, [comp_set_key])
90249
cal_components = props.get(comp_set_key, [])
91-
# Check if calendar supports all required components
92250
if cal_components and all(
93251
comp in cal_components for comp in supported_calendar_component_set
94252
):
95253
matching_calendars.append(c)
96254
except Exception:
97255
pass
98256

99-
# If no matching calendars found by component set, try heuristics
100-
# based on URL/name patterns (some servers like Zimbra don't return
101-
# the supported-calendar-component-set property)
102257
if not matching_calendars:
103258
for c in calendars:
104259
url_path = str(c.url).lower()
105-
# For VTODO, look for task-related calendars
106260
if "VTODO" in supported_calendar_component_set:
107261
if "/tasks/" in url_path or "_tasks/" in url_path:
108262
matching_calendars.append(c)
109-
# For VJOURNAL, look for journal-related calendars
110263
elif "VJOURNAL" in supported_calendar_component_set:
111264
if "/journal" in url_path or "_journal" in url_path:
112265
matching_calendars.append(c)
113266

114-
# Only use matching calendars - if none found, return None
115-
# (caller should skip the test)
116267
if not matching_calendars:
117268
return None, False
118269
calendars = matching_calendars

tests/test_async_integration.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ async def async_calendar(self, async_client: Any) -> Any:
214214
from caldav.aio import AsyncPrincipal
215215
from caldav.lib.error import AuthorizationError, NotFoundError
216216

217-
from .fixture_helpers import get_or_create_test_calendar
217+
from .fixture_helpers import aget_or_create_test_calendar
218218

219219
calendar_name = f"async-test-{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
220220

@@ -226,7 +226,7 @@ async def async_calendar(self, async_client: Any) -> Any:
226226
pass
227227

228228
# Use shared helper for calendar setup
229-
calendar, created = await get_or_create_test_calendar(
229+
calendar, created = await aget_or_create_test_calendar(
230230
async_client, principal, calendar_name=calendar_name
231231
)
232232

@@ -252,7 +252,7 @@ async def async_task_list(self, async_client: Any) -> Any:
252252
from caldav.aio import AsyncPrincipal
253253
from caldav.lib.error import AuthorizationError, NotFoundError
254254

255-
from .fixture_helpers import get_or_create_test_calendar
255+
from .fixture_helpers import aget_or_create_test_calendar
256256

257257
# Check if server supports mixed calendars
258258
supports_mixed = True
@@ -271,7 +271,7 @@ async def async_task_list(self, async_client: Any) -> Any:
271271
# For servers without mixed calendar support, create a dedicated task list
272272
component_set = ["VTODO"] if not supports_mixed else None
273273

274-
calendar, created = await get_or_create_test_calendar(
274+
calendar, created = await aget_or_create_test_calendar(
275275
async_client,
276276
principal,
277277
calendar_name=calendar_name,

0 commit comments

Comments
 (0)