Browse Source

Rewrites the crontab parser using regexes, to remove the pyparsing dependency.

Ask Solem 13 years ago
parent
commit
cd57928d05
6 changed files with 68 additions and 62 deletions
  1. 2 0
      Changelog
  2. 65 57
      celery/schedules.py
  3. 1 2
      celery/tests/test_task/__init__.py
  4. 0 1
      requirements/default.txt
  5. 0 1
      setup.cfg
  6. 0 1
      setup.py

+ 2 - 0
Changelog

@@ -66,6 +66,8 @@ Important Notes
 News
 ----
 
+* No longer depends on :mod:`pyparsing`.
+
 * Broker transports can be now be specified using URLs
 
     The broker hostname can now be given as an URL instead, of the format::

+ 65 - 57
celery/schedules.py

@@ -1,15 +1,19 @@
 from __future__ import absolute_import
 
+import re
+
 from datetime import datetime, timedelta
 from dateutil.relativedelta import relativedelta
-from pyparsing import (Word, Literal, ZeroOrMore, Optional,
-                       Group, StringEnd, alphas)
 
 from .utils import is_iterable
 from .utils.timeutils import (timedelta_seconds, weekday,
                               remaining, humanize_seconds)
 
 
+class ParseException(Exception):
+    """Raised by crontab_parser when the input can't be parsed."""
+
+
 class schedule(object):
     relative = False
 
@@ -50,8 +54,8 @@ class schedule(object):
         return False, rem
 
     def __repr__(self):
-        return "<freq: %s>" % humanize_seconds(
-                timedelta_seconds(self.run_every))
+        return "<freq: %s>" % (
+                    humanize_seconds(timedelta_seconds(self.run_every)), )
 
     def __eq__(self, other):
         if isinstance(other, schedule):
@@ -85,70 +89,74 @@ class crontab_parser(object):
         [0, 1, 2, 3, 4, 5, 6]
 
     """
+    ParseException = ParseException
 
-    def __init__(self, max_=60):
-        # define the grammar structure
-        digits = "0123456789"
-        star = Literal('*')
-        number = Word(digits) | Word(alphas)
-        steps = number
-        range_ = number + Optional(Literal('-') + number)
-        numspec = star | range_
-        expr = Group(numspec) + Optional(Literal('/') + steps)
-        extra_groups = ZeroOrMore(Literal(',') + expr)
-        groups = expr + extra_groups + StringEnd()
-
-        # define parse actions
-        star.setParseAction(self._expand_star)
-        number.setParseAction(self._expand_number)
-        range_.setParseAction(self._expand_range)
-        expr.setParseAction(self._filter_steps)
-        extra_groups.setParseAction(self._ignore_comma)
-        groups.setParseAction(self._join_to_set)
+    _range = r'(\w+?)-(\w+)'
+    _steps = r'/(\w+)?'
+    _star = r'\*'
 
+    def __init__(self, max_=60):
+        _range = self._range
+        _steps = self._steps
+        _star = self._star
+        self.pats = ((re.compile(_range + _steps), self._range_steps),
+                     (re.compile(_range), self._expand_range),
+                     (re.compile(_star + _steps), self._star_steps))
         self.max_ = max_
-        self.parser = groups
-
-    @staticmethod
-    def _expand_number(toks):
-        try:
-            i = int(toks[0])
-        except ValueError:
-            try:
-                i = weekday(toks[0])
-            except KeyError:
-                raise ValueError("Invalid weekday literal '%s'." % toks[0])
-        return [i]
 
-    @staticmethod
-    def _expand_range(toks):
+    def parse(self, spec):
+        acc = set()
+        for part in spec.split(','):
+            if part:
+                acc |= set(self._parse_part(part))
+            else:
+                raise self.ParseException("empty part")
+        return acc
+
+    def _parse_part(self, part):
+        for regex, handler in self.pats:
+            m = regex.match(part)
+            if m:
+                return handler(m.groups())
+        if part == '*':
+            return self._expand_star()
+        return self._expand_range((part, ))
+
+    def _expand_range(self, toks):
+        fr = self._expand_number(toks[0])
         if len(toks) > 1:
-            return range(toks[0], int(toks[2]) + 1)
+            to = self._expand_number(toks[1])
+            return range(fr, min(to + 1, self.max_ + 1))
         else:
-            return toks[0]
+            return [fr]
 
-    def _expand_star(self, toks):
-        return range(self.max_)
+    def _range_steps(self, toks):
+        if len(toks) != 3 or not toks[2]:
+            raise self.ParseException("empty filter")
+        return self._filter_steps(self._expand_range(toks[:2]), int(toks[2]))
 
-    @staticmethod
-    def _filter_steps(toks):
-        numbers = toks[0]
-        if len(toks) > 1:
-            steps = toks[2]
-            return [n for n in numbers if n % steps == 0]
-        else:
-            return numbers
+    def _star_steps(self, toks):
+        if not toks or not toks[0]:
+            raise self.ParseException("empty filter")
+        return self._filter_steps(self._expand_star(), int(toks[0]))
 
-    @staticmethod
-    def _ignore_comma(toks):
-        return [x for x in toks if x != ',']
+    def _filter_steps(self, numbers, steps):
+        return [n for n in numbers if n % steps == 0]
 
-    @staticmethod
-    def _join_to_set(toks):
-        return set(toks.asList())
+    def _expand_star(self):
+        return range(self.max_)
 
-    def parse(self, cronspec):
-        return self.parser.parseString(cronspec).pop()
+    def _expand_number(self, s):
+        if isinstance(s, basestring) and s[0] == '-':
+            raise self.ParseException("negative numbers not supported")
+        try:
+            i = int(s)
+        except ValueError:
+            try:
+                i = weekday(s)
+            except KeyError:
+                raise ValueError("Invalid weekday literal '%s'." % s)
+        return i
 
 
 class crontab(schedule):

+ 1 - 2
celery/tests/test_task/__init__.py

@@ -2,7 +2,6 @@ from datetime import datetime, timedelta
 from functools import wraps
 
 from mock import Mock
-from pyparsing import ParseException
 
 from celery import task
 from celery.app import app_or_default
@@ -10,7 +9,7 @@ from celery.task import task as task_dec
 from celery.exceptions import RetryTaskError
 from celery.execute import send_task
 from celery.result import EagerResult
-from celery.schedules import crontab, crontab_parser
+from celery.schedules import crontab, crontab_parser, ParseException
 from celery.utils import uuid
 from celery.utils.timeutils import parse_iso8601
 

+ 0 - 1
requirements/default.txt

@@ -1,4 +1,3 @@
 python-dateutil>=1.5.0,<2.0.0
 anyjson>=0.3.1
 kombu>=1.3.1,<2.0.0
-pyparsing>=1.5.0,<2.0.0

+ 0 - 1
setup.cfg

@@ -45,4 +45,3 @@ requires = uuid
            python-dateutil >= 1.5.0
            anyjson >= 0.3.1
            kombu >= 1.3.1
-           pyparsing >= 1.5.0

+ 0 - 1
setup.py

@@ -51,7 +51,6 @@ install_requires.extend([
     "python-dateutil>=1.5.0,<2.0.0",
     "anyjson>=0.3.1",
     "kombu>=1.3.1,<2.0.0",
-    "pyparsing>=1.5.0,<2.0.0",
 ])
 if is_py3k:
     install_requires.append("python-dateutil>2.0.0")