From 95e99d39046c0186eded605c41c5674268b2a0d0 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Tue, 20 Oct 2026 17:19:29 +0200 Subject: [PATCH] feat: compatibility features replace the old flags Two old-style flags were still set by a server profile. They become compatibility features: save-load.todo.uncomplete (GMX keeps a task completed) and search.time-range.todo.no-dtstart.no-due (Zimbra misses a task with neither DTSTART nor DUE). The latter follows no-dtstart unless set, so DAViCal and Stalwart need no entry. The other seven flags were set by no profile and are deleted with the test branches reading them, as are incompatibility_description and old_flags. Prompt: Please work on the issue about finally getting rid of the old compatibility_hints [https://github.com/python-caldav/caldav/issues/585]. This involves some work in caldav-server-tester as well, as the policy is that all the "feature hints" should come with checks. Followup-Prompt: "no-dates" doesn't sound right, it may apply to "something having time but not a date". timeless? no-dt? ...todo.without-dt? [on the name proposed for the feature replacing vtodo_datesearch_notime_task_is_skipped] It's not a breaking change, those compatibility_hints was (sic) never intended for anything else than disabling/enabling tests, it was never part of the public API. Even the new featureset is explicitly declared to be unstable in 3.x. Testing towards GMX worked earlier this week, the authorization error problem has been resolved. The zimbra container is already running. caldav-server-tester having red pull requests until caldav is released is normal. Go ahead. Assisted-By: Claude Opus 5.5 (1M context) Reviewed-by: Tobias Brox --- caldav/compatibility_hints.py | 112 +++++---------------- changes/585.added.md | 1 + changes/585.removed.md | 1 + docs/design/FEATURE_COMPLETE_ROADMAP.md | 2 +- docs/design/TODO_COMPATIBILITY_HINTS.md | 30 ++---- tests/test_async_integration.py | 19 +--- tests/test_caldav.py | 128 +++++++----------------- 7 files changed, 74 insertions(+), 219 deletions(-) create mode 100644 changes/585.added.md create mode 100644 changes/585.removed.md diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index bd68a8c7..733759c9 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -32,11 +32,6 @@ ## caldav_server_checker, and things got messy from there. We should ## double-check the values where there is any doubt, and clean up. -## NEW STYLE -## (we're gradually moving stuff from the good old -## "incompatibility_description" below over to -## "compatibility_features") - class FeatureSet: """Work in progress ... TODO: write a better class description. @@ -433,6 +428,7 @@ class FeatureSet: "save-load.todo.recurrences.count": {"description": "The server will receive and store a recurring task with a count set in the RRULE", "default": {"support": "full"}}, "save-load.todo.recurrences.thisandfuture": {"description": "Completing a recurring task with rrule_mode='thisandfuture' works (modifies RRULE and saves back to server)", "default": {"support": "full"}}, "save-load.todo.mixed-calendar": {"description": "The same calendar may contain both events and tasks (Zimbra only allows tasks to be placed on special task lists)", "default": {"support": "full"}}, + "save-load.todo.uncomplete": {"description": "A completed task can be made pending again (Todo.uncomplete()): after saving it with STATUS:NEEDS-ACTION and without COMPLETED, it loads back that way.", "default": {"support": "full"}}, "save-load.journal": { "description": "The server will even accept journals", "default": { "support": "full" } @@ -546,7 +542,12 @@ class FeatureSet: }, "search.time-range.todo": {"description": "basic time range searches for tasks works", "default": {"support": "full"}}, "search.time-range.todo.no-dtstart": { - "description": "A VTODO without DTSTART (but with DUE) is returned by a date-range search. RFC5545 and RFC4791 section 9.9 say such a task has a defined time span and should be found, so 'full' (the default) is the compliant behaviour; some servers (Davical, Stalwart, Synology) skip any task lacking DTSTART. Probed with a closed window; servers that skip such tasks only in closed ranges (returning them in open-ended ones) are instead tracked by the 'vtodo_datesearch_nodtstart_task_is_skipped_in_closed_date_range' flag.", + "description": "A VTODO without DTSTART (but with DUE) is returned by a date-range search. RFC5545 and RFC4791 section 9.9 say such a task has a defined time span and should be found, so 'full' (the default) is the compliant behaviour; some servers (Davical, Stalwart, Synology) skip any task lacking DTSTART. Probed with a closed window.", + "default": {"support": "full"}, + "links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.9"], + }, + "search.time-range.todo.no-dtstart.no-due": { + "description": "A VTODO with neither DTSTART nor DUE (nor COMPLETED or CREATED) is returned by a date-range search. RFC4791 section 9.9 says such a task overlaps every time range. Zimbra only finds tasks with DTSTART or DUE set. Unless set, it follows search.time-range.todo.no-dtstart, as a server skipping tasks without DTSTART skips these too. Probed with a closed window.", "default": {"support": "full"}, "links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.9"], }, @@ -850,7 +851,6 @@ def __init__(self, feature_set_dict=None): if isinstance(feature_set_dict, FeatureSet): self._server_features = copy.deepcopy(feature_set_dict._server_features) self.backward_compatibility_mode = feature_set_dict.backward_compatibility_mode - self._old_flags = copy.copy(feature_set_dict._old_flags) if hasattr(feature_set_dict, '_old_flags') else [] return ## TODO: copy the FEATURES dict, or just the feature_set dict? @@ -858,8 +858,6 @@ def __init__(self, feature_set_dict=None): ## changed ... but we need test code in place) self.backward_compatibility_mode = feature_set_dict is None self._server_features = {} - ## TODO: remove this when it can be removed - self._old_flags = [] if feature_set_dict: self._copy_feature_set(feature_set_dict, collapse=False) @@ -908,10 +906,6 @@ def copy_feature_set(self, feature_set, collapse=True): ## warning with stacklevel=3 points at the user's code def _copy_feature_set(self, feature_set, collapse=True): for feature in feature_set: - ## TODO: temp - should be removed - if feature == 'old_flags': - self._old_flags = feature_set[feature] - continue if feature == 'write-delay': warnings.warn( "The 'write-delay' feature is deprecated - use 'synchronous-write', " @@ -1357,60 +1351,10 @@ def compare(self, observed): ) return mismatches -#### OLD STYLE - -## THE LIST BELOW IS TO BE REMOVED COMPLETELY. DO NOT USE IT. - -## It's not considered to be part of the public API (though, it should -## have been prefixed with _ to make it clear). The list is being -## removed little-by-little, without regards of SemVer. - -## The lists below are specifying what tests should be skipped or -## modified to accept non-conforming resultsets from the different -## calendar servers. In addition there are some hacks in the library -## code itself to work around some known compatibility issues, like -## the caldav.lib.vcal.fix function. -## Here is a list of all observed (in)compatibility issues the test framework needs to know about -## TODO: -## * references to the relevant parts of the RFC would be nice. -## * Research should be done to triple-check that the issue is on the server side, and not on the client side -## * Some of the things below should be possible to probe the server for. -## * Perhaps some more readable format should be considered (yaml?). -## * Consider how to get this into the documentation -incompatibility_description = { - 'event_by_url_is_broken': - """A GET towards a valid calendar object resource URL will yield 404 (wtf?)""", - - 'vtodo_datesearch_nodtstart_task_is_skipped_in_closed_date_range': - """only open-ended date searches for todo-items will find tasks without a dtstart""", - - 'vtodo_datesearch_notime_task_is_skipped': - """date searches for todo-items will (only) find tasks that has either """ - """a dtstart or due set""", - - 'vtodo_no_due_infinite_duration': - """date search will find todo-items without due if dtstart is """ - """before the date search interval. This is in breach of rfc4791""" - """section 9.9""", - - 'vtodo-cannot-be-uncompleted': - """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""", - - 'sticky_events': - """Events should be deleted before the calendar is deleted, """ - """and/or deleting a calendar may not have immediate effect""", - - 'dav_not_supported': - """when asked, the server may claim it doesn't support the DAV protocol. Observed by one baikal server, should be investigated more (TODO) and robur""", - - 'fastmail_buggy_noexpand_date_search': - """The 'blissful anniversary' recurrent example event is returned when asked for a no-expand date search for some timestamps covering a completely different date""", - - 'robur_rrule_freq_yearly_expands_monthly': - """Robur expands a yearly event into a monthly event. I believe I've reported this one upstream at some point, but can't find back to it""", - -} - +## Server profiles. Each dict below is a feature set (see +## FeatureSet.FEATURES above) describing one server. In addition there +## are some hacks in the library code itself to work around some known +## compatibility issues, like the caldav.lib.vcal.fix function. xandikos = { ## Probed 2026-10-11 on 0.4.8, unchanged on master (0.4.9): a token that ## isn't a hex SHA crashes the git store lookup, and a hex one is refused @@ -1677,20 +1621,14 @@ def compare(self, observed): 'save-load.icalendar.related-to': {'support': 'unsupported'}, 'search.time-range.open.start': {'support': 'broken'}, - "old_flags": [ - ## setting display name in zimbra does not work (display name, - ## calendar-ID and URL is the same, the display name cannot be - ## changed, it can only be given if no calendar-ID is given. In - ## earlier versions of Zimbra display-name could be changed, but - ## then the calendar would not be available on the old URL - ## anymore) - ## 'event_by_url_is_broken' removed - works in zimbra/zcs-foss:latest - 'vtodo_datesearch_notime_task_is_skipped', + ## Probed 2026-10-20: a task with DUE but no DTSTART is found by a + ## date search, one with neither is not + 'search.time-range.todo.no-dtstart.no-due': {'support': 'unsupported'}, ## TODO: I just discovered that when searching for a date some ## years after a recurring daily event was made, the event does ## not appear. - ], + ## extra properties not specified in RFC4791/RFC5545. Zimbra stores ## calendar-order, and stores calendar-color only when set as a hex value - ## it rejects/ignores a colour name like "blue". (The old 'calendar_color' @@ -2079,12 +2017,6 @@ def compare(self, observed): 'principal-search': {'support': 'unsupported'}, ## DAViCal skips VTODOs without DTSTART in date-range searches. 'search.time-range.todo.no-dtstart': {'support': 'unsupported'}, - "old_flags": [ - #'no_journal', ## it threw a 500 internal server error! ## for old versions - #'nofreebusy', ## for old versions - ## 'fragile_sync_tokens' removed - covered by 'sync-token': {'support': 'fragile'} - 'vtodo_datesearch_notime_task_is_skipped', - ], ## extra properties not specified in RFC4791/RFC5545 "calendar-color": {"support": "full"}, "calendar-order": {"support": "full"}, @@ -2184,7 +2116,10 @@ def compare(self, observed): 'test-calendar': {'cleanup-regime': 'wipe-calendar'}, } -## Old notes for sogo (todo - incorporate them in the structure above) +## Old notes for sogo (todo - incorporate them in the structure above). +## The commented-out lists here and below name flags of the old-style +## system, removed in https://github.com/python-caldav/caldav/issues/585 - +## kept only as a record of what was once observed on those servers. ## https://www.sogo.nu/bugs/view.php?id=3065 ## left a note about time-based sync tokens on https://www.sogo.nu/bugs/view.php?id=5163 ## https://www.sogo.nu/bugs/view.php?id=5282 @@ -2606,10 +2541,11 @@ def compare(self, observed): ## Open-start searches used to work (no hint, so full), but were ## rejected with 403 Forbidden 2026-10-11 'search.time-range.open.start': {'support': 'fragile'}, - "old_flags": [ - #"text_search_is_case_insensitive", - "vtodo-cannot-be-uncompleted", - ] + ## Probed 2026-10-20 + "save-load.todo.uncomplete": { + "support": "unsupported", + "behaviour": "the task is saved as pending, but loads back with STATUS:COMPLETED", + }, } ## https://ox.io/ diff --git a/changes/585.added.md b/changes/585.added.md new file mode 100644 index 00000000..b94819a5 --- /dev/null +++ b/changes/585.added.md @@ -0,0 +1 @@ +Compatibility features `save-load.todo.uncomplete` (a completed task can be made pending again with `Todo.uncomplete()`; GMX keeps it completed) and `search.time-range.todo.no-dtstart.no-due` (a date search finds a task with neither DTSTART nor DUE, as RFC 4791 section 9.9 says it should; Zimbra does not). They replace the last two old-style flags in the Zimbra and GMX profiles. diff --git a/changes/585.removed.md b/changes/585.removed.md new file mode 100644 index 00000000..c92da999 --- /dev/null +++ b/changes/585.removed.md @@ -0,0 +1 @@ +The old-style compatibility flags are gone: `caldav.compatibility_hints.incompatibility_description`, and the `old_flags` key in a feature set. They were only ever used by the test suite to skip or adjust tests, and every flag still in use is now a compatibility feature. diff --git a/docs/design/FEATURE_COMPLETE_ROADMAP.md b/docs/design/FEATURE_COMPLETE_ROADMAP.md index 014b2aee..14c71d0e 100644 --- a/docs/design/FEATURE_COMPLETE_ROADMAP.md +++ b/docs/design/FEATURE_COMPLETE_ROADMAP.md @@ -535,7 +535,7 @@ Also related: [#697](https://github.com/python-caldav/caldav/issues/697) - smart - **Related issues:** [#585](https://github.com/python-caldav/caldav/issues/585), [#482](https://github.com/python-caldav/caldav/issues/482), [#128](https://github.com/python-caldav/caldav/issues/128), [#619](https://github.com/python-caldav/caldav/issues/619), [#515](https://github.com/python-caldav/caldav/issues/515) **Tasks:** -- [ ] Remove old incompatibility flags ([#585](https://github.com/python-caldav/caldav/issues/585)) +- [x] Remove old incompatibility flags ([#585](https://github.com/python-caldav/caldav/issues/585)) - [ ] Obsolete `get_duration`, `get_due`, `get_dtend` ([#482](https://github.com/python-caldav/caldav/issues/482)) - [ ] v4.0: remove everything that raises a `DeprecationWarning`, and add the warning to methods deprecated without one ([#619](https://github.com/python-caldav/caldav/issues/619)) diff --git a/docs/design/TODO_COMPATIBILITY_HINTS.md b/docs/design/TODO_COMPATIBILITY_HINTS.md index 5928ddd3..1fdcd8f5 100644 --- a/docs/design/TODO_COMPATIBILITY_HINTS.md +++ b/docs/design/TODO_COMPATIBILITY_HINTS.md @@ -44,26 +44,13 @@ and once in the post-loop block (line 606). The second call (line 606) uses the Needs careful review; may be an off-by-one in the loop termination logic, or the two conditions are truly independent and the second call is simply redundant. -## 3. `_old_flags` — in-progress migration, clear removal path +## 3. `_old_flags` (fixed) -**Location**: `FeatureSet.__init__`, `FeatureSet.copy_feature_set`, and ~12 server config -dicts in `compatibility_hints.py` - -`_old_flags` is explicitly marked `## TODO: remove this when it can be removed`. It is -a shim that carries forward the legacy flat-list quirk system while server configs are -being migrated to the new dotted-feature style. - -`test_caldav.py:1006` still reads `self.caldav.features._old_flags` to validate that old -flags are known strings. The test at line 1015-1016 confirms all flags are present in -`incompatibility_description`. - -**Fix**: For each server dict that still has `'old_flags': [...]`, translate the listed -flags into equivalent new-style features and remove the `old_flags` key. Once all server -dicts are migrated: -- Remove the `if feature == 'old_flags':` special-case in `copy_feature_set` -- Remove `self._old_flags = []` from `__init__` -- Remove the `_old_flags` copy in the copy-constructor branch -- Remove the validation in `test_caldav.py` +The last old-style flags are gone (https://github.com/python-caldav/caldav/issues/585): +the two still set by a server profile became `save-load.todo.uncomplete` and +`search.time-range.todo.no-dtstart.no-due`, each probed by caldav-server-tester, and the +rest were set by no profile. `incompatibility_description`, the `old_flags` key and the +test helpers reading them are removed. ## 4. `feature_tree` / `_dots_to_tree` — internal detail that can be simplified @@ -104,8 +91,7 @@ return ( The `enable`, `value` and `observed` slots exist to correctly compare `client-feature` and `server-observation` nodes during collapse. In practice, collapse is only ever invoked -on server-feature nodes (the only type that appears in the server config dicts, aside -from `old_flags`). The extra slots add noise without effect. +on server-feature nodes (the only type that appears in the server config dicts). The extra slots add noise without effect. **Fix**: Simplify to `return feature_dict.get('support')` once the broader type-system simplification is settled. @@ -150,5 +136,5 @@ want a second look at the same time. ## Ordering / dependencies -Items 3 (old_flags) and 6 (rename) are independent and safe to do first. Items 1, 2, +Items 3 (old_flags) and 6 (rename) are done. Items 1, 2, 4, 5 are interrelated around the type-system simplification and are best tackled together. diff --git a/tests/test_async_integration.py b/tests/test_async_integration.py index 404b3a27..ca23923b 100644 --- a/tests/test_async_integration.py +++ b/tests/test_async_integration.py @@ -192,9 +192,6 @@ def skip_unless_support(self, feature): msg = self._features.find_feature(feature).get("description", feature) pytest.skip("Test skipped due to server incompatibility issue: " + msg) - def check_compatibility_flag(self, flag: str) -> bool: - return flag in getattr(self._features, "_old_flags", []) - @pytest.fixture(scope="class") @classmethod def test_server(cls) -> TestServer: @@ -1740,13 +1737,10 @@ async def test_todo_datesearch(self, async_task_list: Any) -> None: foo = 5 if not self.is_supported("search.recurrences.includes-implicit.todo"): foo -= 1 - if not self.is_supported( - "search.time-range.todo.no-dtstart" - ) or self.check_compatibility_flag( - "vtodo_datesearch_nodtstart_task_is_skipped_in_closed_date_range" - ): - foo -= 2 - elif self.check_compatibility_flag("vtodo_datesearch_notime_task_is_skipped"): + if not self.is_supported("search.time-range.todo.no-dtstart"): + foo -= 1 + ## follows no-dtstart unless set + if not self.is_supported("search.time-range.todo.no-dtstart.no-due"): foo -= 1 if implicit_fragile: @@ -1779,14 +1773,9 @@ async def test_create_journal_list_and_journal_entry(self, async_journal_list: A # ==================== Group E – Properties & Meta ==================== - def _skip_on_compatibility_flag(self, flag: str) -> None: - if self.check_compatibility_flag(flag): - pytest.skip(f"Test skipped due to compatibility flag: {flag}") - @pytest.mark.asyncio async def test_support(self, async_client: Any) -> None: """check_dav_support / check_cdav_support / check_scheduling_support.""" - self._skip_on_compatibility_flag("dav_not_supported") assert await async_client.check_dav_support() assert await async_client.check_cdav_support() if self.is_supported("scheduling", return_type=str) != "unknown": diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 46e6950f..c3e95a9a 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -53,10 +53,7 @@ _config = load_test_server_config() rfc6638_users = _config.get("rfc6638_users", []) from caldav import Calendar, DAVObject, Event, FreeBusy, Principal, Todo -from caldav.compatibility_hints import ( - incompatibility_description, ## TEMP - should be removed in the future - write_delay, -) +from caldav.compatibility_hints import write_delay from caldav.davclient import CONNKEYS, DAVClient, DAVResponse from caldav.elements import cdav, dav, ical from caldav.lib import error @@ -1404,24 +1401,12 @@ class RepeatedFunctionalTestsBaseClass: ## TODO: move to davclient or compatibility_hints def is_supported(self, feature, return_type=bool, accept_fragile=False): """ - New-style. It will replace check_compatibility_flag. - TODO: write a better docstring """ return self.caldav.features.is_supported( feature, return_type, accept_fragile=accept_fragile ) - def check_compatibility_flag(self, flag): - ## yield an assertion error if checking for the wrong thig - assert flag in incompatibility_description - return flag in self.old_features - - def skip_on_compatibility_flag(self, flag): - if self.check_compatibility_flag(flag): - msg = incompatibility_description[flag] - pytest.skip("Test skipped due to server incompatibility issue: " + msg) - def skip_unless_support(self, feature): if not self.is_supported(feature): msg = self.caldav.features.find_feature(feature).get("description", feature) @@ -1435,19 +1420,12 @@ def setup_method(self): self.caldav = client(**self.server_params) self.caldav.__enter__() - ## Temp thing - self.old_features = self.caldav.features._old_flags - calendar_info = self.is_supported("test-calendar", dict) self.cleanup_regime = calendar_info.get("cleanup-regime", "light") if "cleanup" in self.server_params: self.cleanup_regime = self.server_params["cleanup"] - ## verify that all old flags are valid - for flag in self.old_features: - assert flag in incompatibility_description - self.testcal_id = "pythoncaldav-test" self.testcal_id2 = "pythoncaldav-test2" @@ -1469,14 +1447,7 @@ def setup_method(self): ## Instance-level wrap (like rate-limit), torn down with the client. self.caldav.request = _write_delay_decorator(self.caldav.request, t=delay) - if False and self.check_compatibility_flag("no-current-user-principal"): - self.principal = Principal(client=self.caldav, url=self.server_params["principal_url"]) - else: - self.principal = self.caldav.principal() - - # if self.check_compatibility_flag('delete_calendar_on_startup'): - # for x in self._fixCalendar().search(): - # x.delete() + self.principal = self.caldav.principal() self._cleanup("pre") @@ -1588,12 +1559,6 @@ def _cleanup(self, mode=None): def _teardownCalendar(self, name=None, cal_id=None): try: cal = self.principal.calendar(name=name, cal_id=cal_id) - if self.check_compatibility_flag("sticky_events"): - for goo in cal.objects(): - try: - goo.delete() - except: - pass cal.delete() except: pass @@ -1761,7 +1726,6 @@ def testSupport(self): """ Test the check_*_support methods """ - self.skip_on_compatibility_flag("dav_not_supported") assert self.caldav.check_dav_support() assert self.caldav.check_cdav_support() if self.is_supported("scheduling", str) != "unknown": @@ -2821,16 +2785,14 @@ def testLoadEvent(self): assert c2 is not None e1_ = c1.add_event(near_now_ics(ev1)) - if not self.check_compatibility_flag("event_by_url_is_broken"): - e1_.load() + e1_.load() e1 = c1.get_events()[0] - if not self.check_compatibility_flag("event_by_url_is_broken"): - ## e1 came from a search and may carry a different (canonical) URL - ## than the PUT URL on servers that don't preserve it (e.g. OX); see - ## save-load.stable-url. - if self.is_supported("save-load.stable-url"): - assert e1.url == e1_.url - e1.load() + ## e1 came from a search and may carry a different (canonical) URL + ## than the PUT URL on servers that don't preserve it (e.g. OX); see + ## save-load.stable-url. + if self.is_supported("save-load.stable-url"): + assert e1.url == e1_.url + e1.load() if self.cleanup_regime == "post": self._teardownCalendar(cal_id=self.testcal_id) self._teardownCalendar(cal_id=self.testcal_id2) @@ -2867,13 +2829,12 @@ def testCopyEvent(self): ## which shares the id with the event in c1? e1_in_c2.vobject_instance.vevent.summary.value = "asdf" e1_in_c2.save() - if not self.check_compatibility_flag("event_by_url_is_broken"): - e1.load() - ## should e1.summary be 'asdf' or 'Bastille Day Party'? I do - ## not know, but all implementations I've tested will treat - ## the copy in the other calendar as a distinct entity, even - ## if the uid is the same. - assert e1.vobject_instance.vevent.summary.value == "Bastille Day Party" + e1.load() + ## should e1.summary be 'asdf' or 'Bastille Day Party'? I do + ## not know, but all implementations I've tested will treat + ## the copy in the other calendar as a distinct entity, even + ## if the uid is the same. + assert e1.vobject_instance.vevent.summary.value == "Bastille Day Party" assert c2.get_events()[0].vobject_instance.vevent.uid == e1.vobject_instance.vevent.uid ## Duplicate the event in the same calendar, with same uid - @@ -2950,8 +2911,7 @@ def testSearchEvent(self): start=datetime(2006, 7, 13, 13, 0), end=datetime(2006, 7, 15, 13, 0), ) - if not self.check_compatibility_flag("fastmail_buggy_noexpand_date_search"): - assert len(some_events) == 1 + assert len(some_events) == 1 ## Search for misc text fields ## UID is a special case, supported by almost all servers @@ -3275,8 +3235,7 @@ def testSearchTodos(self): assert not t3.is_pending() ## Test that uncomplete works - ## (except for GMX ... their server is weird) - self.skip_on_compatibility_flag("vtodo-cannot-be-uncompleted") + self.skip_unless_support("save-load.todo.uncomplete") t5.uncomplete() some_todos = c.search(todo=True) assert len(some_todos) == 4 + pre_cnt @@ -3762,7 +3721,7 @@ def testTodoDatesearch(self): c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) # add todo-item - t1 = c.add_todo(todo) + c.add_todo(todo) t2 = c.add_todo(todo2) assert t2 is not None t3 = c.add_todo(todo3) @@ -3837,13 +3796,10 @@ def testTodoDatesearch(self): ) if not self.is_supported("search.recurrences.includes-implicit.todo"): foo -= 1 ## t6 will not be returned - if not self.is_supported( - "search.time-range.todo.no-dtstart" - ) or self.check_compatibility_flag( - "vtodo_datesearch_nodtstart_task_is_skipped_in_closed_date_range" - ): - foo -= 2 ## t1 and t4 not returned - elif self.check_compatibility_flag("vtodo_datesearch_notime_task_is_skipped"): + if not self.is_supported("search.time-range.todo.no-dtstart"): + foo -= 1 ## t1 not returned + ## follows no-dtstart unless set, so t4 goes too where t1 does + if not self.is_supported("search.time-range.todo.no-dtstart.no-due"): foo -= 1 ## t4 not returned if implicit_todo_fragile: assert len(todos1) in (foo, foo + 1) @@ -3889,8 +3845,8 @@ def testTodoDatesearch(self): ) ## * t6 should be returned, as it's a yearly task spanning over 2025 - ## * t1 should probably be returned, as it has no due date set and hence - ## has an infinite duration. + ## * t1 should not be returned, as it has no dtstart and is due in 2007 + ## (RFC4791 section 9.9) ## * t4 should probably be returned, as it has no dtstart nor due and ## hence is also considered to span over infinite time urls_found = [x.url for x in todos1] @@ -3899,12 +3855,8 @@ def testTodoDatesearch(self): urls_found = set(urls_found) if self.is_supported("search.recurrences.includes-implicit.todo", accept_fragile=True): urls_found.discard(t6.url) - if self.is_supported( - "search.time-range.todo.no-dtstart" - ) and not self.check_compatibility_flag("vtodo_datesearch_notime_task_is_skipped"): + if self.is_supported("search.time-range.todo.no-dtstart.no-due"): urls_found.discard(t4.url) - if self.check_compatibility_flag("vtodo_no_due_infinite_duration"): - urls_found.discard(t1.url) ## everything should be popped from urls_found by now assert len(urls_found) == 0 @@ -4093,9 +4045,8 @@ def testTodoCompletion(self): t2.delete() # ... the deleted one is gone ... - if not self.check_compatibility_flag("event_by_url_is_broken"): - todos = c.get_todos(include_completed=True) - assert len(todos) == 2 + todos = c.get_todos(include_completed=True) + assert len(todos) == 2 # date search should not include completed events ... hum. # TODO, fixme. @@ -4385,8 +4336,7 @@ def testCreateOverwriteDeleteEvent(self): assert e1.url is not None if todo_ok: assert t1.url is not None - if not self.check_compatibility_flag("event_by_url_is_broken"): - assert c.event_by_url(e1.url).url == e1.url + assert c.event_by_url(e1.url).url == e1.url ## get_event_by_uid may return a different (canonical) URL than the PUT ## URL on servers that don't preserve it (e.g. OX); see save-load.stable-url e_by_uid = c.get_event_by_uid(e1.id) @@ -4426,9 +4376,8 @@ def testCreateOverwriteDeleteEvent(self): ) t2.save(no_create=no_create) - if not self.check_compatibility_flag("event_by_url_is_broken"): - e3 = c.event_by_url(e1.url) - assert e3.vobject_instance.vevent.summary.value == "Bastille Day Party!" + e3 = c.event_by_url(e1.url) + assert e3.vobject_instance.vevent.summary.value == "Bastille Day Party!" ## "no_overwrite" should throw a ConsistencyError. with pytest.raises(error.ConsistencyError): @@ -4451,9 +4400,8 @@ def testCreateOverwriteDeleteEvent(self): ): with pytest.raises(self._notFound()): c.event_by_url(e2.url) - if not self.check_compatibility_flag("event_by_url_is_broken"): - with pytest.raises(error.NotFoundError): - c.get_event_by_uid("20010712T182145Z-123401@example.com") + with pytest.raises(error.NotFoundError): + c.get_event_by_uid("20010712T182145Z-123401@example.com") @pytest.mark.filterwarnings("ignore:use `calendar.search:DeprecationWarning") def testDateSearchAndFreeBusy(self): @@ -4640,16 +4588,10 @@ def testRecurringDateSearch(self): ## resultset should be one vcalendar with two events. assert len(r1) == 1 assert "RRULE" not in r1[0].data - if self.check_compatibility_flag("robur_rrule_freq_yearly_expands_monthly"): - assert r1[0].data.count("END:VEVENT") == 13 - else: - assert r1[0].data.count("END:VEVENT") == 2 + assert r1[0].data.count("END:VEVENT") == 2 ## However, the new search method will by default split it into - ## two events. Or 13, in the case of robur - if self.check_compatibility_flag("robur_rrule_freq_yearly_expands_monthly"): - assert len(r2) == 13 - else: - assert len(r2) == 2 + ## two events. + assert len(r2) == 2 assert "RRULE" not in r2[0].data assert "RRULE" not in r2[1].data assert r2[0].data.count("END:VEVENT") == 1