Skip to content

feat(deps)!: Update dependency croniter (6.2.4 ➔ 7.0.0) - #21

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/croniter-7.x
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/croniter-7.x

Conversation

@renovate

@renovate renovate Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
croniter ==6.2.4 → ==7.0.0 age confidence

Release Notes

pallets-eco/croniter (croniter)

v7.0.0

Compare Source

This is a major release because several bug fixes change what existing, valid
expressions mean. Read Upgrading to 7.0 before you upgrade. Every change is
listed in full under Bugfixes.

Upgrading to 7.0


Changes 1 and 2 are silent: nothing fails, schedules just fire at different times.
Changes 3 and 4 raise an exception.

**1. A step that starts at the field maximum now means one value, not the whole field (#​246).**
Each field's maximum is ``59`` for seconds and minutes, ``23`` for hours, ``31`` for
day-of-month, ``12``/``DEC`` for month, ``6``/``SAT`` (or ``7``) for day-of-week and
``2099`` for years.

- *Who is affected:* any expression in which a field starts with its maximum
  followed by ``/``, for example ``59/15``, ``23/6``, ``31/5``, ``DEC/3``,
  ``SAT/2``, ``6/1`` or ``2099/1``.
- *What to do:* find these expressions in your code, configuration and stored data.
  A field value such as ``59/...`` or ``6/...`` is the pattern to look for. Decide
  what each one was meant to do:

  - It meant "every": replace it with ``*`` or ``*/{step}``. ``* * * * 6/1``
    becomes ``* * * * *``, and ``59/15 * * * *`` becomes ``*/15 * * * *``.
  - It meant "only the maximum": no change is needed, because 7.0 now does that.
  - It is in a year field (``2099/1``) or on a day that can be missing
    (``31/5`` with month ``2``): the expression may now have no future match and
    raise ``CroniterBadDateError`` (see 4 below). Rewrite it with ``*``.

**2. ``expand_from_start_time=True`` keeps the bounds of ranges (#​255, #​256, #​246).**
If you never pass ``expand_from_start_time``, skip this item: default-mode schedules
are unchanged.

- *Who is affected:* expressions with an explicit range, an equal range such as
  ``1-1/3``, a wrapping range such as ``6-0``, or a stepped start such as ``5/15``.
  In 6.x such a range could fire outside its own bounds, so ``* 8-17 * * *``
  fired at hours 0-17. The phase was also read in UTC instead of the start time's
  own time zone.
- *What to do:* for each schedule that uses the flag, compare the old and new
  ``expanded`` values. Install 7.0 in a separate environment, or run the snippet
  below under both versions with your own expressions, start times and time zones:

  .. code-block:: python

      from datetime import datetime, timezone
      from croniter import croniter

      start = datetime(2026, 1, 1, 7, 7, tzinfo=timezone.utc)  # use your real start
      for expr in ["* 8-17 * * *", "5/15 * * * *"]:            # your expressions
          print(expr, croniter(expr, start, expand_from_start_time=True).expanded)

  Where the output differs, 7.0 matches the expression as written. If you relied
  on the old output, write that schedule out explicitly, for example as a list
  of values, instead of relying on the flag. A schedule can now fire less often
  (``45/15`` fires once an hour, not four times), or more often in the case of
  equal and wrapping ranges (``1-1/15`` in the minute field fires four times an
  hour). See the warning under #​255 for the full breakdown.

**3. Some expressions that used to be accepted now raise ``CroniterBadCronError``.**

- *Who is affected:* expressions containing any of the following:

  - non-ASCII digits or non-ASCII whitespace (#​284, #​288);
  - a ``H(a-b)`` or ``R(a-b)`` range outside its field (#​289);
  - day-of-month ``0`` in the 6-field seconds form (#​280);
  - an out-of-range ``#`` such as ``1#0`` or ``1#7`` (#​278).

- *What to do:* where expressions come from users or storage, check them with
  ``croniter.is_valid()`` before you upgrade, and fix the ones that fail. Each
  rejected form either matched something the text did not say or depended on the
  hash id, so there is no behaviour to keep.

**4. Some schedules now raise ``CroniterBadDateError`` or ``TypeError``/``ValueError``.**

- An expression changed by item 1 can have no remaining match. ``get_next`` then
  raises ``CroniterBadDateError``, so rewrite the expression as described in item 1.
- ``max_years_between_matches`` must now be an ``int`` >= 1 or ``None`` (#​272).
  Replace ``True`` with ``1``, ``0`` or a negative number with ``1``, and pass
  ``int(...)`` instead of a float or string.

**Other changes in behaviour.** These are corrections of wrong results, and no
action is expected:

- ``get_next``, ``get_prev``, ``match`` and ``croniter_range`` no longer report
  times invented inside a DST gap (#​266).
- pytz zones with negative DST offsets, such as ``Europe/Dublin``, no longer crash
  (#​267).
- Day-of-week ``7`` is accepted in the 6- and 7-field forms (#​263).
- A few hashed divisions wider than their range (``H/60``, ``H/100``) produce
  different values (#​253).

If you cannot audit your expressions yet, pin ``croniter<7``.

Bugfixes
~~~~~~~~
- Reject hashed and random ranges whose bounds lie outside their field, such as
  ``H(0-99)`` in the minute field. The bounds were only checked through the value drawn
  from them, so such an expression was valid for some hash ids and not others, and
  ``is_valid("R(0-99) * * * *")`` returned ``True`` or ``False`` depending on the draw.
  They now raise ``CroniterBadCronError`` for every hash id and draw. [#&#8203;289, @Raghuvardhan-Reddy78]
- ``max_years_between_matches`` must be an ``int`` greater than or equal to 1, or ``None``.
  ``bool`` is rejected (``True`` used to become 1 via ``int(True)``). ``0``, negatives,
  floats, and strings are rejected too: ``0`` and negatives used to be raised to 1,
  floats were truncated, and numeric strings were parsed by ``int()``. A bad type
  raises ``TypeError``. An int below 1 raises ``ValueError``. [#&#8203;272]
- Reject non-ASCII whitespace between cron fields. ``str.split()`` splits on any
  Unicode whitespace, so ``0 0 * * *`` written with a no-break space or an
  ideographic space between the fields passed ``is_valid`` and ran as ``0 0 * * *``,
  while cron only separates fields on spaces and tabs. These now raise
  ``CroniterBadCronError``. The ASCII separators ``\x1c``-``\x1f`` are no longer treated
  as field separators either. Follow-up to #&#8203;284. [#&#8203;288, @Raghuvardhan-Reddy78]
- Fix day-of-month ``0`` being silently rewritten to ``1`` and accepted in the 6-field seconds form (e.g. ``0 0 0 * * *``): it is now rejected with ``CroniterBadCronError``, matching the 5-field form fixed for #&#8203;9 and the 7-field years form. [#&#8203;280]
- Fix ``get_next``/``get_prev`` raising ``AssertionError`` for pytz timezones whose
  tzdb encoding uses a negative DST offset (for example ``Europe/Dublin``). At an
  ambiguous local time the resolution assumed the ``is_dst=True`` interpretation is
  always the later UTC instant; such zones invert that ordering, so the internal
  assertion tripped and evaluation crashed instead of returning a fire time. Both
  interpretations are now ordered by their absolute instants and the first one
  satisfying the successor requirement in the requested direction is returned.
  [#&#8203;267, @abhijeet117]
- Fix day-of-week ``7`` (Sunday) being rejected in the 6-field (seconds) and 7-field
  (year) forms. ``7`` is now accepted as an alias for ``0`` in every form, as it already
  was in the classic 5-field form. [#&#8203;263, @nikolauspschuetz]
- Reject non-ASCII digits in cron expressions. The parsing regexes used Unicode ``\d``
  and ``\w``, and ``int()`` also accepts any Unicode decimal digit, so ``٥ * * * *``
  (Arabic-Indic five) or ``0 0 * * *`` (fullwidth zeros) passed ``is_valid`` and ran as
  ``5 * * * *`` and ``0 0 * * *``. These now raise ``CroniterBadCronError``.
  [#&#8203;284, @Raghuvardhan-Reddy78]
- Fix ``get_next``, ``get_prev``, ``match`` and ``croniter_range`` reporting a DST gap shift
  as a fire time. When a cron slot fell into a spring-forward gap and resolving the
  alternative across the offset change walked forward to the next existing wall clock, that
  manufactured wall clock was returned although its fields did not match the expression: for
  ``0 * * * *`` in ``Australia/Lord_Howe``, whose transition is only 30 minutes, ``get_prev``
  from 03:00 landed on 02:30, ``get_next`` from 01:03 returned 02:30, ``match(02:30)`` was
  true, and ``croniter_range`` listed 02:30. A shifted alternative is now discarded like the
  regular candidate already was when hours are wildcarded, so the four APIs agree again.
  [#&#8203;266, @abhijeet117]
- Fix ``get_next``/``get_prev`` raising ``CroniterBadDateError`` when day-of-month and day-of-week are both restricted and the day-of-month can never occur in the selected month (e.g. ``0 0 31 2 0``). Under the Vixie OR semantics the unsatisfiable side now contributes no dates instead of aborting the whole expression, and ``match()`` and ``croniter_range()``, which swallow the error, no longer return silently wrong results for these expressions. [b245ab6, #&#8203;243, @semx, @MildlyMeticulous]
- Fix hashed divisions (``H/{divisor}``, ``H({begin}-{end})/{divisor}``) when the divisor
  is as wide as, or wider than, the range it is drawn from. Three symptoms, one cause:
  the offset was drawn from the first period without regard for where the range ends,
  and the result was emitted as ``{offset}-{end}/{divisor}`` even when the step could
  not reach a second value. ``H/60`` lost minute 59 and fired at minute 0 twice as
  often as any other minute (59 distinct schedules over 20,000 hash ids, not 60);
  ``H(50-59)/10`` fired six times an hour instead of once for the ids hashing to 59;
  ``H(50-52)/10`` fired outside its own range for 7 hash buckets in 10, and ``H/100``
  raised ``CroniterBadCronError`` for roughly a third of hash ids. The offset is now
  clamped to the declared range and emitted as a single value when the step cannot
  reach a second one. Divisors that fit their range keep their existing hashes, so no
  schedule that was already correct moves. [#&#8203;253, @potiuk]
- Fix ``expand_from_start_time`` taking the phase of a cycle from the UTC form of a
  timezone-aware ``start_time`` rather than from the wall clock the caller wrote.
  ``_get_low_from_current_date_number`` read the start time back with ``tz=UTC``, so
  ``0 */5 * * *`` from ``2025-01-01 05:00+13:00`` phased on UTC hour 16 and expanded to
  hours ``1,6,11,16,21`` instead of ``0,5,10,15,20``. The hour, day-of-month, month and
  day-of-week fields were all affected, and at a date boundary the day and month could
  be a whole day or month out. The start time's own ``tzinfo`` is now carried into the
  expansion. A naive ``start_time`` is converted to a timestamp as if it were UTC, so
  UTC round-trips it to the same wall clock and naive callers see no change at all —
  verified over 45,312 expansions with naive start times, none of which differ.
  [#&#8203;256, @potiuk]
- Fix ``expand_from_start_time`` raising ``ValueError: Can't get current date number for
  index larger than 4`` for any range or step in the optional **seconds** or **years**
  field. ``croniter("* * * * * */15", start, expand_from_start_time=True)`` aborted, and
  because the exception came from outside the ``CroniterError`` hierarchy, code catching
  ``CroniterError`` never saw it. Both fields now re-base like the five that already
  worked: seconds take their phase from the start time's second, and years — which do
  not start at zero — take theirs from the field minimum, as day-of-month and month
  already do. [#&#8203;254, @potiuk]
- Reject out-of-range nth day-of-week values (``1#0``, ``1#7``) with an ordinary check
  instead of an ``assert``. Under ``python -O`` the assert was stripped, so ``1#0``
  passed ``is_valid`` and fired on every Monday and ``1#7`` passed and never fired.
  [#&#8203;278, @Raghuvardhan-Reddy78]
- Fix ``{start}/{step}`` expanding to the whole field when ``start`` is the field
  maximum. ``59/15 * * * *`` fired at ``:00``/``:15``/``:30``/``:45`` instead of
  ``:59``, and the same held in every field at its own maximum: ``23/6`` hours,
  ``31/5`` day-of-month, ``DEC/3`` months, ``SAT/2`` day-of-week, and — not in the
  original report — ``59/15`` seconds and ``2099/5`` years. ``{start}/{step}``
  normalizes to ``{start}-{max}/{step}``; when ``start`` is the maximum the two
  bounds collide, making the token indistinguishable from an explicitly written
  equal range such as ``Jan-Jan``, which croniter deliberately expands to the whole
  cycle. The two forms are now distinguished. An explicitly written equal range still
  means the whole cycle, and every start below the field maximum is unchanged. The fix
  applies in ``expand_from_start_time`` mode as well as by default. (Under that flag
  the whole cycle an equal range names now takes its phase from ``start_time``, as
  every other cycle does — see #&#8203;255 below.) [#&#8203;246, @earfman]

  .. warning::

     **This changes existing schedules, silently and substantially.** Any
     ``{max}/{step}`` expression that was firing across the whole field now fires
     once per cycle. The ``{max}/1`` forms are the sharpest case, because they read
     as "every": ``* * * * 6/1`` meant *every day* and now means *Saturdays only*;
     ``59/1 * * * *`` meant *every minute* and now means *minute 59*. The same
     applies to ``23/1`` hours, ``31/1`` day-of-month, ``12/1`` months, and to the
     optional seconds and years fields (``59/1``, ``2099/1``). Use a wildcard
     (``*`` or ``*/1``) for "every". **Before upgrading, audit for any field whose
     value begins with that field's maximum followed by a slash.**

     One consequence is worth stating separately: a ``{max}/{step}`` expression can
     now be **unsatisfiable** rather than merely different, because it no longer
     covers the whole field. ``0 0 1 1 * * 2099/1`` previously expanded the year
     field to every year and now means 2099 alone, so iterating from a later start
     time raises ``CroniterBadDateError`` instead of returning a date. The same
     happens without a year field wherever the remaining value cannot occur:
     ``0 0 31/5 2 *`` is now day 31 of February.

- Fix ``expand_from_start_time`` discarding the lower bound of every range. Re-basing
  overwrote ``low`` outright instead of supplying the phase of the cycle, so any range
  whose start was above the field minimum fired below its own start: ``* 8-17 * * *``
  expanded to hours ``0-17``, ``* * * * MON-FRI`` included Sunday, and ``10-50/15``
  expanded to minutes ``7,22,37`` from a start at minute ``7``. Over a sweep of 101,088
  ``expand_from_start_time`` expansions — all seven fields, every legal lower bound,
  eight token shapes, seven steps, four start times — 73,898 contained at least one
  value outside the range the expression declares; now none do. Re-basing still
  supplies the phase — ``*/15`` is unchanged, and so is ``5/15`` whenever the start
  time's phase is at or above 5 (from minute 2 it now gives ``17,32,47``, not
  ``2,17,32,47``) — but the phase is advanced to the first value inside the declared
  range, and where no value of the phase fits, the expression is honoured as written,
  so an expansion can never come out empty. Hashed and random divisors (``H/15``,
  ``R/15``, ``H(10-50)/7``) take their phase from the start time inside their declared
  range, exactly like ``*/15`` and ``10-50/7``: the hash offset is a phase, so it is not
  read as a lower bound. [#&#8203;255, @potiuk]

  Two side effects of overwriting the bound are fixed with it: colliding ``low`` onto
  ``high`` made an ordinary range read as an explicit equal range and expand to the
  whole cycle (``* * 10-11/2 * *`` became every second day of the month), and re-basing
  a wrapping range unwrapped it (``* * * * 6-0``, Saturday and Sunday, became every day
  of the week).

  An explicitly written equal range means the whole cycle, and that cycle now takes its
  phase from ``start_time`` like any other, so the two spellings of one schedule agree:
  ``1-1/3`` and ``*/3`` in the month field both expand to February, May, August and
  November from a start in February. Under the old behaviour they agreed by accident —
  the discarded bound happened to land on the phase — and pinning the bound without
  this would have made ``1-1/3`` ignore the flag outright, contradicting what the
  README documents for it.

  Also fixed, on the same line: a ``start_time`` of exactly the Unix epoch skipped
  re-basing altogether, because the start time reaches the expansion as a timestamp
  and ``0.0`` is falsy. ``0 0 * * 2-6/3`` from ``1970-01-01`` expanded to days ``2,5``
  where the same expression one second later expanded to day ``4``.

  .. warning::

     **This changes most ``expand_from_start_time`` schedules.** 93,456 of the 101,088
     expansions in the sweep differ. Any explicitly written range, equal range, wrapping
     range or stepped start is affected; the untouched shapes are the plain wildcard,
     the bare value, and ``*/step`` except where the step is wide enough to collide the
     field's own bounds (``*/60`` in the minute field re-based to minute ``0`` rather
     than to its phase). Default-mode expansions do not move at all, no expansion became
     empty, and no error status changed.

     **A schedule can fire less often than it did.** ``45/15`` goes from four fires an
     hour to one, which is what that expression already means without the flag. More
     generally, re-phasing a step inside a fixed window can cost one period even when
     nothing was wrong with the schedule: ``10-20/7`` is ``10,17`` by default but ``14``
     alone from a start at minute 7. Measured against the default expansion of the same
     expression, no re-based expansion in the sweep fires *more* often, 13,539 fire
     less, and the drop is never more than one period and never to zero.

     **A schedule can also fire more often than it did**, in 17,038 of the swept
     expansions. Every one of them is an explicit equal range or a wrapping range —
     the two shapes the old behaviour re-read as something else. ``1-1/15`` in the
     minute field expanded to ``[0]`` from a start at minute 0 and is now ``[0, 15, 30,
     45]``, the whole cycle the equal range asks for; the wrapping range ``2-1``, which
     covers the whole minute field, expanded to minutes ``0,1`` and is now every minute.
     These are meanings being restored, not values being invented: no expansion holds a
     value the expression does not declare.

Testing and Documentation
  • Document what expand_from_start_time actually means — that it moves the phase of a
    cycle and not the values a field may take, that every field re-bases including the
    optional seconds and years, and that the phase is read in the start time's own
    timezone. Also document that a range with two equal bounds (Jan-Jan, Sun-Sun)
    means the whole cycle rather than the value it names, which was undocumented and is
    the rule behind several of the bugs fixed in this release. [#​257, @​potiuk]
  • Document the bounds half of expand_from_start_time: that a field declaring its own
    range keeps both bounds and only the phase moves, that moving a phase inside a fixed
    window can cost one period, that an expression whose phase does not fit is honoured as
    written, and that a wrapping range is left alone. [#​255, @​potiuk]

Packaging

- Bump pinned GitHub Actions via dependabot. [6e69553, #&#8203;247; 3c6ce9b, #&#8203;251; d2e8a13, #&#8203;260;
  d9f40f0, #&#8203;269; 4be99c3, #&#8203;274; 9fe300d, #&#8203;285; 2725ef5, #&#8203;290]
- Bump pinned build dependencies (``hatchling``, ``packaging``) via dependabot.
  [70564f2, #&#8203;245; 3dd4d14, #&#8203;262; e3ba628, #&#8203;265; 309703f, #&#8203;283; b4c7661, #&#8203;291]
- Remove the unused workflow that built the 32-bit Docker test images. [1b0c241, #&#8203;292, @potiuk]

6.2.4 (2026-07-10)
------------------

Bugfixes
~~~~~~~~
- Fix ``expand_from_start_time`` day-of-week low bound so Sunday wraps correctly for stepped day-of-week ranges (``isoweekday() % 7`` instead of ``weekday() + 1``). [44e3090, #&#8203;239, @alhudz]

Packaging

Configuration

📅 Schedule: (in timezone Europe/London)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
GIT_MERGE_CONFLICT_MARKERS Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅

All files and directories linted successfully

For more information, see the GitHub Actions workflow run

Powered by Super-linter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant