fix(recur): apply BYHOUR, BYMINUTE, BYSECOND, BYYEARDAY and BYWEEKNO - #681
Conversation
…llenporter#678) rfc5545 section 3.3.10 defines fourteen recurrence rule parts. Five were parsed, stored and re-serialised without ever reaching the expansion, so an event with BYHOUR=9,17 fired at whatever hour DTSTART happened to hold and the file round-tripped unchanged. Silently wrong rather than loudly absent. `as_rrule()` passed nine keyword arguments to `dateutil.rrule.rrule()`; that constructor accepts these five as well, so the engine was already capable of computing them. Three places needed the new keys: the model, the rule parser that splits comma lists, and the serialiser. Tests assert against `dateutil` given the same arguments rather than against hand-written expectations, since a hand-written list would only restate whatever the implementation produced. `test_recur_extra_fields_preservation` asserted that BYYEARDAY and BYWEEKNO arrive as unparsed extras, which was a description of this bug. It now checks that they are parsed, and that a genuinely unknown key is still preserved as an extra. Snapshots are regenerated because Recur's repr gained five fields. No occurrence changed: every dtstart in the updated snapshots is identical to the one it replaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| @@ -0,0 +1,144 @@ | |||
| """The BYxxx rule parts that were parsed but never applied. | |||
There was a problem hiding this comment.
This belongs in test_recur.py. We add tests in the module they are testing, not arbitrarily named files that AI likes to generate.
…pansion
Found reviewing the previous commit. Passing these parts through to
dateutil means an out-of-range value now reaches it, and dateutil raises
during iteration:
BYHOUR=99 -> ValueError: hour must be in 0..23, not 99
That is the same shape as the UNTIL defect fixed in allenporter#676: parses cleanly,
then takes down the expansion far from the cause. Before this PR the value
was silently ignored, so nothing failed at all.
Validating on the field matches what by_month_day and by_setpos already
do for exactly this reason, and puts the error where the bad value is.
BYSECOND=60 is a separate case: rfc5545 permits it for a leap second and
dateutil rejects it, so it folds onto 59 rather than failing a calendar
over a value the spec allows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewing my own commit turned up a defect I had introduced, plus a valid rfc5545 value I would have broken. Both fixed in the second commit — flagging rather than force-pushing over it, because one of them is a behaviour change you may want decided differently. Passing these parts through meant out-of-range values now reach dateutil, which raises during iteration: Before this PR, Now validated on the field, which is what
The behaviour change worth your opinionA malformed I chose it for consistency — 18 tests now. Five fail without the validators, and the rest of the PR's cases are unaffected. Suite 1474 passed, 35 skipped; all hooks green except the pre-existing 🤖 Generated with Claude Code |
|
Thanks, proposed solution looks fine by me. |
Fixes #678.
as_rrule()passed nine keyword arguments todateutil.rrule.rrule(). That constructor acceptsbyhour,byminute,bysecond,byyeardayandbyweeknotoo, so the engine could already compute them — they were simply never handed over, while being parsed, stored and re-serialised. An event kept the hour fromDTSTARTand the file round-tripped unchanged, which is why this looked like it worked.Measured before and after,
DTSTART:20250715T140000Z:FREQ=DAILY;COUNT=3;BYHOUR=9,17FREQ=YEARLY;COUNT=2;BYYEARDAY=200Three places needed the new keys, and missing any one of them fails differently: the model, the rule parser that splits comma-separated lists (without it,
BYHOUR=9,17arrives as the string'9,17'and validation fails), and_as_rrule_str.Tests
tests/types/test_recur_byparts.py, 13 cases. Each part is asserted againstdateutil.rrulegiven the same arguments rather than against a hand-written list — the expansion engine is the same one this library uses, so a hand-written expectation would only restate whatever the implementation produced.Also pinned: the parts still round-trip to ics, a rule using none of them is byte-identical to before, and negative
BYYEARDAYcounts from the end of the year.Two things you should look at rather than take on trust
A test asserted the bug.
test_recur_extra_fields_preservationchecked thatBYYEARDAYandBYWEEKNOarrive as unparsed extras — a description of this defect, written when they weren't modelled. Left alone it would have failed; changed carelessly it would have stopped testing anything. It now asserts they parse, and that a genuinely unknown key (X-EXTRA) is still preserved as an extra, which is what the test was really for.Snapshots are regenerated, because
Recur's repr gained five fields. I checked that no occurrence actually changed: everydtstartin the updated snapshots is identical to the one it replaced — the diff is only the five empty lists appearing in the repr.CI note
pre-commit run tyfails onical/recur_adapter.py:84with an unused-ignore warning. That is present on an untouchedmain— verified against a clean worktree ofupstream/main— and is unrelated to this change, so I have not touched it. Every other hook passes. Suite: 1469 passed, 35 skipped.🤖 Generated with Claude Code