From 20c16405355fb45f3e56da47acfd5f492236a318 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 13 Mar 2026 17:04:10 +0100 Subject: [PATCH 1/6] docs: introduction of conventionalcommits standard --- CHANGELOG.md | 10 ++++++++++ CONTRIBUTING.md | 27 ++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f263889..63a4ccfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ Changelogs prior to v2.0 is pruned, but was available in the v2.x releases This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), though for pre-releases PEP 440 takes precedence. +## [Unreleased] + +### Documentation + +I've decided to try to stick to the conventionalcommits standard. This is documented in CONTRIBUTING.md. We'll see how many days it takes before I forget about it ... + ## [3.0.1] - 2026-03-04 Highlights: @@ -43,6 +49,10 @@ Highlights: * The compatibility-hint key `search.comp-type-optional` has been renamed to `search.comp-type.optional` for consistency with the dotted-key naming convention used elsewhere. If you have this key set in a local server configuration, update it accordingly. +### Documentation + +Some minor improvements, including a fix for https://github.com/python-caldav/caldav/issues/635 - use canonical RFC-links. + ## [3.0.0] - 2026-03-03 Version 3.0 should be fully backward-compatible with version 2.x - but there are massive code changes in version 3.0, so if you're using the Python CalDAV client library in some sharp production environment, I would recommend to wait for two months before upgrading. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc0348d8..dad1882f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,31 @@ # Contributing -Contributions are mostly welcome. If the length of this text scares you, then I'd rather want you to skip reading and just produce a pull-request in GitHub. +Contributions are mostly welcome (but do inform about it if you've used AI or other tools). If the length of this text scares you, then I'd rather want you to skip reading and just produce a pull-request in GitHub. + +## Git commit messages + +Starting from v3.0.1, we'll stick to https://www.conventionalcommits.org/en/v1.0.0/ on the master branch. A good pull request contains one commit that follows the conventions. Otherwise the maintainer will have to rewrite the commit message. + +The types used should (as for now) be one of: + +* "revert" - a clean revert of a previous commit (should be used infrequently on the master branch) +* "feat" - a new feature added to the codebase/API. The commit should include test code, documentation and a CHANGELOG entry unless there are good reasons for procrastinating it. "feat" should not be used for new features that only affects the test framework, or changes only affectnig the documentation etc. +* "fix" - a bugfix. Again, documentation and CHANGELOG-entry should be included in the commit (notable exception: if a bug was introduced after the previous release and fixed before the next release, it does not need to be mentioned in the CHANGELOG). If the only "fix" is that some typo is fixed in some existing documentation, then we should use "docs" instead. +* "perf" - a code change in the codebase that is neither a bugfix or a feature, but intends to improve performance. +* "refactor" - a code change in the codebase that is neither a bugfix or a feature, but makes the code more readable, shorter, better or more maintainable. +* "test" - fixes, additions or improvements that only affects the test code or the test framework. The commit may include documentation. +* "docs" - changes that *only* is done to the documentation, documentation framework - this includes minor typo fixes as well as new documentation, and it includes both the user documentation under `docs/source`, other documentation files (including CHANGELOG) as well as inline comments and docstrings in the code itself. +* "other" - if nothing of the above fits + +This is not set in stone. If you feel strongly for using something else, use something else in the commit message and update this file in the same commit. + +"Imperative mood" is to be used in commit messages. + +The boundaries of breaking changes vs "non-breaking" changes [may be blurry](https://xkcd.com/1172/). In the CHANGELOG I've used the concept "potentially breaking changes" for things that most likely won't break anything for anyone. Potentially breaking changes should be marked with `!` in the commit header. Breaking changes should be marked both with `!` and `BREAKING CHANGE:` + +The conventionalcommits guide also says nothing about how to deal with security-relevant changes. Maybe it makes sense to start the commit message (after the "SECURITY: " + +As for now, we do not use the module field. If there is strong reasons for using it, then go ahead and update this file in the same commit. ## Usage of AI and other tools From 8ea2757c0f008f172a9d01bb910dde6c60aff9fa Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 13 Mar 2026 13:30:49 +0100 Subject: [PATCH 2/6] fix: Restore communication dump to sync and async request paths The debug_dump_communication / PYTHON_CALDAV_COMMDUMP feature was lost during the v3.0 refactor. Extract it into a shared _dump_communication() helper in caldav.lib.error so the logic is not duplicated between the sync (_sync_request) and async (_async_request) code paths. Fixes https://github.com/python-caldav/caldav/issues/638 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ caldav/async_davclient.py | 3 +++ caldav/davclient.py | 4 ++++ caldav/lib/error.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_caldav_unit.py | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a4ccfb..ad8dd6f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ## [Unreleased] +### Fixed + +* Communication dump (`PYTHON_CALDAV_COMMDUMP` / `debug_dump_communication`) was accidentally dropped during the v3.0 refactor. Restored, with the dump logic extracted into a shared helper so both the sync and async code paths benefit. Fixes https://github.com/python-caldav/caldav/issues/638 + ### Documentation I've decided to try to stick to the conventionalcommits standard. This is documented in CONTRIBUTING.md. We'll see how many days it takes before I forget about it ... diff --git a/caldav/async_davclient.py b/caldav/async_davclient.py index 18c82e8c..ae7b1228 100644 --- a/caldav/async_davclient.py +++ b/caldav/async_davclient.py @@ -527,6 +527,9 @@ async def _async_request( if response.status in (401, 403): self._raise_authorization_error(str(url_obj), response) + if error.debug_dump_communication: + error._dump_communication(method, url, combined_headers, body, response) + return response # ==================== HTTP Method Wrappers ==================== diff --git a/caldav/davclient.py b/caldav/davclient.py index 61578083..b60374f9 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -936,6 +936,10 @@ def _sync_request( self._raise_authorization_error(str(url_obj), r) response = DAVResponse(r, self) + + if error.debug_dump_communication: + error._dump_communication(method, url, combined_headers, body, response) + return response diff --git a/caldav/lib/error.py b/caldav/lib/error.py index 769dff6a..d1a685cf 100644 --- a/caldav/lib/error.py +++ b/caldav/lib/error.py @@ -34,6 +34,40 @@ def errmsg(r) -> str: return "%s %s\n\n%s" % (r.status, r.reason, r.raw) +def _dump_communication(method: str, url: str, combined_headers: dict, body, response) -> None: + """Write a request/response exchange to a uniquely-named temp file. + + Called when ``debug_dump_communication`` is truthy. Works for both the + sync and async code paths because it only depends on the attributes that + ``BaseDAVResponse`` exposes: ``.status``, ``.reason``, ``.headers``, + ``.tree``, and ``._raw``. + """ + import datetime + from tempfile import NamedTemporaryFile + + from lxml import etree + + from caldav.lib.python_utilities import to_wire + + with NamedTemporaryFile(prefix="caldavcomm", delete=False) as commlog: + commlog.write(b"=" * 80 + b"\n") + commlog.write(f"{datetime.datetime.now():%FT%H:%M:%S}".encode()) + commlog.write(b"\n====>\n") + commlog.write(f"{method} {url}\n".encode()) + commlog.write(b"\n".join(to_wire(f"{k}: {v}") for k, v in combined_headers.items())) + commlog.write(b"\n\n") + commlog.write(to_wire(body) or b"") + commlog.write(b"\n<====\n") + commlog.write(f"{response.status} {response.reason}\n".encode()) + commlog.write(b"\n".join(to_wire(f"{k}: {v}") for k, v in response.headers.items())) + commlog.write(b"\n\n") + if response.tree is not None: + commlog.write(to_wire(etree.tostring(response.tree, pretty_print=True))) + else: + commlog.write(to_wire(response._raw) or b"") + commlog.write(b"\n") + + def weirdness(*reasons): from caldav.lib.debug import xmlstring diff --git a/tests/test_caldav_unit.py b/tests/test_caldav_unit.py index 2321789f..ade2699c 100755 --- a/tests/test_caldav_unit.py +++ b/tests/test_caldav_unit.py @@ -415,6 +415,40 @@ def testNonValidXMLNoContentLength(self, mocked): with pytest.raises(lxml.etree.XMLSyntaxError): client.request("/") + @mock.patch("caldav.davclient.requests.Session.request") + def testCommunicationDump(self, mocked): + """ + ref https://github.com/python-caldav/caldav/issues/638 + When PYTHON_CALDAV_COMMDUMP (or debug_dump_communication) is set, + request/response data should be written to a temp file. + """ + import glob + import os + import tempfile + + mocked().status_code = 200 + mocked().headers = {"Content-Type": "text/plain"} + mocked().content = b"" + mocked().reason = "OK" + mocked().reason_phrase = None + + from caldav.lib import error as caldav_error + + old_value = caldav_error.debug_dump_communication + caldav_error.debug_dump_communication = True + try: + client = DAVClient(url="http://test.example.com/") + before = set(glob.glob(os.path.join(tempfile.gettempdir(), "caldavcomm*"))) + client.request("/") + after = set(glob.glob(os.path.join(tempfile.gettempdir(), "caldavcomm*"))) + new_files = after - before + assert len(new_files) == 1 + content = open(list(new_files)[0], "rb").read() + assert b"GET /" in content + assert b"200 OK" in content + finally: + caldav_error.debug_dump_communication = old_value + def testPathWithEscapedCharacters(self): xml = b""" From bfa33ae7114973e85ba43fffd1eca266af907715 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 13 Mar 2026 16:57:48 +0100 Subject: [PATCH 3/6] fix: Handle full XML in _search_with_comptypes instead of raising When search() is called with a full calendar-query XML and the server does not support search.comp-type.optional, the code raised NotImplementedError. Fall back to a single REPORT with the XML as-is (comp_class auto-detected from response data) for both the sync and async code paths. Fixes https://github.com/python-caldav/caldav/issues/637 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + caldav/search.py | 16 ++++++++++------ tests/test_search.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad8dd6f6..9ad77bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ### Fixed * Communication dump (`PYTHON_CALDAV_COMMDUMP` / `debug_dump_communication`) was accidentally dropped during the v3.0 refactor. Restored, with the dump logic extracted into a shared helper so both the sync and async code paths benefit. Fixes https://github.com/python-caldav/caldav/issues/638 +* `search()` raised `NotImplementedError` when a full calendar-query XML was passed and the server does not support `search.comp-type.optional` (e.g. DavMail). Falls back to a single REPORT with the XML as-is. Fixes https://github.com/python-caldav/caldav/issues/637 ### Documentation diff --git a/caldav/search.py b/caldav/search.py index 0548cfb6..b1a639e7 100644 --- a/caldav/search.py +++ b/caldav/search.py @@ -688,9 +688,12 @@ def _search_with_comptypes( Internal method - does three searches, one for each comp class (event, journal, todo). """ if xml and (isinstance(xml, str) or "calendar-query" in xml.tag): - raise NotImplementedError( - "full xml given, and it has to be patched to include comp_type" - ) + # Full XML provided – cannot inject a comp-type filter into it. + # Fall back to a single REPORT request with the XML as-is; the + # server is expected to return whatever comp-types the query + # matches, and comp_class detection falls back to auto-detect. + _, objects = calendar._request_report_build_resultlist(xml, None, props) + return self.sort(objects) objects = [] assert self.event is None and self.todo is None and self.journal is None @@ -769,9 +772,10 @@ async def _async_search_with_comptypes( Internal async method - does three searches, one for each comp class. """ if xml and (isinstance(xml, str) or "calendar-query" in xml.tag): - raise NotImplementedError( - "full xml given, and it has to be patched to include comp_type" - ) + # Full XML provided – cannot inject a comp-type filter into it. + # Fall back to a single REPORT request with the XML as-is. + _, objects = await calendar._request_report_build_resultlist(xml, None, props) + return self.sort(objects) objects: list[AsyncCalendarObjectResource] = [] assert self.event is None and self.todo is None and self.journal is None diff --git a/tests/test_search.py b/tests/test_search.py index 2d9dfdaa..83bb6f88 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -829,3 +829,41 @@ def mock_is_supported(feat, type_=bool): # Only the recurring event without DTEND should be returned assert len(result) == 1 assert result[0].icalendar_component.get("DTEND") is None + + +class TestSearchWithCompTypesFullXML: + """Regression tests for issue #637. + + When search() is called with a full calendar-query XML and the server does + not support search.comp-type.optional, the code used to raise + NotImplementedError. It should instead do a single REPORT request with + the XML as-is. + """ + + def test_search_full_xml_string_no_comp_type_optional( + self, mock_client: DAVClient, mock_url: str + ) -> None: + """Passing a full XML string to search() must not raise NotImplementedError + when the server does not support search.comp-type.optional.""" + + def mock_is_supported(feat, type_=bool): + if feat == "search.comp-type.optional": + return False + if type_ == str: + return "full" + return True + + mock_client.features.is_supported = mock.Mock(side_effect=mock_is_supported) + mock_client.features.backward_compatibility_mode = False + + event = Event(client=mock_client, url=mock_url, data=SIMPLE_EVENT) + calendar = mock.Mock() + calendar.client = mock_client + calendar._request_report_build_resultlist.return_value = (mock.Mock(), [event]) + + full_xml = "" + searcher = CalDAVSearcher() + result = searcher.search(calendar, xml=full_xml) + + assert result == [event] + calendar._request_report_build_resultlist.assert_called_once_with(full_xml, None, None) From e10104b6e7389391657e30d54c484c99c0502595 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 13 Mar 2026 23:26:37 +0100 Subject: [PATCH 4/6] other: Add search.is-not-defined.class feature Registers the CLASS property variant of the is-not-defined search feature, matching the existing .category and .dtend sub-features. Required by caldav-server-tester's CheckIsNotDefined check. Co-Authored-By: Claude Sonnet 4.6 --- caldav/compatibility_hints.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 182e9a19..af5a1bb4 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -195,6 +195,9 @@ class FeatureSet: "search.is-not-defined.dtend": { ## TODO: this should most likely be removed - it was a client bug fixed in icalendar-search 1.0.5, not a server error. (Discovered in the last minute before releasing caldav v3.0.0 - I won't touch it now) "description": "Supports searching for objects where the DTEND property is not defined (RFC4791 section 9.7.4). Some servers support is-not-defined for some properties but not DTEND" }, + "search.is-not-defined.class": { + "description": "Supports searching for objects where the CLASS property is not defined (RFC4791 section 9.7.4). Some servers support is-not-defined for CLASS but not for other properties like CATEGORIES" + }, "search.text": { "description": "Search for text attributes should work" }, From 7543b928b0fd6cbb9e2d7897b9746d6077c6d08a Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 14 Mar 2026 09:30:10 +0100 Subject: [PATCH 5/6] docs: cleaning up current-user-principal in compatibility_hints * Remove old flag no-current-principal, it was dead code. * Add server observations for missing current-user-principal * Add commit guidelines on compatibility_hints.py --- CONTRIBUTING.md | 8 +++++++- caldav/compatibility_hints.py | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dad1882f..e9c5536a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Starting from v3.0.1, we'll stick to https://www.conventionalcommits.org/en/v1.0 The types used should (as for now) be one of: -* "revert" - a clean revert of a previous commit (should be used infrequently on the master branch) +* "revert" - a clean revert of a previous commit (should be used very infrequently on the master branch) * "feat" - a new feature added to the codebase/API. The commit should include test code, documentation and a CHANGELOG entry unless there are good reasons for procrastinating it. "feat" should not be used for new features that only affects the test framework, or changes only affectnig the documentation etc. * "fix" - a bugfix. Again, documentation and CHANGELOG-entry should be included in the commit (notable exception: if a bug was introduced after the previous release and fixed before the next release, it does not need to be mentioned in the CHANGELOG). If the only "fix" is that some typo is fixed in some existing documentation, then we should use "docs" instead. * "perf" - a code change in the codebase that is neither a bugfix or a feature, but intends to improve performance. @@ -17,6 +17,12 @@ The types used should (as for now) be one of: * "docs" - changes that *only* is done to the documentation, documentation framework - this includes minor typo fixes as well as new documentation, and it includes both the user documentation under `docs/source`, other documentation files (including CHANGELOG) as well as inline comments and docstrings in the code itself. * "other" - if nothing of the above fits +The `compatibility_hints.py` has been moved from the test directory to the codebase not so very long ago. Some special rules here: + +* Adjusting the feature set for some calendar server? Check if there exists some workarounds etc in the code for said feature, if so, then it should be considered a fix or a feature. Perhaps even a breaking change. Otherwise, use `test: ...`. (because it is relevant for the compatibility test, if nothing else). +* Adding a new feature hint? Ensure it's covered by the caldav-server-tester. Since we have a compatibility test, it will be relevant for the test - so use `test: (...)`. It should be covered by the caldav-serveer-tester, so refer to some issue or pull request for the caldav-server-tester in the commit message. +* Changing some descriptions? That goes as `docs: ...` even if it's actually changing a variable in the code. + This is not set in stone. If you feel strongly for using something else, use something else in the commit message and update this file in the same commit. "Imperative mood" is to be used in commit messages. diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index af5a1bb4..f89e163b 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -78,7 +78,7 @@ class FeatureSet: } }, "get-current-user-principal": { - "description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the DAV standard"}, + "description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the DAV standard. Possibly observed missing on mail.ru,DavMail gateway and it is possible to configure the support in some sabre-based servers"}, "get-current-user-principal.has-calendar": { "type": "server-observation", "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)"}, @@ -707,9 +707,6 @@ def dotted_feature_set_list(self, compact=False): ## * Perhaps some more readable format should be considered (yaml?). ## * Consider how to get this into the documentation incompatibility_description = { - 'no_current-user-principal': - """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""", - 'no_scheduling': """RFC6833 is not supported""", From 32acdd18b59bd570ad1a613d98f71aba9dee1c64 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 14 Mar 2026 10:41:55 +0100 Subject: [PATCH 6/6] test: tweaking lychee setup and links --- .github/workflows/linkcheck.yml | 27 ++++++++++++++++++++++----- .pre-commit-config.yaml | 3 +-- CHANGELOG.md | 2 +- README.md | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 5d4adac1..061798b9 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -1,18 +1,35 @@ name: Link check -on: [push, pull_request] +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: "03 22 * * *" jobs: linkcheck: runs-on: ubuntu-latest + permissions: + issues: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check links with Lychee + id: lychee uses: lycheeverse/lychee-action@v2 with: - fail: true + fail: false args: >- + --root-dir "$(pwd)" --timeout 20 --max-retries 3 - '**/*.md' - '**/*.rst' + --cache + --max-cache-age 14d + . + - name: Create Issue From File + if: steps.lychee.outputs.exit_code != 0 + uses: peter-evans/create-issue-from-file@v5 + with: + title: Link Checker Report + content-filepath: ./lychee/out.md + labels: report, automated issue diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87116529..aa319a6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,5 +19,4 @@ repos: hooks: - id: lychee args: ["--no-progress", "--timeout", "10"] - types: [markdown, rst] - stages: [manual] # Run with: pre-commit run lychee --hook-stage manual + stages: [pre-push] diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad77bb1..0b5fafd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -198,7 +198,7 @@ Additionally, direct `DAVClient()` instantiation should migrate to `get_davclien ### Changed * Optimilizations on data conversions in the `CalendarObjectResource` properties (https://github.com/python-caldav/caldav/issues/613 ) -* Lazy imports (PEP 562) -- `import caldav` is now significantly faster. Heavy dependencies (lxml, niquests, icalendar) are deferred until first use. https://github.com/python-caldav/caldav/pull/621 +* Lazy imports (PEP 562) -- `import caldav` is now significantly faster. Heavy dependencies (lxml, niquests, icalendar) are deferred until first use. https://github.com/python-caldav/caldav/issues/621 * Search refactored to use generator-based Sans-I/O pattern -- `_search_impl` yields `(SearchAction, data)` tuples consumed by sync or async wrappers * Configuration system expanded: `get_connection_params()` provides unified config discovery with clear priority (explicit params > test server config > env vars > config file) * `${VAR}` and `${VAR:-default}` environment variable expansion in config values diff --git a/README.md b/README.md index 946efb28..5a101879 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # caldav -This project is a CalDAV ([RFC4791](http://www.ietf.org/rfc/rfc4791.txt)) client library for Python. +This project is a CalDAV ([RFC4791](https://www.ietf.org/rfc/rfc4791.txt)) client library for Python. Features: