Skip to content

Commit 6878921

Browse files
committed
work in progress
1 parent 3778fd4 commit 6878921

4 files changed

Lines changed: 239 additions & 122 deletions

File tree

caldav/compatibility_hints.py

Lines changed: 161 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,159 @@
11
# fmt: off
2-
"""This text was updated 2025-05-17. The plan is to reorganize this
3-
file a lot over the next few months, see
4-
https://github.com/python-caldav/caldav/issues/402
5-
2+
"""
63
This file serves as a database of different compatibility issues we've
74
encountered while working on the caldav library, and descriptions on
85
how the well-known servers behave.
6+
"""
97

10-
As for now, this is a list of binary "flags" that could be turned on
11-
or off. My experience is that there are often neuances, so the
12-
compatibility matrix will be changed from being a list of flags to a
13-
key=value store in the near future (at least, that's the plan).
8+
## NEW STYLE
9+
10+
## (we're gradually moving stuff from the good old
11+
## "incompatibility_description" below over to
12+
## "compatibility_features")
13+
14+
class FeatureSet:
15+
"""Work in progress ... TODO: write a better class description.
16+
17+
This class holds the description of different behaviour observed in
18+
a class constant.
19+
20+
An object of this class describes the feature set of a server.
21+
22+
TODO: use enums?
23+
type -> "client-feature", "server-peculiarity", "server-feature" (last is default)
24+
support -> "supported" (default), "unsupported", "fragile", "broken", "ungraceful"
25+
"""
26+
FEATURES = {
27+
"rate-limit": {
28+
"type": "client-feature",
29+
"description": "client (or test code) must not send requests too fast",
30+
"extra_keys": {
31+
"interval": "Rate limiting window, in seconds",
32+
"count": "Max number of requests to send within the interval",
33+
}},
34+
"search-cache": {
35+
"type": "server-peculiarity",
36+
"description": "The server delivers search results from a cache which is not immediately updated when an object is changed. Hence recent changes may not be reflected in search results",
37+
"extra_keys": {
38+
"delay": "after this number of seconds, we may be reasonably sure that the search results are updated",
39+
}
40+
},
41+
"tests-cleanup-calendar": {
42+
"type": "tests-behaviour",
43+
"description": "Deleting a calendar does not delete the objects, or perhaps create/delete of calendars does not work at all. For each test run, every calendar resource object should be deleted for every test run",
44+
},
45+
"delete-calendar": {
46+
"description": "RFC4791 says nothing about deletion of calendars, so the server implementation is free to choose weather this should be supported or not. Section 3.2.3.2 in RFC 6638 says that if a calendar is deleted, all the calendarobjectresources on the calendar should also be deleted - but it's a bit unclear if this only applies to scheduling objects or not. Some calendar servers moves the object to a trashcan rather than deleting it"
47+
},
48+
"recurrences": {
49+
"description": "Support for recurring events and tasks",
50+
"features": {
51+
"save_load": {
52+
"description": "Calendar server should accept ojects with RRULE given, as well as objects with recurrence sets, and should be able to deliver back the same data",
53+
"features": {
54+
"todo": {"description": "works with tasks"},
55+
"todo": {"description": "works with events"},
56+
}
57+
},
58+
"search_includes_implicit_recurrences": {
59+
"description": "RFC says that the server MUST expand recurring components to determine whether any recurrence instances overlap the specified time range. Considered supported i.e. if a search for 2005 yields a yearly event happening first time in 2004.",
60+
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-7.4"],
61+
"features": {
62+
"infinite-scope": {
63+
"description": "Needless to say, search on any future date range, no matter how far out in the future, should yield the recurring object"
64+
}
65+
}
66+
},
67+
"expanded_search": {
68+
"description": "According to RFC 4791, the server MUST expand recurrence objects if asked for it - but many server doesn't do that. It doesn't matter much by now, as the client library can do the expandation. Some servers don't do expand at all, others deliver broken data, typically missing RECURRENCE-ID",
69+
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5"],
70+
"features": {
71+
"recurrence_exception_handling": {
72+
"description": "Server expand should work correctly also if a recurrence set with exceptions is given"
73+
},
74+
},
75+
},
76+
},
77+
},
78+
}
79+
80+
def __init__(self, feature_set):
81+
"""
82+
TODO: describe the feature_set better.
83+
84+
Should be a dict on the same style as self.FEATURES, but different.
85+
86+
Shortcuts accepted in the dict, like:
87+
88+
{
89+
"recurrences.search_includes_implicit_recurrences.infinite_scope":
90+
"unsupported" }
91+
92+
is equivalent with
93+
94+
{
95+
"recurrences": {
96+
"features": {
97+
"search_includes_inplicit_recurrences": {
98+
"infinite_scope":
99+
"support": "unsupported" }}}}
100+
101+
(TODO: is this sane? Am I reinventing a configuration language?)
102+
"""
103+
## TODO: copy the FEATURES dict, or just the feature_set dict?
104+
## (anyways, that is an internal design decision that may be
105+
## changed ... but we need test code in place)
106+
self._server_features = {}
107+
self.copyFeatureSet(feature_set)
108+
109+
def _copyFeature(def_node, server_node, value):
110+
if isinstance(value, str) and not 'support' in server_node:
111+
server_node['support'] = value
112+
elif isinstance(value, dict):
113+
if value.get('features'):
114+
raise NotImplementedError("todo ... work in progress ... need to recursively process the feature set")
115+
server_node.update(value)
116+
117+
def copyFeatureSet(feature_set):
118+
for feature in feature_set:
119+
fpath = feature.split('.')
120+
def_tree = self.FEATURES
121+
server_tree = self._server_features
122+
for step in fpath:
123+
assert def_tree
124+
assert step in def_tree
125+
def_node = def_tree[step]
126+
if not step in server_tree:
127+
server_tree[step] = {}
128+
server_node = server_tree[step]
129+
if not 'features' in def_tree:
130+
server_tree = None
131+
def_tree = None
132+
else:
133+
if not 'features' in server_node:
134+
server_node['features'] = {}
135+
server_tree = server_node['features']
136+
def_tree = def_node['features']
137+
self._copyFeature(def_node, server_node, feature_set[fpath])
138+
139+
def find_feature(self, feature: str) -> dict:
140+
"""
141+
Feature should be a string like feature.subfeature.subsubfeature.
142+
143+
Looks through the FEATURES list and returns the relevant section.
144+
145+
Will raise an AssertionError if feature is not found
146+
"""
147+
hierarchy = feature.split('.')
148+
node = self.FEATURES
149+
for x in hierarchy:
150+
assert x in node
151+
feature = node[x]
152+
node = feature.get('features', {})
153+
return feature
154+
155+
#### OLD STYLE
14156

15-
The issues may be grouped together, maybe even organized
16-
hierarchically. I did consider organizing the compatibility issues in
17-
some more advanced way, but I don't want to overcomplicate things - I
18-
will try out the key-value-approach first.
19-
"""
20157
## The lists below are specifying what tests should be skipped or
21158
## modified to accept non-conforming resultsets from the different
22159
## calendar servers. In addition there are some hacks in the library
@@ -30,26 +167,6 @@
30167
## * Perhaps some more readable format should be considered (yaml?).
31168
## * Consider how to get this into the documentation
32169
incompatibility_description = {
33-
'rate_limited':
34-
"""It may be needed to pause a bit between each request when doing tests""",
35-
36-
'search_delay':
37-
"""Server populates indexes through some background job, so it takes some time from an event is added/edited until it's possible to search for it""",
38-
39-
'cleanup_calendar':
40-
"""Remove everything on the calendar for every test""",
41-
42-
'no_delete_calendar':
43-
"""Not allowed to delete calendars - or calendar ends up in a 'trashbin'""",
44-
45-
'broken_expand':
46-
"""Server-side expand seems to work, but delivers wrong data (typically missing RECURRENCE-ID)""",
47-
48-
'no_expand':
49-
"""Server-side expand does not seem to work""",
50-
51-
'broken_expand_on_exceptions':
52-
"""The testRecurringDateWithExceptionSearch test breaks as the icalendar_component is missing a RECURRENCE-ID field. TODO: should be investigated more""",
53170

54171
'inaccurate_datesearch':
55172
"""A date search may yield results outside the search interval""",
@@ -66,19 +183,10 @@
66183
'no_current-user-principal':
67184
"""Current user principal not supported by the server (flag is ignored by the tests as for now - pass the principal URL as the testing URL and it will work, albeit with one warning""",
68185

69-
'no_recurring':
70-
"""Server is having issues with recurring events and/or todos. """
71-
"""date searches covering recurrances may yield no results, """
72-
"""and events/todos may not be expanded with recurrances""",
73-
74186
'no_alarmsearch':
75187
"""Searching for alarms may yield too few or too many or even a 500 internal server error""",
76188

77-
'no_recurring_todo':
78-
"""Recurring events are supported, but not recurring todos""",
79189

80-
'no_recurring_todo_expand':
81-
"""Recurring todos aren't expanded (this is ignored by the tests now, as we're doing client-side expansion)""",
82190

83191
'no_scheduling':
84192
"""RFC6833 is not supported""",
@@ -290,10 +398,11 @@
290398
"""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"""
291399
}
292400

293-
xandikos = [
401+
xandikos = {
402+
"recurrences.expanded_search": {'support': 'ungraceful'},
403+
"recurrences.search_includes_implicit_recurrences": {'support': 'unsupported'},
404+
"old_flags": [
294405
## https://github.com/jelmer/xandikos/issues/8
295-
"no_recurring",
296-
297406
'date_todo_search_ignores_duration',
298407
'text_search_is_exact_match_only',
299408
"search_needs_comptype",
@@ -316,14 +425,16 @@
316425

317426
## No alarm search (500 internal server error)
318427
"no_alarmsearch",
319-
]
428+
]
429+
}
320430

321431
## TODO - there has been quite some development in radicale recently, so this list
322432
## should probably be gone through
323-
radicale = [
433+
radicale = {
434+
"recurrences.expanded_search": {'support': 'ungraceful'},
435+
'old_flags': [
324436
## calendar listings and calendar creation works a bit
325437
## "weird" on radicale
326-
"broken_expand",
327438
"no_default_calendar",
328439
"no_alarmsearch", ## This is fixed and will be released soon
329440

@@ -342,7 +453,8 @@
342453
## extra features not specified in RFC5545
343454
"calendar_order",
344455
"calendar_color"
345-
]
456+
]
457+
}
346458

347459
## ZIMBRA IS THE MOST SILLY, AND THERE ARE REGRESSIONS FOR EVERY RELEASE!
348460
## AAARGH!

tests/conf.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def teardown_radicale(self):
144144
"username": "user1",
145145
"password": "",
146146
"backwards_compatibility_url": url + "user1",
147-
"incompatibilities": compatibility_hints.radicale,
147+
"features": compatibility_hints.radicale,
148148
"setup": setup_radicale,
149149
"teardown": teardown_radicale,
150150
}
@@ -229,7 +229,7 @@ def silly_request():
229229
"name": "LocalXandikos",
230230
"url": url,
231231
"backwards_compatibility_url": url + "sometestuser",
232-
"incompatibilities": compatibility_hints.xandikos,
232+
"features": compatibility_hints.xandikos,
233233
"setup": setup_xandikos,
234234
"teardown": teardown_xandikos,
235235
}
@@ -257,6 +257,7 @@ def client(
257257
elif no_args:
258258
return None
259259
for bad_param in (
260+
"features",
260261
"incompatibilities",
261262
"backwards_compatibility_url",
262263
"principal_url",
@@ -274,7 +275,7 @@ def client(
274275
conn = DAVClient(**kwargs_)
275276
conn.setup = setup
276277
conn.teardown = teardown
277-
conn.incompatibilities = kwargs.get("incompatibilities")
278+
conn.features = kwargs.get("features") or kwargs.get("incompatibilities")
278279
conn.server_name = name
279280
return conn
280281

tests/conf_private.py.EXAMPLE

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ caldav_servers = [
3030
## incompatibilities is a list of flags that can be set for
3131
## skipping (parts) of certain tests. See
3232
## compatibility_hints.py for premade lists
33-
#'incompatibilities': compatibility_hints.nextcloud
34-
'incompatibilities': [],
33+
#'features': compatibility_hints.nextcloud
34+
'features': [],
3535

3636
## You may even add setup and teardown methods to set up
3737
## and rig down the calendar server

0 commit comments

Comments
 (0)