fix(cron): accept weekday lists and no-'every' natural schedules (#51975)
Widens _natural_every_to_cron to consume comma/'and'-separated weekday
lists ('Monday, Wednesday at 9am' -> '0 9 * * 1,3') and applies the same
helper to schedules without the 'every' prefix, matching the exact forms
the Desktop dialog advertises in the #51975 repro.
This commit is contained in:
+27
-6
@@ -861,19 +861,40 @@ def _natural_every_to_cron(rest: str) -> Optional[str]:
|
||||
"monday 9am" -> "0 9 * * 1"
|
||||
"day at 9am" -> "0 9 * * *"
|
||||
"weekday at 9am" -> "0 9 * * 1-5"
|
||||
"monday, wednesday at 9am" -> "0 9 * * 1,3"
|
||||
|
||||
Returning None lets ``parse_schedule`` fall back to the interval
|
||||
(``every 30m``) path, so existing duration schedules are unaffected.
|
||||
"""
|
||||
tokens = rest.lower().split()
|
||||
tokens = rest.lower().replace(",", " ").split()
|
||||
if not tokens:
|
||||
return None
|
||||
day_token = tokens[0]
|
||||
dow = _WEEKDAY_TO_CRON_DOW.get(day_token) or _DAYSPEC_TO_CRON_DOW.get(day_token)
|
||||
if dow is None:
|
||||
return None
|
||||
|
||||
time_tokens = tokens[1:]
|
||||
# Consume one or more leading day tokens: a keyword spec ("weekdays"),
|
||||
# a single weekday, or a comma/"and"-separated weekday list
|
||||
# ("monday, wednesday at 9am").
|
||||
day_token = tokens[0]
|
||||
dow = _DAYSPEC_TO_CRON_DOW.get(day_token)
|
||||
idx = 1
|
||||
if dow is None:
|
||||
days = []
|
||||
while idx <= len(tokens):
|
||||
tok = tokens[idx - 1]
|
||||
if tok == "and":
|
||||
idx += 1
|
||||
continue
|
||||
mapped = _WEEKDAY_TO_CRON_DOW.get(tok)
|
||||
if mapped is None:
|
||||
break
|
||||
if mapped not in days:
|
||||
days.append(mapped)
|
||||
idx += 1
|
||||
if not days:
|
||||
return None
|
||||
dow = ",".join(days)
|
||||
idx -= 1
|
||||
|
||||
time_tokens = tokens[idx:]
|
||||
# Optional "at" separator: "every day at 9am".
|
||||
if time_tokens and time_tokens[0] == "at":
|
||||
time_tokens = time_tokens[1:]
|
||||
|
||||
@@ -129,6 +129,28 @@ class TestParseSchedule:
|
||||
pytest.importorskip("croniter")
|
||||
assert parse_schedule("every weekend at 10am")["expr"] == "0 10 * * 0,6"
|
||||
|
||||
def test_no_every_prefix_natural_forms(self):
|
||||
# The Desktop dialog advertises these WITHOUT the "every" prefix
|
||||
# (#51975 repro): they must parse identically.
|
||||
pytest.importorskip("croniter")
|
||||
assert parse_schedule("weekdays at 9am")["expr"] == "0 9 * * 1-5"
|
||||
assert parse_schedule("monday at 9:30")["expr"] == "30 9 * * 1"
|
||||
assert parse_schedule("daily at 7am")["expr"] == "0 7 * * *"
|
||||
|
||||
def test_weekday_list_forms(self):
|
||||
# Comma/"and"-separated day lists from the #51975 repro.
|
||||
pytest.importorskip("croniter")
|
||||
assert parse_schedule("Monday, Wednesday at 9am")["expr"] == "0 9 * * 1,3"
|
||||
assert parse_schedule("monday and friday at 5pm")["expr"] == "0 17 * * 1,5"
|
||||
assert parse_schedule("every tue, thu 8am")["expr"] == "0 8 * * 2,4"
|
||||
|
||||
def test_natural_form_negatives_still_reject(self):
|
||||
pytest.importorskip("croniter")
|
||||
with pytest.raises(ValueError):
|
||||
parse_schedule("monday banana at 9am")
|
||||
with pytest.raises(ValueError):
|
||||
parse_schedule("funday at 9am")
|
||||
|
||||
def test_every_time_formats(self):
|
||||
pytest.importorskip("croniter")
|
||||
# 24-hour, explicit minutes, noon/midnight, bare hour.
|
||||
|
||||
Reference in New Issue
Block a user