Skip to content

Dedupe edits to a single instance of a recurring event - #565

Closed
stutrek wants to merge 4 commits into
allenporter:mainfrom
stutrek:fix-single-recurring-event-edits
Closed

Dedupe edits to a single instance of a recurring event#565
stutrek wants to merge 4 commits into
allenporter:mainfrom
stutrek:fix-single-recurring-event-edits

Conversation

@stutrek

@stutrek stutrek commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

This PR dedupes edited recurring events. Right now if a recurring event is edited, the original shows alongside the edited version, making a duplicate entry. An example of this is even in the existing snapshot data.

This PR does not fully fix #278, but it will fix the issue almost all the time, as many calendar clients delete the old event and create a new one when "this and all recurring events" is chosen instead of "just this event".

This scratches my personal itch.

I have another PR coming that will properly handle recurring events, it will include this PR, but is a much larger change.

In this PR:

  • Normalize dates to ensure time zones work
  • group events by their uid
  • dedupe edited events using their start time, as specified in the spec.

There should be no breaking changes.

@allenporter

Copy link
Copy Markdown
Owner

Thank you for the contribution, very much appreciated. Iit may take me a little time to review as i'm about to do some work travel -- but in the mean time: Would you be able to add test coverage for the cases covered beyond the snapshot updates? You could also add tests for the uncovered cases you plan to fix in the future.

In particular, i've known the specific issues here in the past, but do not currently remember them, so I think it would be very helpful to spell out in great detail all the corner cases, otherwise it'll probably take longer to review to know the impact of this change.

Thank you agian.

@allenporter

Copy link
Copy Markdown
Owner

Maybe said another way: Editing recurring events does NOT duplicate them in all scenarios -- just some particular scenario being hit here. Understanding the difference would be helpful. There should be many existing examples in the tests of editing recurring events that do not duplicate the events, so showing why the behavior works there but fails here would be helpful.

@codecov

codecov Bot commented Jan 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.16%. Comparing base (1b747da) to head (2b1d7c7).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #565      +/-   ##
==========================================
+ Coverage   97.15%   97.16%   +0.01%     
==========================================
  Files          53       53              
  Lines        3234     3248      +14     
==========================================
+ Hits         3142     3156      +14     
  Misses         92       92              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@allenporter allenporter left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry this has performance problems. Can you have a test that reproduces the issue you are trying to fix so we can understand if there are other approaches? e.g. if the original event information is wrong, we fix it there. I'm still not sure i exactly understand the gap here in the current event editing code.

Comment thread ical/recur_adapter.py Outdated
]
)
else:
# Recurring item - filter out dates that have been overridden

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this iterate over the entire series inline? that seems like a problem.

@allenporter
allenporter marked this pull request as draft February 1, 2026 19:15
@allenporter

Copy link
Copy Markdown
Owner

Converting to draft, you can mark as ready for review when ready.

@allenporter allenporter left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me clarify for the test: I'd like you to reproduce the issue using the Store APIs that support editing. There are existing tests of editing events and returning the results without dupicating things unnecessarily and they should be putting events in the corret state. So i'm wondering: is this not handling events edited by other systems? is there a difference between how the store edit is applied (it's incorrect?) vs other calendars?

@stutrek

stutrek commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

This is not reproducible when creating events with the Store API because this library adds EXDATE during edit. Other providers, like Google and Apple, do not use EXDATE, but only RECURRENCE-ID. This is valid in the spec.

I didn't realize this already worked, but the files I was using were simply missing the field that's used to check. We can just add EXDATE at parse time and it will work as intended. It has a side effect that a serialized ics file will have EXDATE added to recurring events (still valid, just different). I updated the PR to do this.

Here's some proof of the bug, a small calendar created in Google Calendar and shown in Home Assistant. I have included this ics in the repo as test data. It has an event that occurs three times, with the second one edited.

This is what it looks like in Google Calendar:
image

In Home Assistant, it looks like this:
image

Note that the second event is doubled up.

This bug has been reported to HA several times, my heart goes out to everyone that maintains popular open source projects, they... politely... direct people here.

I think this was the whole problem and my other PR is unnecessary, I will close it.

@stutrek
stutrek marked this pull request as ready for review February 2, 2026 03:39
@allenporter

allenporter commented Feb 2, 2026

Copy link
Copy Markdown
Owner

Thank you, this context is very helpful. In that case, using the .ics itself is fine. For all I know what they are doing is the "propper" fix and the approach this library takes is a hack :)

Is the .ics used in the test / example the same one produced in the screen shots or similar? I think trying to group by uid in that case makes sense.

@allenporter

allenporter commented Feb 2, 2026

Copy link
Copy Markdown
Owner

Are you seeing this with remote calendar integration? I wonder if we should only enable this behavior in compat mode....

Edit: On second thought, i can imagine this also happening with uploaded files of remote calendar, which i don't think enables compat mode yet.

@stutrek

stutrek commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

This screenshot is from the remote calendar integration. The ICS file I included is curl'd from the URL Google gave me to paste into HA.

I went through a few iterations with AI on how to do this, trying to see if there was a way to have this library work with only recurrence-id, but most of them were kind of far reaching and didn't look like a good fit. IMO this PR is the best one, but in case the serialized data is important to you, I made #572. It uses something similar to the initial method, but makes a set for fast lookup.

@stutrek

stutrek commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

For all I know what they are doing is the "propper" fix and the approach this library takes is a hack :)

I think everyone is just hacking. In Apple Calendar it was surprisingly easy to get phantom events, I saw some surprises from Google too.

Neither app will make edits to "this and future events" the way it's specified in the spec, they truncate the original event and make a new one.

Comment thread ical/calendar.py Outdated
# Group by UID
by_uid: dict[str, list[Event | Todo | Journal]] = {}
for item in items:
if item.uid:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it (a) correct and (b) faster for this to only look at values with either rrule or recurrence-id?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread ical/calendar.py Outdated
by_uid.setdefault(item.uid, []).append(item)

for uid_items in by_uid.values():
# Find the parent recurring event (has rrule, no recurrence_id)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you do if there are two with rrule? doe that happen? (e.g. with multiple edits or multiple edits where one has "THISANDFUTURE"?)

I worry there are a lot more corner cases that need to be considered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have test data with this situation where we could delete the EXDATE? It's going to be very hard for me to recreate this, the tools I use to make calendars make a new event when you check the "this and future events" checkbox.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The best I can think of is the home assistant local calendar can do THISANDFUTURE edits -- but you'll probably get ics that will work here since this library created it...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not saying this couldn’t be a corner case, but you’d need a this and future without an exdate. Is that allowed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(looking into this now, will be a follow up)

Comment thread ical/calendar.py Outdated
) -> None:
"""Reconcile recurrence overrides for a list of components."""
# Group by UID
by_uid: dict[str, list[Event | Todo | Journal]] = {}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be done with itertools.groupby(items, key=lambda x: x.uid)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itertools.groupby requires pre-sorted input, so it wouldn't work directly here. It looks like filtering the data and using items_by_uuid will work.

Comment thread ical/calendar.py
if (
isinstance(exdate, datetime.datetime)
and isinstance(parent.dtstart, datetime.datetime)
and parent.dtstart.tzinfo

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it needs to have a timezone? utc values won't work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can edit a recurrence to be in a different time zone than the original. This captures that edge case.


# Feb 2 - edited instance with RECURRENCE-ID
assert events[1].summary == "Edited Title"
assert events[1].recurrence_id == "20260202T100000"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My impression is our recur adapter always adds a recurrence_id. Is there something unique about this check or should all of the emitted instances from the timeline have it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks to ensure that it shows the correct recurrence_id and that it's not a duped event.

@stutrek

stutrek commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Ok, this is pretty hilarious. I asked Opus 4.6 to make a plan addressing these comments, this is what it came back with. If you like this solution more I'll argue with it when I have time, but I think it makes some good points.

image

@stutrek

stutrek commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

In case you've just finished the super bowl and sitting down to read PR reviews, this PR is almost definitely better suited to handle the THISANDFUTURE case. The tests pass, so everything generated from this library is working, but it's not likely that we'd be able to get THISANDFUTURE from less functional calendar libraries working in the other PR. I'm going to take a shot at making that work here.

@stutrek

stutrek commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

I gave the AI the whole spec, it chugged through a few dollars worth of tokens and said we shouldn't do anything for THISANDFUTURE. So I dug in and ended up agreeing. All the cases this library currently works for will continue to work, and the changes today do reduce the scope of the change. Same for #572.

I tried THISANDFUTURE in the big calendar apps, and came away surprised at how bad they are. I understand your passion for calendar libraries.

  • Apple - truncates and makes a new event. When you delete the whole event, all single event edits remain on your calendar as bugs and you can't delete them, you'll get notifications about them that you can't stop.
  • Google - truncates but cleans up the edited instances
  • Microsoft - does not support THISANDFUTURE, only editing ALL events; editing an instance in the middle causes previous instances to update.
  • Thunderbird - same as MS

If we were to make test data for this it would be complex to create, and entirely hypothetical. We can't generate failing data from an existing product like we can for the single event bug. It's outside of the scope of this PR, but it might be worth testing to see if Thunderbird and MS display THISANDFUTURE updates correctly.

I have no strong opinion on which PR is better, but my entire family begs you accept one of these, or add the test data to #513 and accept it if the test passes.

note: I didn't include broken data from Apple in the test data. Google displays the broken calendars correctly. I don't know if this library will

@allenporter

Copy link
Copy Markdown
Owner

I believe #572 has obsoleted this, and this can now be closed, but let me know if that is not the case.

@allenporter
allenporter marked this pull request as draft February 14, 2026 23:13
@stutrek stutrek closed this Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Revisit RECURRENCE-ID, RANGE, UID

2 participants