Skip to content

Commit 80df419

Browse files
tobixenclaude
andcommitted
fix: bump caldav>=1.5.0, drop Python 3.9, add 3.14, fix xandikos tests
The journal test (_select(journal=True)) was silently broken in CI: caldav>=1.5.0 is the first release with journal search support (May 2025). The bug was in caldav, not xandikos — xandikos supports VJOURNAL REPORT fine. Dropping Python 3.9 follows from the caldav>=1.5.0 requirement; caldav does not officially support 3.9 from that version onwards. Python 3.14 is added to the matrix en passant. Other changes: - Fix xandikos test for SingleUserFilesystemBackend rename (replaces the try/except workaround that was in the discarded PR #35) - Reload todo1/todo2 before _adjust_relations reset: _check_for_panic with fix_timeline modifies these objects on the server, staling ETags - Exclude xandikos.org from lychee (intermittent 502, not our fault) - Fix caldav deprecation warning in plann/lib.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ea0625e commit 80df419

5 files changed

Lines changed: 50 additions & 35 deletions

File tree

‎.github/workflows/tests.yaml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
18+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
1919

2020
steps:
2121
- name: Checkout repository
@@ -37,7 +37,6 @@ jobs:
3737
args: check .
3838

3939
- name: Check dependencies with deptry
40-
if: matrix.python-version != '3.9'
4140
run: python -m deptry .
4241

4342
- name: Run tests

‎.lychee.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ exclude = [
77
".*some\\.davical\\.server.*",
88
# External site with expired SSL certificate (not under our control)
99
".*bedework\\.org.*",
10+
# xandikos.org returns intermittent 5xx errors (not under our control)
11+
".*xandikos\\.org.*",
1012
]

‎plann/lib.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def _try(meth, kwargs, errmsg):
120120
## TODO: test this more thoroughly.
121121
## The code above is supposed to remote the `caldav_`-prefix
122122
## Stil the lines below was added to fix
123-
## https://github.com/tobixen/plann/issues/11, credits to @bergercookie
123+
## https://github.com/pycalendar/plann/issues/11, credits to @bergercookie
124124
if 'caldav_url' in conn_params:
125125
conn_params['url'] = conn_params.pop('caldav_url')
126126
if conn_params:
@@ -530,7 +530,7 @@ def _list(objs, ics=False, template="{DTSTART:?{DUE:?(date missing)?}?%F %H:%M:%
530530
## This should be a top-level thing
531531
more_info = {}
532532
if 'calendar_name' in template.template:
533-
more_info['calendar_name'] = obj.parent.name or obj.parent.get_display_name()
533+
more_info['calendar_name'] = obj.parent.get_display_name()
534534
more_info['calendar_url'] = obj.parent.url
535535
output.append(" "*indent + template.format(**obj.icalendar_component, **more_info))
536536
## Recursively add children in an indented way

‎pyproject.toml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,25 @@ authors = [{ name = "Tobias Brox", email = "t-plann@tobixen.no" }]
66
maintainers = [{ name = "Tobias Brox", email = "t-plann@tobixen.no" }]
77
license = { text = "GPL-3.0" }
88
readme = "README.md"
9-
requires-python = ">=3.9"
9+
requires-python = ">=3.10"
1010
classifiers = [
1111
"Development Status :: 4 - Beta",
1212
"Environment :: Console",
1313
"Intended Audience :: End Users/Desktop",
1414
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
1515
"Operating System :: POSIX",
1616
"Programming Language :: Python :: 3",
17-
"Programming Language :: Python :: 3.9",
1817
"Programming Language :: Python :: 3.10",
1918
"Programming Language :: Python :: 3.11",
2019
"Programming Language :: Python :: 3.12",
2120
"Programming Language :: Python :: 3.13",
21+
"Programming Language :: Python :: 3.14",
2222
"Topic :: Office/Business :: Scheduling",
2323
"Topic :: Utilities",
2424
]
2525
keywords = ["caldav", "calendar", "cli", "todo", "planning"]
2626
dependencies = [
27-
"caldav>=1.4.0",
27+
"caldav>=1.5.0",
2828
"Click",
2929
"PyYAML",
3030
"sortedcontainers",
@@ -65,7 +65,7 @@ version-file = "plann/_version.py"
6565
packages = ["plann"]
6666

6767
[tool.ruff]
68-
target-version = "py39"
68+
target-version = "py310"
6969
line-length = 120
7070

7171
[tool.ruff.lint]

‎tests/test_functional.py‎

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
import aiohttp
1515
import aiohttp.web
16+
import caldav
1617
import niquests as requests
17-
from xandikos.web import XandikosApp, XandikosBackend
18+
from xandikos.web import SingleUserFilesystemBackend, XandikosApp
1819

1920
from plann.cli import _add_journal, _add_todo, _check_for_panic, _list, _select
2021
from plann.interactive import (
@@ -24,7 +25,7 @@
2425
_mass_reprioritize,
2526
command_edit,
2627
)
27-
from plann.lib import _adjust_ical_relations, _adjust_relations, find_calendars
28+
from plann.lib import _adjust_ical_relations, _adjust_relations
2829
from plann.panic_planning import timeline_suggestion
2930
from tests.test_panic import datetime_
3031

@@ -89,7 +90,7 @@ def start_xandikos_server():
8990
serverdir.__enter__()
9091
## Most of the stuff below is cargo-cult-copied from xandikos.web.main
9192
## Later jelmer created some API that could be used for this
92-
backend = XandikosBackend(path=serverdir.name)
93+
backend = SingleUserFilesystemBackend(serverdir.name)
9394
backend._mark_as_principal("/sometestuser/")
9495
backend.create_principal("/sometestuser/", create_defaults=True)
9596
mainapp = XandikosApp(
@@ -132,7 +133,7 @@ def stop_xandikos_server(server_params):
132133
## ... but the thread may be stuck waiting for a request ...
133134
def silly_request():
134135
try:
135-
requests.get(server_params["caldav_url"])
136+
requests.get(server_params["caldav_url"]).close()
136137
except Exception:
137138
pass
138139
threading.Thread(target=silly_request).start()
@@ -162,24 +163,37 @@ def passthrough(x):
162163
def test_plann():
163164
conn_details = start_xandikos_server()
164165
try:
165-
ctx = MagicMock()
166-
ctx.obj = dict()
167-
ctx.obj['calendars'] = find_calendars(conn_details, raise_errors=True)
168-
169-
def dag(obj, reltype, observed=None):
170-
if not hasattr(obj, 'get_relatives'):
171-
obj = ctx.obj['calendars'][0].object_by_uid(obj)
172-
else:
173-
obj.load()
174-
if not observed:
175-
observed = set()
176-
ret = {}
177-
relatives = obj.get_relatives(reltypes={reltype}, fetch_objects=False)
178-
for x in relatives[reltype]:
179-
assert x not in observed
180-
observed.add(x)
181-
ret[x] = dag(x, reltype, observed)
182-
return ret
166+
with caldav.get_calendars(
167+
url=conn_details['caldav_url'],
168+
username=conn_details['caldav_user'],
169+
password=conn_details['caldav_password'],
170+
raise_errors=True,
171+
) as calendars:
172+
_run_plann_tests(calendars)
173+
174+
finally:
175+
stop_xandikos_server(conn_details)
176+
177+
178+
def _run_plann_tests(calendars):
179+
ctx = MagicMock()
180+
ctx.obj = dict()
181+
ctx.obj['calendars'] = list(calendars)
182+
183+
def dag(obj, reltype, observed=None):
184+
if not hasattr(obj, 'get_relatives'):
185+
obj = ctx.obj['calendars'][0].object_by_uid(obj)
186+
else:
187+
obj.load()
188+
if not observed:
189+
observed = set()
190+
ret = {}
191+
relatives = obj.get_relatives(reltypes={reltype}, fetch_objects=False)
192+
for x in relatives[reltype]:
193+
assert x not in observed
194+
observed.add(x)
195+
ret[x] = dag(x, reltype, observed)
196+
return ret
183197

184198
## We create two tasks todo1 and todo2, todo2 being a child of todo1
185199
todo1 = _add_todo(ctx, summary=['make plann good'], set_due='2012-12-20 23:15:00', set_dtstart='2012-12-20 22:15:00', set_uid='todo1', set_categories='plann,keyboard')
@@ -234,8 +248,8 @@ def dag(obj, reltype, observed=None):
234248

235249
## Test that the calendar_name template attribute works
236250
list_cal = _list(ctx.obj['objs'], template="{calendar_name}", echo=False)
237-
assert list_cal[0] == todo1.parent.name
238-
assert list_cal[1] == todo2.parent.name
251+
assert list_cal[0] == todo1.parent.get_display_name()
252+
assert list_cal[1] == todo2.parent.get_display_name()
239253

240254
## panic planning, timeline_suggestion.
241255
## We have two tasks in the calendar, each with one hour duration
@@ -312,6 +326,9 @@ def dag(obj, reltype, observed=None):
312326
assert len(ctx.obj['objs'])==4
313327

314328
## Reset ... let's connect todo1 and todo2 again, and remove events
329+
## Reload to get fresh ETags — _check_for_panic modified these objects on the server
330+
todo1.load()
331+
todo2.load()
315332
_adjust_relations(todo1, {todo2})
316333
_adjust_relations(todo2, set())
317334
todo1.load()
@@ -435,8 +452,5 @@ def prompt(*largs, **kwargs):
435452
command_edit(todo1, 'set due=2012-12-19 19:15:00', interactive=False)
436453
assert todo1.icalendar_component['DUE'].dt > todo1.icalendar_component['DTSTART'].dt
437454

438-
finally:
439-
stop_xandikos_server(conn_details)
440-
441455
## TODO:
442456
## Things to be tested: lib._procrastinate, cli._select, cli._cats, cli._list, cli._interactive_edit, cli._set_something, cli._interactive_ical_edit, cli._edit, cli._check_for_panic, _add_todo, _agenda, _check_due,

0 commit comments

Comments
 (0)