Explorar o código

No longer depends on python-dateutil, but depends on pytz instead

Ask Solem %!s(int64=12) %!d(string=hai) anos
pai
achega
c3c32bb726

+ 3 - 2
celery/app/builtins.py

@@ -221,8 +221,9 @@ def add_chain_task(app):
                     # set the results parent attribute.
                     # set the results parent attribute.
                     res.parent = prev_res
                     res.parent = prev_res
 
 
-                results.append(res)
-                tasks.append(task)
+                if not isinstance(prev_task, chord):
+                    results.append(res)
+                    tasks.append(task)
                 prev_task, prev_res = task, res
                 prev_task, prev_res = task, res
 
 
             return tasks, results
             return tasks, results

+ 27 - 27
celery/schedules.py

@@ -13,14 +13,13 @@ import re
 
 
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 
 
-from dateutil.relativedelta import relativedelta
 from kombu.utils import cached_property
 from kombu.utils import cached_property
 
 
 from . import current_app
 from . import current_app
 from .utils import is_iterable
 from .utils import is_iterable
 from .utils.timeutils import (
 from .utils.timeutils import (
     timedelta_seconds, weekday, maybe_timedelta, remaining,
     timedelta_seconds, weekday, maybe_timedelta, remaining,
-    humanize_seconds, timezone, maybe_make_aware
+    humanize_seconds, timezone, maybe_make_aware, ffwd
 )
 )
 from .datastructures import AttributeDict
 from .datastructures import AttributeDict
 
 
@@ -400,13 +399,13 @@ class crontab(schedule):
             datedata.dom += 1
             datedata.dom += 1
             roll_over()
             roll_over()
 
 
-        return relativedelta(year=datedata.year,
-                             month=months_of_year[datedata.moy],
-                             day=days_of_month[datedata.dom],
-                             hour=next_hour,
-                             minute=next_minute,
-                             second=0,
-                             microsecond=0)
+        return ffwd(year=datedata.year,
+                    month=months_of_year[datedata.moy],
+                    day=days_of_month[datedata.dom],
+                    hour=next_hour,
+                    minute=next_minute,
+                    second=0,
+                    microsecond=0)
 
 
     def __init__(self, minute='*', hour='*', day_of_week='*',
     def __init__(self, minute='*', hour='*', day_of_week='*',
             day_of_month='*', month_of_year='*', nowfun=None):
             day_of_month='*', month_of_year='*', nowfun=None):
@@ -440,9 +439,7 @@ class crontab(schedule):
                                  self._orig_day_of_month,
                                  self._orig_day_of_month,
                                  self._orig_month_of_year), None)
                                  self._orig_month_of_year), None)
 
 
-    def remaining_estimate(self, last_run_at, tz=None):
-        """Returns when the periodic task should run next as a timedelta."""
-        tz = tz or self.tz
+    def remaining_delta(self, last_run_at, ffwd=ffwd):
         last_run_at = self.maybe_make_aware(last_run_at)
         last_run_at = self.maybe_make_aware(last_run_at)
         dow_num = last_run_at.isoweekday() % 7  # Sunday is day 0, not day 7
         dow_num = last_run_at.isoweekday() % 7  # Sunday is day 0, not day 7
 
 
@@ -457,9 +454,9 @@ class crontab(schedule):
         if execute_this_hour:
         if execute_this_hour:
             next_minute = min(minute for minute in self.minute
             next_minute = min(minute for minute in self.minute
                                         if minute > last_run_at.minute)
                                         if minute > last_run_at.minute)
-            delta = relativedelta(minute=next_minute,
-                                  second=0,
-                                  microsecond=0)
+            delta = ffwd(minute=next_minute,
+                         second=0,
+                         microsecond=0)
         else:
         else:
             next_minute = min(self.minute)
             next_minute = min(self.minute)
             execute_today = (execute_this_date and
             execute_today = (execute_this_date and
@@ -468,10 +465,10 @@ class crontab(schedule):
             if execute_today:
             if execute_today:
                 next_hour = min(hour for hour in self.hour
                 next_hour = min(hour for hour in self.hour
                                         if hour > last_run_at.hour)
                                         if hour > last_run_at.hour)
-                delta = relativedelta(hour=next_hour,
-                                      minute=next_minute,
-                                      second=0,
-                                      microsecond=0)
+                delta = ffwd(hour=next_hour,
+                             minute=next_minute,
+                             second=0,
+                             microsecond=0)
             else:
             else:
                 next_hour = min(self.hour)
                 next_hour = min(self.hour)
                 all_dom_moy = (self._orig_day_of_month == '*' and
                 all_dom_moy = (self._orig_day_of_month == '*' and
@@ -482,19 +479,22 @@ class crontab(schedule):
                                 self.day_of_week)
                                 self.day_of_week)
                     add_week = next_day == dow_num
                     add_week = next_day == dow_num
 
 
-                    delta = relativedelta(weeks=add_week and 1 or 0,
-                                          weekday=(next_day - 1) % 7,
-                                          hour=next_hour,
-                                          minute=next_minute,
-                                          second=0,
-                                          microsecond=0)
+                    delta = ffwd(weeks=add_week and 1 or 0,
+                                 weekday=(next_day - 1) % 7,
+                                 hour=next_hour,
+                                 minute=next_minute,
+                                 second=0,
+                                 microsecond=0)
                 else:
                 else:
                     delta = self._delta_to_next(last_run_at,
                     delta = self._delta_to_next(last_run_at,
                                                 next_hour, next_minute)
                                                 next_hour, next_minute)
 
 
         now = self.maybe_make_aware(self.now())
         now = self.maybe_make_aware(self.now())
-        return remaining(self.to_local(last_run_at), delta,
-                         self.to_local(now))
+        return self.to_local(last_run_at), delta, self.to_local(now)
+
+    def remaining_estimate(self, last_run_at, ffwd=ffwd):
+        """Returns when the periodic task should run next as a timedelta."""
+        return remaining(*self.remaining_delta(last_run_at, ffwd=ffwd))
 
 
     def is_due(self, last_run_at):
     def is_due(self, last_run_at):
         """Returns tuple of two items `(is_due, next_time_to_run)`,
         """Returns tuple of two items `(is_due, next_time_to_run)`,

+ 22 - 0
celery/tests/tasks/test_tasks.py

@@ -1035,9 +1035,31 @@ class test_crontab_is_due(Case):
             else:
             else:
                 break
                 break
 
 
+    def assertRelativedelta(self, due, last_ran):
+        try:
+            from dateutil.relativedelta import relativedelta
+        except ImportError:
+            return
+        l1, d1, n1 = due.run_every.remaining_delta(last_ran)
+        l2, d2, n2 = due.run_every.remaining_delta(last_ran,
+                                                   ffwd=relativedelta)
+        if not isinstance(d1, relativedelta):
+            self.assertEqual(l1, l2)
+            for field, value in d1._fields().iteritems():
+                self.assertEqual(getattr(d1, field), value)
+            self.assertFalse(d2.years)
+            self.assertFalse(d2.months)
+            self.assertFalse(d2.days)
+            self.assertFalse(d2.leapdays)
+            self.assertFalse(d2.hours)
+            self.assertFalse(d2.minutes)
+            self.assertFalse(d2.seconds)
+            self.assertFalse(d2.microseconds)
+
     def test_every_minute_execution_is_due(self):
     def test_every_minute_execution_is_due(self):
         last_ran = self.now - timedelta(seconds=61)
         last_ran = self.now - timedelta(seconds=61)
         due, remaining = every_minute.run_every.is_due(last_ran)
         due, remaining = every_minute.run_every.is_due(last_ran)
+        self.assertRelativedelta(every_minute, last_ran)
         self.assertTrue(due)
         self.assertTrue(due)
         self.seconds_almost_equal(remaining, self.next_minute, 1)
         self.seconds_almost_equal(remaining, self.next_minute, 1)
 
 

+ 0 - 9
celery/tests/utilities/test_timeutils.py

@@ -79,12 +79,3 @@ class test_timezone(Case):
             self.assertTrue(timezone.get_timezone('UTC'))
             self.assertTrue(timezone.get_timezone('UTC'))
         finally:
         finally:
             timeutils.pytz = prev
             timeutils.pytz = prev
-
-    def test_get_timezone_without_pytz(self):
-        prev, timeutils.pytz = timeutils.pytz, None
-        try:
-            self.assertTrue(timezone.get_timezone('UTC'))
-            with self.assertRaises(ImproperlyConfigured):
-                timezone.get_timezone('Europe/Oslo')
-        finally:
-            timeutils.pytz = prev

+ 1 - 0
celery/utils/__init__.py

@@ -247,6 +247,7 @@ def gen_task_name(app, name, module_name):
         return '.'.join([app.main, name])
         return '.'.join([app.main, name])
     return '.'.join(filter(None, [module_name, name]))
     return '.'.join(filter(None, [module_name, name]))
 
 
+
 # ------------------------------------------------------------------------ #
 # ------------------------------------------------------------------------ #
 # > XXX Compat
 # > XXX Compat
 from .log import LOG_LEVELS     # noqa
 from .log import LOG_LEVELS     # noqa

+ 5 - 0
celery/utils/functional.py

@@ -261,3 +261,8 @@ class _regen(UserList, list):
     @cached_property
     @cached_property
     def data(self):
     def data(self):
         return list(self.__it)
         return list(self.__it)
+
+
+def dictfilter(d, **keys):
+    d = dict(d, **keys) if keys else d
+    return dict((k, v) for k, v in d.iteritems() if v is not None)

+ 52 - 22
celery/utils/timeutils.py

@@ -12,25 +12,20 @@ import os
 import time as _time
 import time as _time
 from itertools import izip
 from itertools import izip
 
 
-from datetime import datetime, timedelta, tzinfo
+from calendar import monthrange
+from datetime import date, datetime, timedelta, tzinfo
 
 
-from dateutil import tz
-from dateutil.parser import parse as parse_iso8601
-from kombu.utils import cached_property
+from kombu.utils import cached_property, reprcall
 
 
-from celery.exceptions import ImproperlyConfigured
+from pytz import timezone as _timezone
 
 
+from .functional import dictfilter
+from .iso8601 import parse_iso8601
 from .text import pluralize
 from .text import pluralize
 
 
-try:
-    import pytz
-except ImportError:     # pragma: no cover
-    pytz = None         # noqa
-
 
 
 C_REMDEBUG = os.environ.get('C_REMDEBUG', False)
 C_REMDEBUG = os.environ.get('C_REMDEBUG', False)
 
 
-
 DAYNAMES = 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
 DAYNAMES = 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
 WEEKDAYS = dict(izip(DAYNAMES, range(7)))
 WEEKDAYS = dict(izip(DAYNAMES, range(7)))
 
 
@@ -54,8 +49,7 @@ _local_timezone = None
 class LocalTimezone(tzinfo):
 class LocalTimezone(tzinfo):
     """Local time implementation taken from Python's docs.
     """Local time implementation taken from Python's docs.
 
 
-    Used only when pytz isn't available, and most likely inaccurate. If you're
-    having trouble with this class, don't waste your time, just install pytz.
+    Used only when UTC is not enabled.
     """
     """
 
 
     def __init__(self):
     def __init__(self):
@@ -122,12 +116,7 @@ class _Zone(object):
 
 
     def get_timezone(self, zone):
     def get_timezone(self, zone):
         if isinstance(zone, basestring):
         if isinstance(zone, basestring):
-            if pytz is None:
-                if zone == 'UTC':
-                    return tz.gettz('UTC')
-                raise ImproperlyConfigured(
-                    'Timezones requires the pytz library')
-            return pytz.timezone(zone)
+            return _timezone(zone)
         return zone
         return zone
 
 
     @cached_property
     @cached_property
@@ -192,7 +181,7 @@ def delta_resolution(dt, delta):
     return dt
     return dt
 
 
 
 
-def remaining(start, ends_in, now=None, relative=False, debug=False):
+def remaining(start, ends_in, now=None, relative=False):
     """Calculate the remaining time for a start date and a timedelta.
     """Calculate the remaining time for a start date and a timedelta.
 
 
     e.g. "how many seconds left for 30 seconds after start?"
     e.g. "how many seconds left for 30 seconds after start?"
@@ -212,8 +201,8 @@ def remaining(start, ends_in, now=None, relative=False, debug=False):
         end_date = delta_resolution(end_date, ends_in)
         end_date = delta_resolution(end_date, ends_in)
     ret = end_date - now
     ret = end_date - now
     if C_REMDEBUG:
     if C_REMDEBUG:
-        print('rem: NOW:%s START:%s END_DATE:%s REM:%s' % (
-            now, start, end_date, ret))
+        print('rem: NOW:%r START:%r ENDS_IN:%r END_DATE:%s REM:%s' % (
+            now, start, ends_in, end_date, ret))
     return ret
     return ret
 
 
 
 
@@ -309,3 +298,44 @@ def maybe_make_aware(dt, tz=None):
         dt = to_utc(dt)
         dt = to_utc(dt)
     return localize(dt,
     return localize(dt,
         timezone.utc if tz is None else timezone.tz_or_local(tz))
         timezone.utc if tz is None else timezone.tz_or_local(tz))
+
+
+class ffwd(object):
+    """Version of relativedelta that only supports addition."""
+
+    def __init__(self, year=None, month=None, weeks=0, weekday=None, day=None,
+            hour=None, minute=None, second=None, microsecond=None, **kwargs):
+        self.year = year
+        self.month = month
+        self.weeks = weeks
+        self.weekday = weekday
+        self.day = day
+        self.hour = hour
+        self.minute = minute
+        self.second = second
+        self.microsecond = microsecond
+        self.days = weeks * 7
+        self._has_time = self.hour is not None or self.minute is not None
+
+    def __repr__(self):
+        return reprcall('ffwd', (), self._fields(weeks=self.weeks,
+                                                 weekday=self.weekday))
+
+    def __radd__(self, other):
+        if not isinstance(other, date):
+            return NotImplemented
+        year = self.year or other.year
+        month = self.month or other.month
+        day = min(monthrange(year, month)[1], self.day or other.day)
+        ret = other.replace(**dict(dictfilter(self._fields()),
+                            year=year, month=month, day=day))
+        if self.weekday is not None:
+            ret += timedelta(days=(7 - ret.weekday() + self.weekday) % 7)
+        return ret + timedelta(days=self.days)
+
+    def _fields(self, **extra):
+        return dictfilter({
+            'year': self.year, 'month': self.month, 'day': self.day,
+            'hour': self.hour, 'minute': self.minute,
+            'second': self.second, 'microsecond': self.microsecond,
+        }, **extra)

+ 3 - 17
docs/configuration.rst

@@ -67,24 +67,10 @@ CELERY_TIMEZONE
 
 
 Configure Celery to use a custom time zone.
 Configure Celery to use a custom time zone.
 The timezone value can be any time zone supported by the :mod:`pytz`
 The timezone value can be any time zone supported by the :mod:`pytz`
-library.  :mod:`pytz` must be installed for the selected zone
-to be used.
+library.
 
 
-If not set then the systems default local time zone is used.
-
-.. warning::
-
-    Celery requires the :mod:`pytz` library to be installed,
-    when using custom time zones (other than UTC).  You can
-    install it using :program:`pip` or :program:`easy_install`:
-
-    .. code-block:: bash
-
-        $ pip install pytz
-
-    Pytz is a library that defines the timzones of the world,
-    it changes quite frequently so it is not included in the Python Standard
-    Library.
+If not set then the UTC timezone is used if :setting:`CELERY_ENABLE_UTC` is
+enabled, otherwise it falls back to the local timezone.
 
 
 .. _conf-tasks:
 .. _conf-tasks:
 
 

+ 5 - 6
docs/faq.rst

@@ -98,17 +98,16 @@ Billiard is a fork of the Python multiprocessing module containing
 many performance and stability improvements.  It is an eventual goal
 many performance and stability improvements.  It is an eventual goal
 that these improvements will be merged back into Python one day.
 that these improvements will be merged back into Python one day.
 
 
-It is also used for compatibility with older Python versions.
+It is also used for compatibility with older Python versions
+that doesn't come with the multiprocessing module.
 
 
 .. _`billiard`: http://pypi.python.org/pypi/billiard
 .. _`billiard`: http://pypi.python.org/pypi/billiard
 
 
-- `python-dateutil`_
+- `pytz`
 
 
-The dateutil module is used by Celery to parse ISO-8601 formatted time strings,
-as well as its ``relativedelta`` class which is used in the implementation
-of crontab style periodic tasks.
+The pytz module provides timezone definitions and related tools.
 
 
-.. _`python-dateutil`: http://pypi.python.org/pypi/python-dateutil
+.. _`pytz`: http://pypi.python.org/pypi/pytz
 
 
 django-celery
 django-celery
 ~~~~~~~~~~~~~
 ~~~~~~~~~~~~~

+ 1 - 9
docs/getting-started/next-steps.rst

@@ -675,15 +675,7 @@ All times and dates, internally and in messages uses the UTC timezone.
 When the worker receives a message, for example with a countdown set it
 When the worker receives a message, for example with a countdown set it
 converts that UTC time to local time.  If you wish to use
 converts that UTC time to local time.  If you wish to use
 a different timezone than the system timezone then you must
 a different timezone than the system timezone then you must
-configure that using the :setting:`CELERY_TIMEZONE` setting.
-
-To use custom timezones you also have to install the :mod:`pytz` library:
-
-.. code-block:: bash
-
-    $ pip install pytz
-
-Setting a custom timezone::
+configure that using the :setting:`CELERY_TIMEZONE` setting::
 
 
     celery.conf.CELERY_TIMEZONE = 'Europe/London'
     celery.conf.CELERY_TIMEZONE = 'Europe/London'
 
 

+ 0 - 7
docs/userguide/application.rst

@@ -146,13 +146,6 @@ that are consulted in order:
 ``config_from_object``
 ``config_from_object``
 ----------------------
 ----------------------
 
 
-.. sidebar:: Timezones & pytz
-
-    Setting a time zone other than UTC requires the :mod:`pytz` library
-    to be installed, see the :setting:`CELERY_TIMEZONE` setting for more
-    information.
-
-
 The :meth:`@Celery.config_from_object` method loads configuration
 The :meth:`@Celery.config_from_object` method loads configuration
 from a configuration object.
 from a configuration object.
 
 

+ 0 - 22
docs/userguide/periodic-tasks.rst

@@ -31,15 +31,6 @@ The periodic task schedules uses the UTC time zone by default,
 but you can change the time zone used using the :setting:`CELERY_TIMEZONE`
 but you can change the time zone used using the :setting:`CELERY_TIMEZONE`
 setting.
 setting.
 
 
-If you use a time zone other than UTC it's recommended to install the
-:mod:`pytz` library as this can improve the accuracy and keep your timezone
-specifications up to date:
-
-.. code-block:: bash
-
-    $ pip install -U pytz
-
-
 An example time zone could be `Europe/London`:
 An example time zone could be `Europe/London`:
 
 
 .. code-block:: python
 .. code-block:: python
@@ -231,19 +222,6 @@ the :setting:`CELERY_TIMEZONE` setting:
     Celery is also compatible with the new ``USE_TZ`` setting introduced
     Celery is also compatible with the new ``USE_TZ`` setting introduced
     in Django 1.4.
     in Django 1.4.
 
 
-.. note::
-
-    The `pytz`_ library is recommended when setting a default timezone.
-    If :mod:`pytz` is not installed it will fallback to the mod:`dateutil`
-    library, which depends on a system timezone file being available for
-    the timezone selected.
-
-    Timezone definitions change frequently, so for the best results
-    an up to date :mod:`pytz` installation should be used.
-
-
-.. _`pytz`: http://pypi.python.org/pypi/pytz/
-
 .. _beat-starting:
 .. _beat-starting:
 
 
 Starting the Scheduler
 Starting the Scheduler

+ 1 - 1
requirements/default.txt

@@ -1,3 +1,3 @@
+pytz
 billiard>=2.7.3.13
 billiard>=2.7.3.13
-python-dateutil>=2.1
 kombu>=2.4.6,<3.0
 kombu>=2.4.6,<3.0

+ 2 - 2
setup.cfg

@@ -14,6 +14,6 @@ all_files = 1
 upload-dir = docs/.build/html
 upload-dir = docs/.build/html
 
 
 [bdist_rpm]
 [bdist_rpm]
-requires = billiard>=2.7.3.13
-           python-dateutil >= 2.1
+requires = pytz
+           billiard>=2.7.3.13
            kombu >= 2.4.6
            kombu >= 2.4.6