Skip to content

Commit 7b2df67

Browse files
authored
Compatibility fixes
* The search for pending tasks will not do send any complicated search requests to the server if it's flagged that the server does not support such requests. * Bugfix for the PrincipalPropertySearch * Some typo-fixes in "server feature"-descriptions * Since the new feature matrix isn't included in any official release yet, I'm free to rename `features.check_support` to `client.features.is_supported`. It's not doing any checking, it's just looking up support in a dict. * Quite some work on the test code * While hammering on this, I got tests to complete (again) on Posteo, GMX, eCloud and possibly some other servers (but then I've hammered even more on it later ... so possibly it will break again)
1 parent 4978418 commit 7b2df67

5 files changed

Lines changed: 211 additions & 121 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Except for that, some minor bugfixes.
2424

2525
### Changed
2626

27+
* The search for pending tasks will not do send any complicated search requests to the server if it's flagged that the server does not support such requests. (automatically setting such flags will come in a later version)
28+
* If the server is flagged not to support MKCALENDAR but supporting MKCOL instead (baikal), then it will use MKCOL when creating a calendar. (automatically setting such flags will come in a later version)
2729
* In 1.5.0, I moved the compability matrix from the tests directory and into the project itself - now I'm doing a major overhaul of it. This change is much relevant for end users yet - but already now it's possible to configure "compatibility hints" when setting up the davclient, and the idea is that different kind of workarounds may be applied depending on the compatibility-matrix. Search without comp-type is wonky on many servers, now the `search`-method will automatically deliver a union of a search of the three different comp-types if a comp-type is not set in the parameters *and* it's declared that the compatibility matrix does not work. In parallel I'm developing a stand-alone tool caldav-server-tester to check the compatibility of a caldav server. https://github.com/python-caldav/caldav/issues/532 / https://github.com/python-caldav/caldav/pull/537
2830
* Littered the code with `try: import niquests as requests except: import requests`, making it easier to flap between requests and niquests.
2931
* Use the "caldav" logger consistently instead of global logging. https://github.com/python-caldav/caldav/pull/543 - fixed by Thomas Lovden
@@ -34,6 +36,8 @@ Except for that, some minor bugfixes.
3436
* Tweaks to support upcoming version 7 of the icalendar library.
3537
* Compatibility-tweaks for baikal, but as for now manual intervention is needed - see https://github.com/python-caldav/caldav/pull/556 and https://github.com/python-caldav/caldav/issues/553
3638
* Bugfix on authentication - things broke on Baikal if authentication method (i.e. digest) was set in the config. I found a quite obvious bug, I did not investigate why the test code has been passing on all the other servers. Weird thing.
39+
* Bugfix in the `davclient.principals`-method, allowing it to work on more servers - https://github.com/python-caldav/caldav/pull/559
40+
* Quite some compatibility-fixing of the test code
3741

3842
### Added
3943

caldav/collection.py

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def _create(
419419

420420
if method is None:
421421
if self.client:
422-
supported = self.client.features.check_support(
422+
supported = self.client.features.is_supported(
423423
"create-calendar", return_type=dict
424424
)
425425
if supported["support"] not in ("full", "fragile", "quirk"):
@@ -887,32 +887,74 @@ def search(
887887

888888
## special compatibility-case when searching for pending todos
889889
if todo and not include_completed:
890-
matches1 = self.search(
891-
todo=True,
892-
ignore_completed1=True,
893-
include_completed=True,
894-
**kwargs,
895-
)
896-
matches2 = self.search(
897-
todo=True,
898-
ignore_completed2=True,
899-
include_completed=True,
900-
**kwargs,
901-
)
902-
matches3 = self.search(
903-
todo=True,
904-
ignore_completed3=True,
905-
include_completed=True,
906-
**kwargs,
907-
)
890+
## There are two ways to get the pending tasks - we can
891+
## ask the server to filter them out, or we can do it
892+
## client side.
893+
894+
## If the server does not support combined searches, then it's
895+
## safest to do it client-side.
896+
897+
## There is a special case (observed with radicale as of
898+
## 2025-11) where future recurrences of a task does not
899+
## match when doing a server-side filtering, so for this
900+
## case we also do client-side filtering (but the
901+
## "feature"
902+
## search.recurrences.includes-implicit.todo.pending will
903+
## not be supported if the feature
904+
## "search.recurrences.includes-implicit.todo" is not
905+
## supported ... hence the weird or below)
906+
907+
## To be completely sure to get all pending tasks, for all
908+
## server implementations and for all valid icalendar
909+
## objects, we send three different searches to the
910+
## server. This is probably bloated, and may in many
911+
## cases be more expensive than to ask for all tasks. At
912+
## the other hand, for a well-used and well-handled old
913+
## todo-list, there may be a small set of pending tasks
914+
## and heaps of done tasks.
915+
916+
## TODO: consider if not ignore_completed3 is sufficient,
917+
## then the recursive part of the query here is moot, and
918+
## we wouldn't waste so much time on repeated queries
919+
920+
if self.client.features.is_supported("search.combined-is-logical-and") and (
921+
not self.client.features.is_supported(
922+
"search.recurrences.includes-implicit.todo"
923+
)
924+
or self.client.features.is_supported(
925+
"search.recurrences.includes-implicit.todo.pending"
926+
)
927+
):
928+
matches = (
929+
self.search(
930+
todo=True,
931+
ignore_completed1=True,
932+
include_completed=True,
933+
**kwargs,
934+
)
935+
+ self.search(
936+
todo=True,
937+
ignore_completed2=True,
938+
include_completed=True,
939+
**kwargs,
940+
)
941+
+ self.search(
942+
todo=True,
943+
ignore_completed3=True,
944+
include_completed=True,
945+
**kwargs,
946+
)
947+
)
948+
else:
949+
matches = self.search(todo=True, include_completed=True, **kwargs)
908950
objects = []
909951
match_set = set()
910-
for item in matches1 + matches2 + matches3:
952+
for item in matches:
911953
if item.url not in match_set:
912954
match_set.add(item.url)
913-
## and still, Zimbra seems to deliver too many TODOs in the
914-
## matches2 ... let's do some post-filtering in case the
915-
## server fails in filtering things the right way
955+
## Client-side filtering is probably cheap, so we'll do it
956+
## even when it shouldn't be needed.
957+
## (can we assert all tasks have a valid STATUS field?)
916958
if any(
917959
x.get("STATUS") not in ("COMPLETED", "CANCELLED")
918960
for x in item.icalendar_instance.subcomponents
@@ -941,7 +983,7 @@ def search(
941983
"props": props,
942984
}
943985

944-
if not comp_class and not self.client.features.check_support(
986+
if not comp_class and not self.client.features.is_supported(
945987
"search.comp-type-optional"
946988
):
947989
if kwargs2["include_completed"] is None:

caldav/compatibility_hints.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ class FeatureSet:
3232
* "support" -> "quirk" if we have a server-peculiarity where it's needed with special care to get the request through.
3333
"""
3434
FEATURES = {
35+
"get-all-principals": {
36+
"description": "Search for all principals, using a DAV REPORT query, yields at least one principal"
37+
},
3538
"get-current-user-principal": {
36-
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the standard"},
39+
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the DAV standard"},
3740
"get-current-user-principal.has-calendar": {
3841
"type": "server-observation",
3942
"description": "Principal has one or more calendars. Some servers and providers comes with a pre-defined calendar for each user, for other servers a calendar has to be explicitly created (supported means there exists a calendar - it may be because the calendar was already provisioned together with the principal, or it may be because a calendar was created manually, the checks can't see the difference)"},
@@ -109,21 +112,28 @@ class FeatureSet:
109112
"search.recurrences.includes-implicit.todo": {
110113
"description": "tasks can also be recurring"
111114
},
115+
"search.recurrences.includes-implicit.todo.pending": {
116+
"description": "a future recurrence of a pending task should always be pending and appear in searches for pending tasks"
117+
},
112118
"search.recurrences.includes-implicit.event": {
113119
"description": "support for events"
114120
},
115121
"search.recurrences.includes-implicit.infinite-scope": {
116122
"description": "Needless to say, search on any future date range, no matter how far out in the future, should yield the recurring object"
117123
},
124+
"search.combined-is-logical-and": {
125+
"description": "Multiple search filters should yield only those that passes all filters"
126+
## For "unsupported", we could also add a "behaviour" (returns everything, returns nothing, returns logical OR, etc).
127+
},
118128
"search.recurrences.expanded": {
119129
"description": "According to RFC 4791, the server MUST expand recurrence objects if asked for it - but many server doesn't do that. Some servers don't do expand at all, others deliver broken data, typically missing RECURRENCE-ID. The python caldav client library (from 2.0) does the expand-operation client-side no matter if it's supported or not",
120130
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5"],
121131
},
122132
"search.recurrences.expanded.todo": {
123-
"description": "examding tasks"
133+
"description": "expanding tasks"
124134
},
125135
"search.recurrences.expanded.event": {
126-
"description": "examding events"
136+
"description": "exanding events"
127137
},
128138
"search.recurrences.expanded.exception": {
129139
"description": "Server expand should work correctly also if a recurrence set with exceptions is given"
@@ -215,7 +225,7 @@ def collapse(self):
215225
parent_info = self.find_feature(parent)
216226

217227
if len(parent_info['subfeatures']):
218-
foo = self.check_support(parent, return_type=dict, return_defaults=False)
228+
foo = self.is_supported(parent, return_type=dict, return_defaults=False)
219229
if len(parent_info['subfeatures']) > 1 or foo is not None:
220230
dont_collapse = False
221231
for sub in parent_info['subfeatures']:
@@ -255,13 +265,9 @@ def _default(self, feature_info):
255265
else:
256266
breakpoint()
257267

258-
def check_support(self, feature, return_type=bool, return_defaults=True):
268+
def is_supported(self, feature, return_type=bool, return_defaults=True, accept_fragile=False):
259269
"""Work in progress
260270
261-
TODO: rename. This method does not do any checking, just a
262-
lookup. "get_support" sounds wrong, but perhaps
263-
"lookup_support"?
264-
265271
TODO: write a better docstring
266272
267273
The dotted features is essentially a tree. If feature foo
@@ -272,14 +278,14 @@ def check_support(self, feature, return_type=bool, return_defaults=True):
272278
feature_ = feature
273279
while True:
274280
if feature_ in self._server_features:
275-
return self._convert_node(self._server_features[feature_], feature_info, return_type)
281+
return self._convert_node(self._server_features[feature_], feature_info, return_type, accept_fragile)
276282
if not '.' in feature_:
277283
if not return_defaults:
278284
return None
279-
return self._convert_node(self._default(feature_info), feature_info, return_type)
285+
return self._convert_node(self._default(feature_info), feature_info, return_type, accept_fragile)
280286
feature_ = feature_[:feature_.rfind('.')]
281287

282-
def _convert_node(self, node, feature_info, return_type):
288+
def _convert_node(self, node, feature_info, return_type, accept_fragile=False):
283289
"""
284290
Return the information in a "node" given the wished return_type
285291
@@ -298,7 +304,13 @@ def _convert_node(self, node, feature_info, return_type):
298304
support = node.get('support', 'full')
299305
if support == 'quirk':
300306
return True
301-
return support == 'full' and not node.get('enable') and not node.get('behaviour') and not node.get('observed')
307+
if accept_fragile and support == 'fragile':
308+
support = 'full'
309+
if feature_info.get('type', 'server-feature') == 'server-feature':
310+
return support == 'full'
311+
else:
312+
## TODO: this may be improved
313+
return not node.get('enable') and not node.get('behaviour') and not node.get('observed')
302314
else:
303315
assert False
304316

@@ -535,9 +547,6 @@ def dotted_feature_set_list(self, compact=False):
535547
'text_search_is_exact_match_sometimes':
536548
"""Some servers are doing an exact match on summary field but substring match on category or vice versa""",
537549

538-
'combined_search_not_working':
539-
"""When querying for a text match and a date range in the same report, weird things happen""",
540-
541550
'text_search_not_working':
542551
"""Text search is generally broken""",
543552

@@ -621,6 +630,7 @@ def dotted_feature_set_list(self, compact=False):
621630
## so I'm expecting this list to shrink a lot soon.
622631
radicale = {
623632
"search.category.fullstring": {"support": "unsupported"},
633+
"search.recurrences.includes-implicit.todo.pending": {"support": "unsupported"},
624634
"search.recurrences.expanded.todo": {"support": "unsupported"},
625635
"search.recurrences.expanded.exception": {"support": "unsupported"},
626636
'old_flags': [
@@ -634,9 +644,9 @@ def dotted_feature_set_list(self, compact=False):
634644
"no-principal-search-self", ## this may be because we haven't set up any users or authentication - so the display name of the current user principal is None
635645

636646
'no_scheduling',
647+
'no_search_openended',
637648

638649
'text_search_is_case_insensitive',
639-
'combined_search_not_working',
640650
#'text_search_is_exact_match_sometimes',
641651

642652
## extra features not specified in RFC5545
@@ -656,10 +666,14 @@ def dotted_feature_set_list(self, compact=False):
656666
'support': 'fragile',
657667
'behaviour': 'Deleting a recently created calendar fails'},
658668
'delete-calendar.free-namespace': { ## TODO: not caught by server-tester
659-
'behaviour': "deleting a calendar moves it to a trashbin, thrashbin has to be manually 'emptied' from the web-ui before the namespace is freed up"},
669+
'behaviour': "deleting a calendar moves it to a trashbin, thrashbin has to be manually 'emptied' from the web-ui before the namespace is freed up",
670+
'support': 'fragile',
671+
},
660672
'search.comp-type-optional': {
661673
'support': 'ungraceful',
662674
},
675+
"search.combined-is-logical-and": {"support": "unsupported"},
676+
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
663677
## TODO: this applies only to test runs, not to ordinary usage
664678
'rate-limit': {
665679
'enable': True,
@@ -726,15 +740,17 @@ def dotted_feature_set_list(self, compact=False):
726740

727741
baikal = {
728742
'create-calendar': {'support': 'quirk', 'behaviour': 'mkcol-required'},
743+
'create-calendar.auto': {'support': 'unsupported'}, ## this is the default, but the "quirk" from create-calendar overwrites it. Hm.
729744
'search.category.fullstring.smart': {'support': 'unsupported'},
730745
'search.comp-type-optional': {'support': 'ungraceful'},
731746
'search.recurrences.expanded.todo': {'support': 'unsupported'},
732747
'search.recurrences.expanded.exception': {'support': 'unsupported'},
748+
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
749+
"search.combined-is-logical-and": {"support": "unsupported"},
733750
'old_flags': [
734751
## date search on todos does not seem to work
735752
## (TODO: do some research on this)
736753
'sync_breaks_on_delete',
737-
'combined_search_not_working',
738754
'text_search_is_exact_match_sometimes',
739755
## extra features not specified in RFC5545
740756
"calendar_order",
@@ -834,8 +850,9 @@ def dotted_feature_set_list(self, compact=False):
834850
"search.category": { "support": "unsupported" },
835851
"search.comp-type-optional": { "support": "ungraceful" },
836852
"search.recurrences.expanded.todo": { "support": "unsupported" },
837-
#"search.recurrences.expanded.event": { "support": "fragile" },
853+
"search.recurrences.expanded.event": { "support": "fragile" },
838854
"search.recurrences.expanded.exception": { "support": "unsupported" },
855+
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
839856
'old_flags': [
840857
'non_existing_raises_other', ## AuthorizationError instead of NotFoundError
841858
'no_scheduling',
@@ -853,15 +870,18 @@ def dotted_feature_set_list(self, compact=False):
853870
}
854871

855872
posteo = {
856-
"create-calendar": { "support": "unsupported" },
857-
"search.recurrences.expanded.exception": { "support": "unsupported" },
858-
"search.recurrences.includes-implicit.todo": { "support": "unsupported" },
873+
'create-calendar': {'support': 'unsupported'},
874+
'search.category.fullstring.smart': {'support': 'unsupported'},
875+
'search.comp-type-optional': {'support': 'ungraceful'},
876+
'search.recurrences.expanded.todo': {'support': 'unsupported'},
877+
'search.recurrences.expanded.exception': {'support': 'unsupported'},
878+
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
879+
"search.combined-is-logical-and": {"support": "unsupported"},
859880
'old_flags': [
860881
'no_scheduling',
861882
'no_journal',
862883
#'no_recurring_todo', ## todo
863884
'no_sync_token',
864-
'combined_search_not_working',
865885
'no_alarmsearch',
866886
"no-principal-search-self"
867887
]
@@ -885,6 +905,7 @@ def dotted_feature_set_list(self, compact=False):
885905
## Purelymail claims that the search indexes are "lazily" populated,
886906
## so search works some minutes after the event was created/edited.
887907
'search-cache': {'behaviour': 'delay', 'delay': 120},
908+
"create-calendar.auto": {"support": "full"},
888909
'old_flags': [
889910
## Known, work in progress
890911
'no_scheduling',
@@ -900,11 +921,6 @@ def dotted_feature_set_list(self, compact=False):
900921
}
901922

902923
gmx = {
903-
## This WILL create some arbitrary objects from year 2000 on your calendar when running
904-
## tests.
905-
## It's fine for me as my gmx calendar is only for testing, but it may not be
906-
## fine for you.
907-
"test-calendar.compatibility-tests": { "name": "Mein Kalender", "cleanup": True },
908924
'create-calendar': {'support': 'unsupported'},
909925
'search.category.fullstring.smart': {'support': 'unsupported'},
910926
'search.comp-type-optional': {'support': 'fragile', 'description': 'unexpected results from date-search without comp-type - but only sometimes - TODO: research more'},

0 commit comments

Comments
 (0)