From 2e1577eb87e5f7890b5cafa6480308664408728a Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 22 Jun 2025 09:57:14 +0200 Subject: [PATCH 1/2] Remove vobject from the official dependency list. Fixes https://github.com/python-caldav/caldav/issues/477 Piggybacking in some documetation fixes, CHANGELOG, etc, preparing for release 2.0 --- CHANGELOG.md | 24 +++++++++++++++++++++--- RELEASE-HOWTO.md | 1 + caldav/calendarobjectresource.py | 32 ++++++++++++++++++++++---------- caldav/collection.py | 6 ------ caldav/davclient.py | 2 +- pyproject.toml | 2 +- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f942c0c9..6f781440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,20 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ## [2.0.0] - [Unreleased] +Here are the most important changes in 2.0: + +* Version 2.0 drops support for old python versions and replaces requests 2.x with niquests 3.x, a fork of requests. +* Major overhaul of the documentation +* Support for reading configuration from a config file or environmental variables - I didn't consider that to be within the scope of the caldav library, but why not - why should every application reinvent some configuration file format, and if an end-user have several applications based on python-caldav, why should he need to configure the caldav credentials explicitly for each of them? +* New method `davclient.principals()` to search for other principals on the server - and from there it's possible to do calendar searches and probe what calendars one have access to. If the server will allow it. + ### Deprecated +* `calendar.date_search` - use `calendar.search` instead. (this one has been deprecated for a while, but only with info-logging). This is almost a drop-in replacement, except for two caveats: + * `date_search` does by default to recurrence-expansion when doing searches on closed time ranges. The default value is `False` in search (this gives better consistency - no surprise differences when changing between open-ended and closed-ended searches, but it's recommended to use `expand=True` when possible). + * In `calendar.search`, `split_expanded` is set to `True`. This may matter if you have any special code for handling recurrences in your code. If not, probably the recurrences that used to be hidden will now be visible in your search results. +* I introduced the possibility to set `expand='server'` and `expand='client'` in 1.x to force through expansion either at the server side or client side (and the default was to try server side with fallback to client side). The four possible values "`True`/`False`/`client`/`server`" does not look that good in my opinion so the two latter is now deprecated, a new parameter `server_expand=True` will force server-side expansion now (see also the Changes section) * The `event.instance` property currently yields a vobject. For quite many years people have asked for the python vobject library to be replaced with the python icalendar objects, but I haven't been able to do that due to backward compatibility. In version 2.0 deprecation warnings will be given whenever someone uses the `event.instance` property. In 3.0, perhaps `event.instance` will yield a `icalendar` instance. Old test code has been updated to use `.vobject_instance` instead of `.instance`. -* `calendar.date_search` - use `calendar.search` instead. (this one has been deprecated for a while, but only with info-logging) * `davclient.auto_conn` that was introduced just some days ago has already been renamed to `davclient.get_davclient`. ### Added @@ -24,7 +34,7 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 * Improved tests (but no test for inheritance yet). * Documentation, linked up from the reference section of the doc. * It's allowable with a yaml config file, but the yaml module is not included in the dependencies yet ... so late imports as for now, and the import is wrapped in a try/except-block -* New method `davclient.principals()` will return all principals on server - if server permits. It can also do server-side search for a principal with a given user name - if server permits +* New method `davclient.principals()` will return all principals on server - if server permits. It can also do server-side search for a principal with a given user name - if server permits - https://github.com/python-caldav/caldav/pull/514 / https://github.com/python-caldav/caldav/issues/131 * `todo.is_pending` returns a bool. This was an internal method, but is now promoted to a public method. Arguably, it belongs to icalendar library and not here. ### Documentation and examples @@ -40,7 +50,8 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ### Changed * The request library has been in a feature freeze for ages and may seem like a dead end. There exists a fork of the project niquests, we're migrating to that one. This means nothing except for one additional dependency. (httpx was also considered, but it's not a drop-in replacement for the requests library, and it's a risk that such a change will break compatibility with various other servers - see https://github.com/python-caldav/caldav/issues/457 for details). Work by @ArtemIsmagilov, https://github.com/python-caldav/caldav/pull/455. -* Search has a new parameter server_expand, which defaults to False. Earlier it would default to True if expand was set. This change makes `search(expand=True, ...)` more consistent regardless of which server is in use, but it breaks bug-backward-compatibility with 1.x. +* Expanded date searches (using either `event.search(..., expand=True)` or the deprecated `event.date_search`) will now by default do a client-side expand. This gives better consistency and probably improved performance, but makes 2.0 bug-incompatible with 1.x. +* To force server-side expansion, a new parameter server_expand can be used ### Removed @@ -50,6 +61,13 @@ If you disagree with any of this, please raise an issue and I'll consider if it' * Dependency on the requests library. * The `calendar.build_date_search_query` was ripped out. (it was deprecated for a while, but only with info-logging - however, this was an obscure internal method, probably not used by anyone?) +### Changes in test framework + +* Proxy test has been rewritten. https://github.com/python-caldav/caldav/issues/462 / https://github.com/python-caldav/caldav/pull/514 +* Some more work done on improving test coverage +* Fixed a test issue that would break arbitrarily doe to clock changes during the test run - https://github.com/python-caldav/caldav/issues/380 / https://github.com/python-caldav/caldav/pull/520 +* Added test code for some observed problem that I couldn't reproduce - https://github.com/python-caldav/caldav/issues/397 - https://github.com/python-caldav/caldav/pull/521 + ## [1.6.0] - 2025-05-30 This will be the last minor release before 2.0. The scheduling support has been fixed up a bit, and saving a single recurrence does what it should do, rather than messing up the whole series. diff --git a/RELEASE-HOWTO.md b/RELEASE-HOWTO.md index f690c627..882ab28a 100644 --- a/RELEASE-HOWTO.md +++ b/RELEASE-HOWTO.md @@ -8,6 +8,7 @@ I have no clue on the proper procedures for doing releases, and I keep on doing * Go through changes since last release and compare it with the `CHANGELOG.md`. Any change should be logged. * Run tests towards as many servers as possible + * Use the `PYTHON_CALDAV_DEBUGMODE=pdb` environment variable! Should do some research if we hit any "soft asserts" or "weirdness". * Do research on breakages. If the test breaks also for the previous release of the caldav library, then it's likely to be due to some regression on the server side. For patch-level releases we don't care about such breakages, for minor-level releases we should try to work around problems * It's proper to document somewhere (TODO: where? how?) what servers have been tested * Does any of the changes require documentation to be rewritten? The documentation should ideally be in sync with the code upon release time. diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index 8b381ea5..aa03c500 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -31,11 +31,9 @@ from urllib.parse import unquote import icalendar -import vobject from dateutil.rrule import rrulestr from icalendar import vCalAddress from icalendar import vText -from vobject.base import VBase try: from typing import ClassVar, Optional, Union, Type @@ -150,6 +148,7 @@ def set_end(self, end, move_dtstart=False): please put caldav<3.0 in the requirements. """ i = self.icalendar_component + ## TODO: are those lines useful for anything? if hasattr(end, "tzinfo") and not end.tzinfo: end = end.astimezone(timezone.utc) duration = self.get_duration() @@ -249,8 +248,9 @@ def expand_rrule( and occurrence.get("STATUS") in ("COMPLETED", "CANCELLED") ): continue + ## TODO: If there are no reports of missing RECURRENCE-ID until 2027, + ## the if-statement below may be deleted error.assert_("RECURRENCE-ID" in occurrence) - ## TODO: do we need this? if "RECURRENCE-ID" not in occurrence: occurrence.add("RECURRENCE-ID", occurrence.get("DTSTART").dt) calendar.add_component(occurrence) @@ -703,6 +703,7 @@ def load_by_multiget(self) -> Self: return self ## TODO: self.id should either always be available or never + ## TODO: run this logic on load, to ensure `self.id` is set after loading def _find_id_path(self, id=None, path=None) -> None: """ With CalDAV, every object has a URL. With icalendar, every object @@ -715,7 +716,7 @@ def _find_id_path(self, id=None, path=None) -> None: 2) if ID is not given, but the path is given, generate the ID from the path 3) If neither ID nor path is given, use the uuid method to generate an - ID (TODO: recommendation is to concat some timestamp, serial or + ID (TODO: recommendation in the RFC is to concat some timestamp, serial or random number and a domain) 4) if no path is given, generate the URL from the ID """ @@ -732,6 +733,7 @@ def _find_id_path(self, id=None, path=None) -> None: id = re.search("(/|^)([^/]*).ics", str(path)).group(2) if id is None: id = str(uuid.uuid1()) + i.pop("UID", None) i.add("UID", id) @@ -756,6 +758,11 @@ def _put(self, retry_on_failure=True): if r.status == 302: path = [x[1] for x in r.headers if x[0] == "location"][0] elif r.status not in (204, 201): + if retry_on_failure: + try: + import vobject + except ImportError: + retry_on_failure = False if retry_on_failure: ## This looks like a noop, but the object may be "cleaned". ## See https://github.com/python-caldav/caldav/issues/43 @@ -1078,13 +1085,18 @@ def _get_wire_data(self): doc="vCal representation of the object in wire format (UTF-8, CRLN)", ) - def _set_vobject_instance(self, inst: vobject.base.Component): + def _set_vobject_instance(self, inst: 'vobject.base.Component'): self._vobject_instance = inst self._data = None self._icalendar_instance = None return self - def _get_vobject_instance(self) -> Optional[vobject.base.Component]: + def _get_vobject_instance(self) -> Optional['vobject.base.Component']: + try: + import vobject + except ImportError: + logging.critical("A vobject instance has been requested, but the vobject library is not installed (vobject is no longer an official dependency in 2.0)") + return None if not self._vobject_instance: if self._get_data() is None: return None @@ -1102,7 +1114,7 @@ def _get_vobject_instance(self) -> Optional[vobject.base.Component]: ## event.instance has always yielded a vobject, but will probably yield an icalendar_instance ## in version 3.0! - def _get_deprecated_vobject_instance(self) -> Optional[vobject.base.Component]: + def _get_deprecated_vobject_instance(self) -> Optional['vobject.base.Component']: warnings.warn( "use event.vobject_instance or event.icalendar_instance", DeprecationWarning, @@ -1110,7 +1122,7 @@ def _get_deprecated_vobject_instance(self) -> Optional[vobject.base.Component]: ) return self._get_vobject_instance() - def _set_deprecated_vobject_instance(self, inst: vobject.base.Component): + def _set_deprecated_vobject_instance(self, inst: 'vobject.base.Component'): warnings.warn( "use event.vobject_instance or event.icalendar_instance", DeprecationWarning, @@ -1118,13 +1130,13 @@ def _set_deprecated_vobject_instance(self, inst: vobject.base.Component): ) return self._get_vobject_instance(inst) - vobject_instance: VBase = property( + vobject_instance: 'vobject.base.VBase' = property( _get_vobject_instance, _set_vobject_instance, doc="vobject instance of the object", ) - instance: VBase = property( + instance: 'vobject.base.VBase' = property( _get_deprecated_vobject_instance, _set_deprecated_vobject_instance, doc="vobject instance of the object (DEPRECATED! This will yield an icalendar instance in caldav 3.0)", diff --git a/caldav/collection.py b/caldav/collection.py index 9f4d67b8..01f27f4b 100644 --- a/caldav/collection.py +++ b/caldav/collection.py @@ -689,12 +689,6 @@ def date_search( else: comp_class = None - ## xandikos now yields a 5xx-error when trying to pass - ## expand=True, after I prodded the developer that it doesn't - ## work. By now there is some workaround in the test code to - ## avoid sending expand=True to xandikos, but perhaps we - ## should run a try-except-retry here with expand=False in the - ## retry, and warnings logged ... or perhaps not. objects = self.search( start=start, end=end, diff --git a/caldav/davclient.py b/caldav/davclient.py index 9bfacb04..161c39f8 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -472,7 +472,7 @@ def __init__( username and password may be omitted. THe niquest library will honor standard proxy environmental variables like - HTTP_PROXY, HTTPS_PROXY and ALL_PROXY. + HTTP_PROXY, HTTPS_PROXY and ALL_PROXY. See https://niquests.readthedocs.io/en/latest/user/advanced.html#proxies """ headers = headers or {} diff --git a/pyproject.toml b/pyproject.toml index 818e3a96..dbbaf35a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ classifiers = [ ] dependencies = [ - "vobject", "lxml", "niquests", "recurring-ical-events>=2.0.0", @@ -49,6 +48,7 @@ Changelog = "https://github.com/python-caldav/caldav/blob/master/CHANGELOG.md" [project.optional-dependencies] test = [ + "vobject", "pytest", "coverage", "manuel", From 6540b5063e1d6fdcb8fc08980d8ae9a6b964b52a Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 22 Jun 2025 10:19:05 +0200 Subject: [PATCH 2/2] style --- caldav/calendarobjectresource.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/caldav/calendarobjectresource.py b/caldav/calendarobjectresource.py index aa03c500..58e71a50 100644 --- a/caldav/calendarobjectresource.py +++ b/caldav/calendarobjectresource.py @@ -1085,17 +1085,19 @@ def _get_wire_data(self): doc="vCal representation of the object in wire format (UTF-8, CRLN)", ) - def _set_vobject_instance(self, inst: 'vobject.base.Component'): + def _set_vobject_instance(self, inst: "vobject.base.Component"): self._vobject_instance = inst self._data = None self._icalendar_instance = None return self - def _get_vobject_instance(self) -> Optional['vobject.base.Component']: + def _get_vobject_instance(self) -> Optional["vobject.base.Component"]: try: import vobject except ImportError: - logging.critical("A vobject instance has been requested, but the vobject library is not installed (vobject is no longer an official dependency in 2.0)") + logging.critical( + "A vobject instance has been requested, but the vobject library is not installed (vobject is no longer an official dependency in 2.0)" + ) return None if not self._vobject_instance: if self._get_data() is None: @@ -1114,7 +1116,7 @@ def _get_vobject_instance(self) -> Optional['vobject.base.Component']: ## event.instance has always yielded a vobject, but will probably yield an icalendar_instance ## in version 3.0! - def _get_deprecated_vobject_instance(self) -> Optional['vobject.base.Component']: + def _get_deprecated_vobject_instance(self) -> Optional["vobject.base.Component"]: warnings.warn( "use event.vobject_instance or event.icalendar_instance", DeprecationWarning, @@ -1122,7 +1124,7 @@ def _get_deprecated_vobject_instance(self) -> Optional['vobject.base.Component'] ) return self._get_vobject_instance() - def _set_deprecated_vobject_instance(self, inst: 'vobject.base.Component'): + def _set_deprecated_vobject_instance(self, inst: "vobject.base.Component"): warnings.warn( "use event.vobject_instance or event.icalendar_instance", DeprecationWarning, @@ -1130,13 +1132,13 @@ def _set_deprecated_vobject_instance(self, inst: 'vobject.base.Component'): ) return self._get_vobject_instance(inst) - vobject_instance: 'vobject.base.VBase' = property( + vobject_instance: "vobject.base.VBase" = property( _get_vobject_instance, _set_vobject_instance, doc="vobject instance of the object", ) - instance: 'vobject.base.VBase' = property( + instance: "vobject.base.VBase" = property( _get_deprecated_vobject_instance, _set_deprecated_vobject_instance, doc="vobject instance of the object (DEPRECATED! This will yield an icalendar instance in caldav 3.0)",