summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLowell Alleman <lowell@kintyre.co>2021-03-29 21:25:50 -0400
committerkiorky <kiorky@cryptelium.net>2021-04-07 17:01:14 +0200
commit60ced5cd2a96f0a62d9ef87d72065aaf36349ffb (patch)
treec9d62db0f0a0e6611654a87fe4f8069f3d507f78
parenteaf067317ae18e3831c616b75db2239ba72e6241 (diff)
downloadcroniter-60ced5cd2a96f0a62d9ef87d72065aaf36349ffb.tar.gz
Cleanup PR based on feedback
- Optimize scenario where only 'L' modifier exists in the wday expression to reduce the number of invocations of the calendar module. - Cleanup and combined parsing logic for specialty wday syntax (d#n/ Ld) - Add new unit tests to confirm proper exception handling for out-of-range 'L' and '#' nth month for the wday expression. - Add examples of new syntax in README. - Update changelog to cover new syntax and new exception.
-rw-r--r--README.rst5
-rw-r--r--docs/CHANGES.rst9
-rw-r--r--src/croniter/croniter.py42
-rwxr-xr-xsrc/croniter/tests/test_croniter.py12
4 files changed, 49 insertions, 19 deletions
diff --git a/README.rst b/README.rst
index d658874..7ecdca0 100644
--- a/README.rst
+++ b/README.rst
@@ -50,8 +50,13 @@ A simple example::
>>> print(iter.get_next(datetime)) # 2010-09-01 04:02:00
>>> print(iter.get_next(datetime)) # 2010-12-01 04:02:00
>>> print(iter.get_next(datetime)) # 2011-06-01 04:02:00
+ >>>
>>> iter = croniter('0 0 * * sat#1,sun#2', base)
>>> print(iter.get_next(datetime)) # datetime.datetime(2010, 2, 6, 0, 0)
+ >>> iter = croniter('0 0 * * 5#3,L5', base) # 3rd and last Fridays of the month
+ >>> print(iter.get_next(datetime)) # 2010-01-29 00:00:00
+ >>> print(iter.get_next(datetime)) # 2010-02-19 00:00:00
+
All you need to know is how to use the constructor and the ``get_next``
method, the signature of these methods are listed below::
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 2ef4104..7ef9e9d 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -4,7 +4,14 @@ Changelog
1.0.11 (unreleased)
-------------------
-- Nothing changed yet.
+- Add support for ``L`` in the day_of_week component. This enable expressions like ``* * * * L4``, which means last Thursday of the month. This resolves #159.
+ [Kintyre]
+- Create ``CroniterUnsupportedSyntaxError`` exception for situations where CRON syntax may be valid but some combinations of features is not supported.
+ Currently, this is used when the ``day_of_week`` component has a combination of literal values and nth/last syntax at the same time.
+ For example, ``0 0 * * 1,L6`` or ``0 0 * * 15,sat#1`` will both raise this exception because of mixing literal days of the week with nth-weekday or last-weekday syntax.
+ This *may* impact existing cron expressions in prior releases, because ``0 0 * * 15,sat#1`` was previously allowed but incorrectly handled.
+ [Kintyre]
+
- Update ``croniter_range()`` to allow an alternate ``croniter`` class to be used. Helpful when using a custom class derived from croniter.
[Kintyre]
diff --git a/src/croniter/croniter.py b/src/croniter/croniter.py
index c2f6e1d..17e883d 100644
--- a/src/croniter/croniter.py
+++ b/src/croniter/croniter.py
@@ -17,7 +17,7 @@ import calendar
step_search_re = re.compile(r'^([^-]+)-([^-/]+)(/(\d+))?$')
only_int_re = re.compile(r'^\d+$')
star_or_int_re = re.compile(r'^(\d+|\*)$')
-last_weekday_re = re.compile(r'^l([0-7])$')
+special_weekday_re = re.compile(r'^(\w+)#(\d+)|l(\d+)$')
VALID_LEN_EXPRESSION = [5, 6]
@@ -394,24 +394,24 @@ class croniter(object):
candidates = []
for wday, nth in nth_weekday_of_month.items():
- # XXX: Optimize to only run calendar, when needed: if len(nth) > 1 or nth[0] != "l":
- w = (wday + 6) % 7
- c = calendar.Calendar(w).monthdayscalendar(d.year, d.month)
- if c[0][0] == 0:
- c.pop(0)
+ c = None
+ if nth != {"l"}:
+ w = (wday + 6) % 7
+ c = calendar.Calendar(w).monthdayscalendar(d.year, d.month)
+ if c[0][0] == 0:
+ c.pop(0)
for n in nth:
if n == "l":
candidate = self._get_last_weekday_of_month(d.year, d.month, wday)
+ elif len(c) < n:
+ continue
else:
- if len(c) < n:
- continue
candidate = c[n - 1][0]
if (
(is_prev and candidate <= d.day) or
(not is_prev and d.day <= candidate)
):
candidates.append(candidate)
- del w, c
if not candidates:
if is_prev:
@@ -592,18 +592,24 @@ class croniter(object):
if i == 4:
# Handle special case in the day-of-week expression
- m = last_weekday_re.match(str(e))
+ m = special_weekday_re.match(str(e))
if m:
- e = m.group(1)
- nth = "l"
- else:
- e, sep, nth = str(e).partition('#')
+ orig_e = e
+ e, nth, last = m.groups()
if nth:
- if not re.match(r'[1-5]', nth):
+ try:
+ nth = int(nth)
+ assert (nth >= 1 and nth <= 5)
+ except (ValueError, AssertionError):
raise CroniterBadCronError(
- "[{0}] is not acceptable".format(expr_format))
- nth = int(nth)
- del sep
+ "[{0}] is not acceptable. Invalid day_of_week "
+ "value: '{1}'".format(expr_format, orig_e))
+ elif last:
+ nth = "l"
+ e = last
+ del last, orig_e
+ else:
+ nth = None
# Before matching step_search_re, normalize "*" to "{min}-{max}".
# Example: in the minute field, "*/5" normalizes to "0-59/5"
diff --git a/src/croniter/tests/test_croniter.py b/src/croniter/tests/test_croniter.py
index fd82580..c171983 100755
--- a/src/croniter/tests/test_croniter.py
+++ b/src/croniter/tests/test_croniter.py
@@ -1308,6 +1308,18 @@ class CroniterTest(base.TestCase):
self.assertListEqual(getn(cron_b, 3), expect_b)
self.assertListEqual(getn(cron_c, 5), expect_c)
+ def test_nth_out_of_range(self):
+ with self.assertRaises(CroniterBadCronError):
+ croniter("0 0 * * 1#7")
+ with self.assertRaises(CroniterBadCronError):
+ croniter("0 0 * * 1#0")
+
+ def test_last_out_of_range(self):
+ with self.assertRaises(CroniterBadCronError):
+ croniter("0 0 * * L-1")
+ with self.assertRaises(CroniterBadCronError):
+ croniter("0 0 * * L8")
+
def test_issue_142_dow(self):
ret = []
for i in range(1, 31):