Skip to content

Commit 3d94b0b

Browse files
tobixenclaude
andcommitted
chore: Work on the compatibility_hints
New "features" added: * 'save-load.mutable' (replaces an old "compatibility flag", this has been observed unsupported for the old google API, long time ago) Compatibilities changed: * search.recurrences.expanded.exception was wrongly taggd as unsupported on many servers due to a bug in the checker script. Other: The old unique_calendar_ids "compatibility flag" seems not to be needed anymore by any servers I test, so it was removed (but possibly we will need it at some point in the future ...). AI-disclaimer: The unique_calendar_ids yanking job was hand-made, the other was AI-generated. The compatibility_hints.py is one of the places where AI-maintenance is considered OK. It's not considered to be a part of the sharp edge of the library, and some of the updates may be tedious. prompt: Create tests for a new feature checking if it's possible to overwrite a calendar event with new data. Should replace the old "overwrite" compatibility flag. (Perhaps this "incompatibility" was due to the caldav library not supporting etags and not updating from no sequence to SEQUENCE:1 ... i somehow doubt we'll find any servers not supporting this feature, but we should have the check anyway). load-save.mutable ? Update the compatibility_hints.py in /tmp/caldav-async-tests/ and update the tests there as well. prompt: On the main branch in the current directory, we get AssertionError: expectation is unsupported, observation is full for search.recurrences.expanded.exception from many servers now when running pytest -k compat in ~/caldav. Reproducable by doing cd ~/caldav ; status=good ; pytest tests/test_caldav.py::TestForServerRadicale::testCheckCompatibility || status=bad; cd ~/caldav-server-tester ; echo $status. Checking out tag v3.2.0, the test passes. There aren't that many differences between main and v3.2.0. Please investigate. followup-prompt: Throw it into the current async-test-infrastructure branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9b1d8d2 commit 3d94b0b

2 files changed

Lines changed: 30 additions & 70 deletions

File tree

caldav/compatibility_hints.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class FeatureSet:
7777
## TODO: in the future, templates for the principal URL, calendar URLs etc may also be added.
7878
}
7979
},
80+
"url": {
81+
"type": "client-hints",
82+
},
8083
"get-current-user-principal": {
8184
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the DAV standard. Possibly observed missing on mail.ru, DavMail gateway and it is possible to configure the support in some sabre-based servers",
8285
"links": ["https://datatracker.ietf.org/doc/html/rfc5397"],
@@ -177,6 +180,10 @@ class FeatureSet:
177180
"default": {"support": "full"},
178181
"links": ["https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.5"],
179182
},
183+
"save-load.mutable": {
184+
"description": "A saved calendar object resource can be modified and PUT back to the server; the server accepts the update and returns the modified data on the next GET/REPORT. When 'unsupported', the server treats calendar objects as immutable after initial creation (e.g. Google Calendar's legacy CalDAV API). Replaces the old 'no_overwrite' compatibility flag.",
185+
"default": {"support": "full"},
186+
},
180187
"search": {
181188
"description": "calendar MUST support searching for objects using the REPORT method, as specified in RFC4791, section 7",
182189
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-7"],
@@ -855,9 +862,6 @@ def dotted_feature_set_list(self, compact=False):
855862
'vtodo-cannot-be-uncompleted':
856863
"""If a VTODO object has been set with STATUS:COMPLETE, it's not possible to delete the COMPLTEDED attribute and change back to STATUS:IN-ACTION""",
857864

858-
'unique_calendar_ids':
859-
"""For every test, generate a new and unique calendar id""",
860-
861865
'sticky_events':
862866
"""Events should be deleted before the calendar is deleted, """
863867
"""and/or deleting a calendar may not have immediate effect""",
@@ -927,7 +931,7 @@ def dotted_feature_set_list(self, compact=False):
927931
"search.text.case-sensitive": {"support": "unsupported"},
928932
"search.recurrences.includes-implicit.todo.pending": {"support": "fragile", "behaviour": "inconsistent results between runs"},
929933
"search.recurrences.expanded.todo": {"support": "unsupported"},
930-
"search.recurrences.expanded.exception": {"support": "unsupported"},
934+
"search.recurrences.expanded.exception": {"support": "full"},
931935
"principal-search": {"support": "unsupported"},
932936
## this only applies for very simple installations
933937
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},
@@ -947,7 +951,6 @@ def dotted_feature_set_list(self, compact=False):
947951
## I'm surprised, I'm quite sure this was reported ungraceful earlier. Passed with caldav commit a98d50490b872e9b9d8e93e2e401c936ad193003, caldav server checker commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad 2026-02-15. The commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad was however development done on the wrong branch and has been force-pushed awway. It was again observed ungraceful at commits be26d42b1ca3ff3b4fd183761b4a9b024ce12b84 / 537a23b145487006bb987dee5ab9e00cdebb0492
948952
'search.comp-type.optional': {'support': 'ungraceful'},
949953
'search.recurrences.expanded.todo': {'support': 'unsupported'},
950-
'search.recurrences.expanded.exception': {'support': 'unsupported'}, ## TODO: verify
951954
"search.recurrences.includes-implicit.infinite-scope": False,
952955
'delete-calendar': {
953956
'support': 'fragile',
@@ -961,7 +964,6 @@ def dotted_feature_set_list(self, compact=False):
961964
'principal-search.by-name.self': {'support': 'unsupported'},
962965
'principal-search': {'support': 'ungraceful'},
963966
'search.time-range.open.start.duration': 'broken',
964-
#'old_flags': ['unique_calendar_ids'],
965967
## I'm surprised, I'm quite sure this was passing earlier. Caldav commit a98d50490b872e9b9d8e93e2e401c936ad193003, caldav server checker commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad
966968
'search.combined-is-logical-and': False,
967969
## Observed with Nextcloud 33: server delivers iTIP notification to the inbox AND
@@ -993,10 +995,11 @@ def dotted_feature_set_list(self, compact=False):
993995
zimbra = {
994996
'auto-connect.url': {'basepath': '/dav/'},
995997
'delete-calendar': {'support': 'fragile', 'behaviour': 'may move to trashbin instead of deleting immediately'},
996-
'save-load.get-by-url': {'support': 'fragile', 'behaviour': '404 most of the time - but sometimes 200. Weird, should be investigated more'},
998+
## This is a zimbra bug when creating calendars with a display
999+
## name. Now mitigated in the calendar creation code.
1000+
#'save-load.get-by-url': {'support': 'fragile', 'behaviour': '404 most of the time - but sometimes 200. Weird, should be investigated more'},
9971001
## Zimbra treats same-UID events across calendars as aliases of the same event
9981002
'save.duplicate-uid.cross-calendar': {'support': 'unsupported'},
999-
'search.recurrences.expanded.exception': {'support': 'unsupported'}, ## TODO: verify
10001003
'create-calendar.set-displayname': {'support': 'unsupported'},
10011004
'save-load.todo.mixed-calendar': {'support': 'unsupported'},
10021005
'save-load.todo.recurrences.count': {'support': 'unsupported'}, ## This is a new problem?
@@ -1060,7 +1063,6 @@ def dotted_feature_set_list(self, compact=False):
10601063
"search.text": False, ## sometimes ungraceful
10611064
"search.recurrences.includes-implicit": False,
10621065
"sync-token": { "support": "fragile" },
1063-
"search.recurrences.expanded.exception": False,
10641066
"search.recurrences.expanded.event": False,
10651067
"search.recurrences.expanded.todo": False,
10661068
'search.comp-type': {'support': 'broken', 'behaviour': 'Server returns everything when searching for events and nothing when searching for todos'},
@@ -1095,7 +1097,6 @@ def dotted_feature_set_list(self, compact=False):
10951097
'search.is-not-defined': {'support': 'fragile', 'behaviour': 'works for CLASS but not for CATEGORIES'},
10961098
'search.text.case-sensitive': {'support': 'unsupported'},
10971099
'search.time-range.alarm': {'support': 'unsupported'},
1098-
"search.recurrences.expanded.exception": False,
10991100
'old_flags': ['vtodo_datesearch_nodtstart_task_is_skipped'],
11001101
'test-calendar': {'cleanup-regime': 'wipe-calendar'},
11011102
'scheduling.schedule-tag': False,
@@ -1109,7 +1110,6 @@ def dotted_feature_set_list(self, compact=False):
11091110
"http.multiplexing": "fragile", ## ref https://github.com/python-caldav/caldav/issues/564
11101111
'search.comp-type.optional': {'support': 'ungraceful'},
11111112
'search.recurrences.expanded.todo': {'support': 'unsupported'},
1112-
'search.recurrences.expanded.exception': {'support': 'unsupported'},
11131113
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
11141114
"search.recurrences.includes-implicit.infinite-scope": False,
11151115
'save-load.journal.mixed-calendar': {'support': 'unsupported'},
@@ -1134,7 +1134,6 @@ def dotted_feature_set_list(self, compact=False):
11341134

11351135
cyrus = {
11361136
"search.comp-type.optional": {"support": "ungraceful"},
1137-
"search.recurrences.expanded.exception": {"support": "unsupported"},
11381137
"search.recurrences.includes-implicit.infinite-scope": False,
11391138
"search.time-range.alarm": {"support": "ungraceful"},
11401139
'principal-search': {'support': 'ungraceful'},
@@ -1156,7 +1155,6 @@ def dotted_feature_set_list(self, compact=False):
11561155

11571156
## See comments on https://github.com/python-caldav/caldav/issues/3
11581157
#icloud = [
1159-
# 'unique_calendar_ids',
11601158
# 'duplicate_in_other_calendar_with_same_uid_breaks',
11611159
# 'sticky_events',
11621160
# 'no_journal', ## it threw a 500 internal server error!
@@ -1175,7 +1173,6 @@ def dotted_feature_set_list(self, compact=False):
11751173
# into their calendar.
11761174
"scheduling.schedule-tag": False,
11771175
"search.comp-type.optional": { "support": "fragile" },
1178-
"search.recurrences.expanded.exception": { "support": "unsupported" },
11791176
"search.time-range.alarm": { "support": "unsupported" },
11801177
'sync-token': {'support': 'fragile'},
11811178
'principal-search': {'support': 'unsupported'},
@@ -1285,7 +1282,6 @@ def dotted_feature_set_list(self, compact=False):
12851282
"search.comp-type.optional": { "support": "ungraceful" },
12861283
"search.recurrences.expanded.todo": { "support": "unsupported" },
12871284
"search.recurrences.expanded.event": { "support": "fragile" },
1288-
"search.recurrences.expanded.exception": { "support": "unsupported" },
12891285
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
12901286
'principal-search': {'support': 'ungraceful'},
12911287
'freebusy-query': {'support': 'ungraceful'},
@@ -1327,7 +1323,6 @@ def dotted_feature_set_list(self, compact=False):
13271323
## foo ... "full" observed, 70938dc1cbb6a839978eee4315699746d38ee5f0/3cae24cf99da1702b851b5a74a9b88c8e5317dad, 2026-02-17
13281324
#'search.time-range.todo.old-dates': {'support': 'unsupported'},
13291325
'search.recurrences.expanded.todo': {'support': 'unsupported'},
1330-
'search.recurrences.expanded.exception': {'support': 'unsupported'},
13311326
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
13321327
'search.combined-is-logical-and': {'support': 'unsupported'},
13331328
'sync-token': {'support': 'ungraceful'},
@@ -1356,7 +1351,6 @@ def dotted_feature_set_list(self, compact=False):
13561351
# attendee inbox AND auto-schedules into their calendar.
13571352
"scheduling.schedule-tag": False,
13581353
"search.recurrences.expanded.todo": {"support": "unsupported"},
1359-
"search.recurrences.expanded.exception": {"support": "unsupported"},
13601354
"search.recurrences.includes-implicit.todo": {"support": "unsupported"},
13611355
"search.recurrences.includes-implicit.infinite-scope": False,
13621356
"principal-search.by-name.self": {"support": "unsupported"},
@@ -1467,7 +1461,6 @@ def dotted_feature_set_list(self, compact=False):
14671461
'search.time-range.event': {'support': 'fragile'},
14681462
## was: ungraceful - observed unsupported 2026-02 (for .old-dates)
14691463
'search.time-range.todo': {'support': 'fragile'},
1470-
'search.recurrences.expanded.exception': {'support': 'unsupported'},
14711464
'principal-search': {'support': 'ungraceful'},
14721465
'principal-search.by-name.self': {'support': 'ungraceful'},
14731466
'principal-search.list-all': {'support': 'ungraceful'},

tests/test_caldav.py

Lines changed: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,12 +1322,8 @@ def setup_method(self):
13221322
for flag in self.old_features:
13231323
assert flag in incompatibility_description
13241324

1325-
if self.check_compatibility_flag("unique_calendar_ids"):
1326-
self.testcal_id = "testcalendar-" + str(uuid.uuid4())
1327-
self.testcal_id2 = "testcalendar-" + str(uuid.uuid4())
1328-
else:
1329-
self.testcal_id = "pythoncaldav-test"
1330-
self.testcal_id2 = "pythoncaldav-test2"
1325+
self.testcal_id = "pythoncaldav-test"
1326+
self.testcal_id2 = "pythoncaldav-test2"
13311327

13321328
foo = self.is_supported("rate-limit", dict)
13331329
if foo.get("enable"):
@@ -1408,8 +1404,6 @@ def _cleanup(self, mode=None):
14081404
pass
14091405
else:
14101406
cal.delete()
1411-
if self.check_compatibility_flag("unique_calendar_ids") and mode == "pre":
1412-
a = self._teardownCalendar(name="Yep")
14131407
for calid in (self.testcal_id, self.testcal_id2, self.testcal_id + "-tasks"):
14141408
self._teardownCalendar(cal_id=calid)
14151409
if self.cleanup_regime == "thorough":
@@ -1471,10 +1465,7 @@ def _fixCalendar_(self, **kwargs):
14711465

14721466
# Pre-processing: set up defaults for name and cal_id
14731467
if "name" not in kwargs:
1474-
if not self.check_compatibility_flag("unique_calendar_ids") and self.cleanup_regime in (
1475-
"light",
1476-
"pre",
1477-
):
1468+
if self.cleanup_regime in ("light", "pre"):
14781469
self._teardownCalendar(cal_id=self.testcal_id)
14791470
if not self.is_supported("create-calendar.set-displayname"):
14801471
kwargs["name"] = None
@@ -2103,7 +2094,7 @@ def testObjectBySyncToken(self):
21032094
assert len(list(my_changed_objects)) == 0
21042095

21052096
## I was unable to run the rest of the tests towards Google using their legacy caldav API
2106-
self.skip_on_compatibility_flag("no_overwrite")
2097+
self.skip_unless_support("save-load.mutable")
21072098

21082099
## MODIFYING an object
21092100
if is_time_based:
@@ -2234,7 +2225,7 @@ def testSync(self):
22342225
time.sleep(1)
22352226

22362227
## I was unable to run the rest of the tests towards Google using their legacy caldav API
2237-
self.skip_on_compatibility_flag("no_overwrite")
2228+
self.skip_unless_support("save-load.mutable")
22382229

22392230
## MODIFYING an object
22402231
obj.icalendar_instance.subcomponents[0]["SUMMARY"] = "foobar"
@@ -2291,10 +2282,7 @@ def testSync(self):
22912282
def testLoadEvent(self):
22922283
self.skip_unless_support("save-load.event")
22932284
self.skip_unless_support("create-calendar")
2294-
if not self.check_compatibility_flag("unique_calendar_ids") and self.cleanup_regime in (
2295-
"light",
2296-
"pre",
2297-
):
2285+
if self.cleanup_regime in ("light", "pre"):
22982286
self._teardownCalendar(cal_id=self.testcal_id)
22992287
self._teardownCalendar(cal_id=self.testcal_id2)
23002288
c1 = self._fixCalendar(name="Yep", cal_id=self.testcal_id)
@@ -2307,20 +2295,14 @@ def testLoadEvent(self):
23072295
if not self.check_compatibility_flag("event_by_url_is_broken"):
23082296
assert e1.url == e1_.url
23092297
e1.load()
2310-
if (
2311-
not self.check_compatibility_flag("unique_calendar_ids")
2312-
and self.cleanup_regime == "post"
2313-
):
2298+
if self.cleanup_regime == "post":
23142299
self._teardownCalendar(cal_id=self.testcal_id)
23152300
self._teardownCalendar(cal_id=self.testcal_id2)
23162301

23172302
def testCopyEvent(self):
23182303
self.skip_unless_support("save-load.event")
23192304
self.skip_unless_support("create-calendar")
2320-
if not self.check_compatibility_flag("unique_calendar_ids") and self.cleanup_regime in (
2321-
"light",
2322-
"pre",
2323-
):
2305+
if self.cleanup_regime in ("light", "pre"):
23242306
self._teardownCalendar(cal_id=self.testcal_id)
23252307
self._teardownCalendar(cal_id=self.testcal_id2)
23262308

@@ -2366,10 +2348,7 @@ def testCopyEvent(self):
23662348
else:
23672349
assert len(c1.get_events()) == 2
23682350

2369-
if (
2370-
not self.check_compatibility_flag("unique_calendar_ids")
2371-
and self.cleanup_regime == "post"
2372-
):
2351+
if self.cleanup_regime == "post":
23732352
self._teardownCalendar(cal_id=self.testcal_id)
23742353
self._teardownCalendar(cal_id=self.testcal_id2)
23752354

@@ -3496,10 +3475,7 @@ def testUtf8Event(self):
34963475
# TODO: split up in creating a calendar with non-ascii name
34973476
# and an event with non-ascii description
34983477
self.skip_unless_support("create-calendar")
3499-
if not self.check_compatibility_flag("unique_calendar_ids") and self.cleanup_regime in (
3500-
"light",
3501-
"pre",
3502-
):
3478+
if self.cleanup_regime in ("light", "pre"):
35033479
self._teardownCalendar(cal_id=self.testcal_id)
35043480

35053481
c = self._fixCalendar(name="Yølp", cal_id=self.testcal_id)
@@ -3519,19 +3495,13 @@ def testUtf8Event(self):
35193495
if "zimbra" not in str(c.url):
35203496
assert len(events) == 1
35213497

3522-
if (
3523-
not self.check_compatibility_flag("unique_calendar_ids")
3524-
and self.cleanup_regime == "post"
3525-
):
3498+
if self.cleanup_regime == "post":
35263499
self._teardownCalendar(cal_id=self.testcal_id)
35273500

35283501
def testUnicodeEvent(self):
35293502
self.skip_unless_support("save-load.event")
35303503
self.skip_unless_support("create-calendar")
3531-
if not self.check_compatibility_flag("unique_calendar_ids") and self.cleanup_regime in (
3532-
"light",
3533-
"pre",
3534-
):
3504+
if self.cleanup_regime in ("light", "pre"):
35353505
self._teardownCalendar(cal_id=self.testcal_id)
35363506
c = self._fixCalendar(name="Yølp", cal_id=self.testcal_id)
35373507

@@ -3565,18 +3535,15 @@ def testSetCalendarProperties(self):
35653535

35663536
# Creating a new calendar with different ID but with existing name
35673537
# TODO: why do we do this?
3568-
if not self.check_compatibility_flag("unique_calendar_ids") and self.cleanup_regime in (
3569-
"light",
3570-
"pre",
3571-
):
3538+
# TODO: we're doing this all over the placee, it should be consolidated
3539+
# TODO: it should be in the test setup/teardown
3540+
if self.cleanup_regime in ("light", "pre"):
35723541
self._teardownCalendar(cal_id=self.testcal_id2)
35733542
cc = self._fixCalendar(name="Yep", cal_id=self.testcal_id2)
35743543
try:
35753544
cc.delete()
35763545
except error.DeleteError:
3577-
if not self.is_supported("delete-calendar") or self.check_compatibility_flag(
3578-
"unique_calendar_ids"
3579-
):
3546+
if not self.is_supported("delete-calendar"):
35803547
raise
35813548

35823549
c.set_properties(
@@ -3696,7 +3663,7 @@ def testCreateOverwriteDeleteEvent(self):
36963663

36973664
## add same event again. As it has same uid, it should be overwritten
36983665
## (but some calendars may throw a "409 Conflict")
3699-
if not self.check_compatibility_flag("no_overwrite"):
3666+
if self.is_supported("save-load.mutable"):
37003667
e2 = c.add_event(ev1)
37013668
if todo_ok:
37023669
t2 = c.add_todo(todo)
@@ -3742,7 +3709,7 @@ def testCreateOverwriteDeleteEvent(self):
37423709
# Verify that we can't look it up, both by URL and by ID
37433710
with pytest.raises(self._notFound()):
37443711
c.event_by_url(e1.url)
3745-
if not self.check_compatibility_flag("no_overwrite"):
3712+
if self.is_supported("save-load.mutable"):
37463713
with pytest.raises(self._notFound()):
37473714
c.event_by_url(e2.url)
37483715
if not self.check_compatibility_flag("event_by_url_is_broken"):
@@ -3799,7 +3766,7 @@ def testDateSearchAndFreeBusy(self):
37993766
## (But events should not be immutable! One should be able to change an event, push the changes
38003767
## out to all participants and all copies of the calendar, and let everyone know that it's a
38013768
## changed event and not a cancellation and a new event).
3802-
self.skip_on_compatibility_flag("no_overwrite")
3769+
self.skip_unless_support("save-load.mutable")
38033770

38043771
# ev2 is same UID, but one year ahead.
38053772
# The timestamp should change.

0 commit comments

Comments
 (0)