Skip to content

Commit 357183e

Browse files
tobixenclaude
andcommitted
feat: warn on orphaned RECURRENCE-ID overrides
When `add ical` splits a VCALENDAR, a UID group may hold only RECURRENCE-ID overrides and no master event. That usually means the recurring event was deleted elsewhere without cleaning up its overrides. They are still imported as detached occurrences, but a warning now names the UID and the summaries rather than letting it pass silently. Rescued from an unreferenced commit that a rebase left behind on no branch (1d06e47, 2026-05-21, "fix: warn on orphaned RECURRENCE-ID overrides during ical import", by Claude Sonnet 4.6), and adapted: _split_vcal returns ical strings now, so the check runs before the conversion. The NotFoundError this data caused on import was fixed in the caldav library at the time. Prompt: Repair the PR and apply anything useful from 1d06e47 and 2634df2 into the PR [both are unreferenced commits found while checking stale worktrees and branches for work worth rescuing; 2634df2 held only a superseded roadmap draft and a scratch estimates file, so nothing of it is applied] Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b65df04 commit 357183e

3 files changed

Lines changed: 31 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and I do try to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.h
1010
* `edit --add-categories`, `--add-resource` and `--add-resources` options (alongside the existing `--add-category`). A single value containing commas is split by the plural forms (`--add-categories`/`--add-resources`) and kept literal by the singular forms (`--add-category`/`--add-resource`).
1111
* `select ... list --separator=...` to join the listed items with something other than a newline. (Ported from the archived development branch.)
1212
* The interactive edit prompt now advertises the `start` command (kicks off time tracking for the task) and re-prompts afterwards so a follow-up command can be given for the same task. (Ported from the archived development branch.)
13+
* `add ical` warns when the imported data holds RECURRENCE-ID overrides with no master event for the same UID - typically left behind when the recurring event was deleted elsewhere. The overrides are still imported, as detached occurrences.
1314
* `select --warn-on-missing-uid/--no-warn-on-missing-uid` (default: warn). A `--uid` that matches nothing in any calendar now prints a warning naming the missing uid(s) to stderr, rather than being silently ignored. `--abort-on-missing-uid` still takes precedence, and `--no-warn-on-missing-uid` restores the old fully-silent behaviour. Ref https://github.com/tobixen/plann/issues/42
1415
* `plann configure`: an interactive configuration mode (EXPERIMENTAL/under-tested) that prompts for connection parameters and writes them to the config file. The underlying code existed but had been orphaned in the argparse→click migration; it is now wired up again, and the prompted keys match what the caldav library actually reads (so e.g. `ssl_verify_cert` is no longer written under a key that is silently ignored at connect time).
1516

‎plann/lib.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ def _split_vcal(ical):
138138
for tz in ical_cal_stripped.subcomponents:
139139
split_by_uid[uid].add_component(tz)
140140
split_by_uid[uid].add_component(subcomponent)
141+
for uid, vcal in split_by_uid.items():
142+
components = [x for x in vcal.subcomponents if not isinstance(x, icalendar.Timezone)]
143+
has_master = any('RECURRENCE-ID' not in x for x in components)
144+
has_exception = any('RECURRENCE-ID' in x for x in components)
145+
if has_exception and not has_master:
146+
logging.warning(
147+
"UID %s has RECURRENCE-ID override(s) but no master event in this calendar: %s. "
148+
"The master may have been deleted. Will import as detached occurrence(s).",
149+
uid, [str(x.get('SUMMARY', '(no summary)')) for x in components])
150+
141151
## Return ical strings, like _split_vcals does - the callers hand the
142152
## result on to _caldav_objclass()/add_object(), which parse text.
143153
return [cal.to_ical().decode() for cal in split_by_uid.values()]

‎tests/test_lib.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,26 @@ def test_split_vcal():
403403
assert(len(output) == 2)
404404

405405

406+
def test_split_vcal_warns_on_orphaned_recurrence_id(caplog):
407+
"""A UID group holding only RECURRENCE-ID overrides has lost its master
408+
event - typically deleted by the originating calendar application without
409+
cleaning up the overrides. Import it, but say so."""
410+
orphan = todo.replace(
411+
'UID:19970901T130000Z-123404@host.com',
412+
'UID:19970901T130000Z-123404@host.com\nRECURRENCE-ID:19970415T133000Z')
413+
with caplog.at_level('WARNING'):
414+
parts = _split_vcal(orphan)
415+
assert len(parts) == 1
416+
assert 'RECURRENCE-ID' in caplog.text
417+
assert '19970901T130000Z-123404@host.com' in caplog.text
418+
419+
## a normal object must not warn
420+
caplog.clear()
421+
with caplog.at_level('WARNING'):
422+
_split_vcal(todo)
423+
assert caplog.text == ''
424+
425+
406426
def test_split_vcal_yields_ical_strings():
407427
"""`add ical` feeds every element of _split_vcal() straight into
408428
_caldav_objclass(), which parses text - so _split_vcal must yield ical

0 commit comments

Comments
 (0)