Skip to content

Commit 4dd5df2

Browse files
committed
getting somewhere, one code line at the time
1 parent ac19803 commit 4dd5df2

4 files changed

Lines changed: 56 additions & 20 deletions

File tree

caldav/compatibility_hints.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
encountered while working on the caldav library, and descriptions on
55
how the well-known servers behave.
66
"""
7+
8+
import copy
9+
710
## NEW STYLE
811
## (we're gradually moving stuff from the good old
912
## "incompatibility_description" below over to
@@ -18,14 +21,20 @@ class FeatureSet:
1821
An object of this class describes the feature set of a server.
1922
2023
TODO: use enums?
21-
type -> "client-feature", "server-peculiarity", "server-feature" (last is default)
24+
type -> "client-feature", "server-peculiarity", "tests-behaviour", "server-observation", "server-feature" (last is default)
2225
support -> "supported" (default), "unsupported", "fragile", "broken", "ungraceful"
26+
27+
types:
28+
* client-feature means the client is supposed to do special things (like, rate-limiting). While the need for rate-limiting may be set by the server, it may not be possible to reliably establish it by probling the server, and the value may differ for different clients.
29+
* server-peculiarity - weird behaviour detected at the server side, behaviour that is too odd to be described as "missing support for a feature". Example: there is some cache working, causing a delay from some object is sent to the server and until it can be retrieved. The difference between an "unsupported server-feature" and a "server-peculiarity" may be a bit floating - like, arguably "instant updates" may be considered a feature.
2330
"""
2431
FEATURES = {
2532
"get-current-user-principal": {
2633
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the standard",
2734
"features": {
28-
"has-calendar": "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)"
35+
"has-calendar": {
36+
"type": "server-observation",
37+
"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)"}
2938
}
3039
},
3140
"rate-limit": {
@@ -50,6 +59,7 @@ class FeatureSet:
5059
"description": "RFC4791 says that \"support for MKCALENDAR on the server is only RECOMMENDED and not REQUIRED because some calendar stores only support one calendar per user (or principal), and those are typically pre-created for each account\". Hence a conformant server may opt to not support creating calendars, this is often seen for cloud services (some services allows extra calendars to be made, but not through the CalDAV protocol). (RFC4791 also says that the server MAY support MKCOL in section 8.5.2. I do read it as MKCOL may be used for creating calendars - which is weird, since section 8.5.2 is titled \"external attachments\". We should consider testing this as well)",
5160
"features": {
5261
"auto": {
62+
"default": { "support": "unsupported" },
5363
"description": "Accessing a calendar which does not exist automatically creates it",
5464
},
5565
"set-displayname": {
@@ -122,6 +132,9 @@ def __init__(self, feature_set_dict=None):
122132
123133
(TODO: is this sane? Am I reinventing a configuration language?)
124134
"""
135+
if isinstance(feature_set_dict, FeatureSet):
136+
self._server_features = copy.deepcopy(feature_set_dict._server_features)
137+
125138
## TODO: copy the FEATURES dict, or just the feature_set dict?
126139
## (anyways, that is an internal design decision that may be
127140
## changed ... but we need test code in place)
@@ -162,6 +175,24 @@ def copyFeatureSet(self, feature_set):
162175
def_tree = def_node['features']
163176
self._copyFeature(def_node, server_node, feature_set[feature])
164177

178+
def _default(self, feature_info):
179+
if isinstance(feature_info, str):
180+
feature_info = self.find_feature(feature_info)
181+
if 'default' in feature_info:
182+
return feature_info['default']
183+
feature_type = feature_info.get('type', 'server-feature')
184+
## TODO: move the default values up to some constant dict probably, like self.DEFAULTS = { "server-feature": {...}}
185+
if feature_type == 'server-feature':
186+
return { "support": "full" }
187+
elif feature_type == 'client-feature':
188+
return { "enable": False }
189+
elif feature_type == 'server-peculiarity':
190+
return { "behaviour": "normal" }
191+
elif feature_type == 'server-observation':
192+
return { "observed": True }
193+
else:
194+
breakpoint()
195+
165196
def check_support(self, feature, return_type=bool):
166197
"""
167198
Work in progress
@@ -178,9 +209,7 @@ def check_support(self, feature, return_type=bool):
178209
node = support.get('features', {})
179210
else:
180211
if support is None:
181-
## default is that the server supports everything
182-
## TODO: consider feature_info['type'], be smarter about this
183-
support = {'support': 'full'}
212+
support = self._default(feature_info)
184213
break
185214
if return_type == str:
186215
## TODO: consider type, be smarter about it
@@ -189,7 +218,7 @@ def check_support(self, feature, return_type=bool):
189218
return support
190219
elif return_type == bool:
191220
## TODO: consider feature_info['type'], be smarter about this
192-
return support.get('support', 'full') == 'full' and not support.get('enable') and not support.get('behaviour')
221+
return support.get('support', 'full') == 'full' and not support.get('enable') and not support.get('behaviour') and not support.get('observed')
193222

194223
## TODO: could be a class method?
195224
def find_feature(self, feature: str) -> dict:
@@ -208,26 +237,28 @@ def find_feature(self, feature: str) -> dict:
208237
node = feature.get('features', {})
209238
return feature
210239

211-
def dotted_feature_set_list(self):
212-
return self._dotted_feature_set_list('', self._server_features)
240+
def dotted_feature_set_list(self, compact=False):
241+
return self._dotted_feature_set_list('', self._server_features, compact=compact)
213242

214-
def _dotted_feature_set_list(self, feature_path, feature_node):
243+
def _dotted_feature_set_list(self, feature_path, feature_node, compact):
215244
ret = {}
216245
if feature_path:
217246
feature_path = f"{feature_path}."
218247
for x in feature_node:
219248
feature = feature_node[x].copy()
220249
full = f"{feature_path}{x}"
221-
ret[full] = feature
250+
feature_info = self.find_feature(full)
251+
if compact and feature_info.get('type', 'server-feature') in ('client-feature', 'server-observation'):
252+
continue
222253
if 'features' in feature:
223254
subnode = feature.pop('features')
224-
ret.update(self._dotted_feature_set_list(full, subnode))
255+
ret.update(self._dotted_feature_set_list(full, subnode, compact))
256+
if compact and feature == self._default(feature_info):
257+
continue
258+
if feature:
259+
ret[full] = feature
225260
return ret
226261

227-
def __eq__(self, other):
228-
## TODO: this won't work, but it's a start
229-
return self.dotted_feature_set_list() == other.dotted_feature_set_list()
230-
231262
#### OLD STYLE
232263

233264
## The lists below are specifying what tests should be skipped or

caldav/davclient.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ def __init__(
459459
ssl_cert: Union[str, Tuple[str, str], None] = None,
460460
headers: Mapping[str, str] = None,
461461
huge_tree: bool = False,
462-
features: FeatureSet = None
462+
features: Union[FeatureSet, dict] = None
463463
) -> None:
464464
"""
465465
Sets up a HTTPConnection object towards the server in the url.
@@ -493,7 +493,7 @@ def __init__(
493493
log.debug("url: " + str(url))
494494
self.url = URL.objectify(url)
495495
self.huge_tree = huge_tree
496-
self.features = features
496+
self.features = FeatureSet(features)
497497
# Prepare proxy info
498498
if proxy is not None:
499499
_proxy = proxy

tests/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from caldav import compatibility_hints
1414
from caldav.davclient import CONNKEYS
1515
from caldav.davclient import DAVClient
16+
from caldav.compatibility_hints import FeatureSet
1617

1718
####################################
1819
# Import personal test server config
@@ -275,7 +276,7 @@ def client(
275276
conn = DAVClient(**kwargs_)
276277
conn.setup = setup
277278
conn.teardown = teardown
278-
conn.features = kwargs.get("features") or kwargs.get("incompatibilities")
279+
conn.features = FeatureSet(kwargs.get("features"))
279280
conn.server_name = name
280281
return conn
281282

tests/test_caldav.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,8 +742,8 @@ def setup_method(self):
742742

743743
def teardown_method(self):
744744
if (
745-
self.check_support("search-cache", dict).get("behaviour", "no-cache")
746-
!= "no-cache"
745+
self.check_support("search-cache", dict).get("behaviour", "normal")
746+
!= "normal"
747747
):
748748
Calendar.search = Calendar._search
749749
logging.debug("############################")
@@ -851,6 +851,10 @@ def _fixCalendar(self, **kwargs):
851851
def testCheckCompatibility(self):
852852
checker = ServerQuirkChecker(self.caldav)
853853
checker.check_all()
854+
observed = checker.features_checked.dotted_feature_set_list(compact=True)
855+
expected = self.caldav.features.dotted_feature_set_list(compact=True)
856+
assert(observed == expected)
857+
854858
assert(checker.features_checked == self.caldav.features)
855859

856860
def testSupport(self):

0 commit comments

Comments
 (0)