diff options
| author | Aymeric Augustin <aymeric.augustin@m4x.org> | 2011-11-18 13:01:06 +0000 |
|---|---|---|
| committer | Aymeric Augustin <aymeric.augustin@m4x.org> | 2011-11-18 13:01:06 +0000 |
| commit | 9b1cb755a28f020e27d4268c214b25315d4de42e (patch) | |
| tree | 2ff0827176f0eb49defa4ce7ce10164f2fc26e86 /django/utils | |
| parent | 01f70349c9ef23d6751437dcd57d2efc193b2661 (diff) | |
Added support for time zones. Thanks Luke Plant for the review. Fixed #2626.
For more information on this project, see this thread:
http://groups.google.com/group/django-developers/browse_thread/thread/cf0423bbb85b1bbf
git-svn-id: http://code.djangoproject.com/svn/django/trunk@17106 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/utils')
| -rw-r--r-- | django/utils/cache.py | 5 | ||||
| -rw-r--r-- | django/utils/dateformat.py | 14 | ||||
| -rw-r--r-- | django/utils/dateparse.py | 93 | ||||
| -rw-r--r-- | django/utils/feedgenerator.py | 5 | ||||
| -rw-r--r-- | django/utils/timesince.py | 16 | ||||
| -rw-r--r-- | django/utils/timezone.py | 266 | ||||
| -rw-r--r-- | django/utils/tzinfo.py | 19 |
7 files changed, 400 insertions, 18 deletions
diff --git a/django/utils/cache.py b/django/utils/cache.py index be4fa58645..1015c2f277 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -25,6 +25,7 @@ from django.conf import settings from django.core.cache import get_cache from django.utils.encoding import smart_str, iri_to_uri from django.utils.http import http_date +from django.utils.timezone import get_current_timezone_name from django.utils.translation import get_language cc_delim_re = re.compile(r'\s*,\s*') @@ -157,12 +158,14 @@ def has_vary_header(response, header_query): return header_query.lower() in existing_headers def _i18n_cache_key_suffix(request, cache_key): - """If enabled, returns the cache key ending with a locale.""" + """If necessary, adds the current locale or time zone to the cache key.""" if settings.USE_I18N or settings.USE_L10N: # first check if LocaleMiddleware or another middleware added # LANGUAGE_CODE to request, then fall back to the active language # which in turn can also fall back to settings.LANGUAGE_CODE cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language()) + if settings.USE_TZ: + cache_key += '.%s' % get_current_timezone_name() return cache_key def _generate_cache_key(request, method, headerlist, key_prefix): diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 0afda1850d..d87fb13105 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -14,10 +14,13 @@ Usage: import re import time import calendar +import datetime + from django.utils.dates import MONTHS, MONTHS_3, MONTHS_ALT, MONTHS_AP, WEEKDAYS, WEEKDAYS_ABBR from django.utils.tzinfo import LocalTimezone from django.utils.translation import ugettext as _ from django.utils.encoding import force_unicode +from django.utils.timezone import is_aware, is_naive re_formatchars = re.compile(r'(?<!\\)([aAbBcdDEfFgGhHiIjlLmMnNOPrsStTUuwWyYzZ])') re_escaped = re.compile(r'\\(.)') @@ -115,9 +118,12 @@ class DateFormat(TimeFormat): def __init__(self, dt): # Accepts either a datetime or date object. self.data = dt - self.timezone = getattr(dt, 'tzinfo', None) - if hasattr(self.data, 'hour') and not self.timezone: - self.timezone = LocalTimezone(dt) + self.timezone = None + if isinstance(dt, datetime.datetime): + if is_naive(dt): + self.timezone = LocalTimezone(dt) + else: + self.timezone = dt.tzinfo def b(self): "Month, textual, 3 letters, lowercase; e.g. 'jan'" @@ -218,7 +224,7 @@ class DateFormat(TimeFormat): def U(self): "Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)" - if getattr(self.data, 'tzinfo', None): + if isinstance(self.data, datetime.datetime) and is_aware(self.data): return int(calendar.timegm(self.data.utctimetuple())) else: return int(time.mktime(self.data.timetuple())) diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py new file mode 100644 index 0000000000..3ce475f5ea --- /dev/null +++ b/django/utils/dateparse.py @@ -0,0 +1,93 @@ +"""Functions to parse datetime objects.""" + +# We're using regular expressions rather than time.strptime because: +# - they provide both validation and parsing, +# - they're more flexible for datetimes, +# - the date/datetime/time constructors produce friendlier error messages. + + +import datetime +import re + +from django.utils.timezone import utc +from django.utils.tzinfo import FixedOffset + + +date_re = re.compile( + r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' +) + + +datetime_re = re.compile( + r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})' + r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})' + r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' + r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$' +) + + +time_re = re.compile( + r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' + r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' +) + + +def parse_date(value): + """Parse a string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Return None if the input isn't well formatted. + """ + match = date_re.match(value) + if match: + kw = dict((k, int(v)) for k, v in match.groupdict().iteritems()) + return datetime.date(**kw) + + +def parse_time(value): + """Parse a string and return a datetime.time. + + This function doesn't support time zone offsets. + + Sub-microsecond precision is accepted, but ignored. + + Raise ValueError if the input is well formatted but not a valid time. + Return None if the input isn't well formatted, in particular if it + contains an offset. + """ + match = time_re.match(value) + if match: + kw = match.groupdict() + if kw['microsecond']: + kw['microsecond'] = kw['microsecond'].ljust(6, '0') + kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) + return datetime.time(**kw) + + +def parse_datetime(value): + """Parse a string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses an instance of FixedOffset as tzinfo. + + Sub-microsecond precision is accepted, but ignored. + + Raise ValueError if the input is well formatted but not a valid datetime. + Return None if the input isn't well formatted. + """ + match = datetime_re.match(value) + if match: + kw = match.groupdict() + if kw['microsecond']: + kw['microsecond'] = kw['microsecond'].ljust(6, '0') + tzinfo = kw.pop('tzinfo') + if tzinfo == 'Z': + tzinfo = utc + elif tzinfo is not None: + offset = 60 * int(tzinfo[1:3]) + int(tzinfo[4:6]) + if tzinfo[0] == '-': + offset = -offset + tzinfo = FixedOffset(offset) + kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) + kw['tzinfo'] = tzinfo + return datetime.datetime(**kw) diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index 7d7c7af65c..df3cf41652 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -28,6 +28,7 @@ import urlparse from django.utils.xmlutils import SimplerXMLGenerator from django.utils.encoding import force_unicode, iri_to_uri from django.utils import datetime_safe +from django.utils.timezone import is_aware def rfc2822_date(date): # We can't use strftime() because it produces locale-dependant results, so @@ -40,7 +41,7 @@ def rfc2822_date(date): dow = days[date.weekday()] month = months[date.month - 1] time_str = date.strftime('%s, %%d %s %%Y %%H:%%M:%%S ' % (dow, month)) - if date.tzinfo: + if is_aware(date): offset = date.tzinfo.utcoffset(date) timezone = (offset.days * 24 * 60) + (offset.seconds // 60) hour, minute = divmod(timezone, 60) @@ -51,7 +52,7 @@ def rfc2822_date(date): def rfc3339_date(date): # Support datetime objects older than 1900 date = datetime_safe.new_datetime(date) - if date.tzinfo: + if is_aware(date): time_str = date.strftime('%Y-%m-%dT%H:%M:%S') offset = date.tzinfo.utcoffset(date) timezone = (offset.days * 24 * 60) + (offset.seconds // 60) diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 369a3e2c01..511acb518a 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -1,6 +1,6 @@ import datetime -from django.utils.tzinfo import LocalTimezone +from django.utils.timezone import is_aware, utc from django.utils.translation import ungettext, ugettext def timesince(d, now=None): @@ -31,13 +31,10 @@ def timesince(d, now=None): now = datetime.datetime(now.year, now.month, now.day) if not now: - if d.tzinfo: - now = datetime.datetime.now(LocalTimezone(d)) - else: - now = datetime.datetime.now() + now = datetime.datetime.now(utc if is_aware(d) else None) - # ignore microsecond part of 'd' since we removed it from 'now' - delta = now - (d - datetime.timedelta(0, 0, d.microsecond)) + delta = now - d + # ignore microseconds since = delta.days * 24 * 60 * 60 + delta.seconds if since <= 0: # d is in the future compared to now, stop processing. @@ -61,8 +58,5 @@ def timeuntil(d, now=None): the given time. """ if not now: - if getattr(d, 'tzinfo', None): - now = datetime.datetime.now(LocalTimezone(d)) - else: - now = datetime.datetime.now() + now = datetime.datetime.now(utc if is_aware(d) else None) return timesince(now, d) diff --git a/django/utils/timezone.py b/django/utils/timezone.py new file mode 100644 index 0000000000..22860eb8cc --- /dev/null +++ b/django/utils/timezone.py @@ -0,0 +1,266 @@ +"""Timezone helper functions. + +This module uses pytz when it's available and fallbacks when it isn't. +""" + +from datetime import datetime, timedelta, tzinfo +from threading import local +import time as _time + +try: + import pytz +except ImportError: + pytz = None + +from django.conf import settings + +__all__ = [ + 'utc', 'get_default_timezone', 'get_current_timezone', + 'activate', 'deactivate', 'override', + 'aslocaltime', 'isnaive', +] + + +# UTC and local time zones + +ZERO = timedelta(0) + +class UTC(tzinfo): + """ + UTC implementation taken from Python's docs. + + Used only when pytz isn't available. + """ + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + +class LocalTimezone(tzinfo): + """ + 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. + """ + + def __init__(self): + # This code is moved in __init__ to execute it as late as possible + # See get_default_timezone(). + self.STDOFFSET = timedelta(seconds=-_time.timezone) + if _time.daylight: + self.DSTOFFSET = timedelta(seconds=-_time.altzone) + else: + self.DSTOFFSET = self.STDOFFSET + self.DSTDIFF = self.DSTOFFSET - self.STDOFFSET + tzinfo.__init__(self) + + def utcoffset(self, dt): + if self._isdst(dt): + return self.DSTOFFSET + else: + return self.STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return self.DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return _time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, 0) + stamp = _time.mktime(tt) + tt = _time.localtime(stamp) + return tt.tm_isdst > 0 + + +utc = pytz.utc if pytz else UTC() +"""UTC time zone as a tzinfo instance.""" + +# In order to avoid accessing the settings at compile time, +# wrap the expression in a function and cache the result. +# If you change settings.TIME_ZONE in tests, reset _localtime to None. +_localtime = None + +def get_default_timezone(): + """ + Returns the default time zone as a tzinfo instance. + + This is the time zone defined by settings.TIME_ZONE. + + See also :func:`get_current_timezone`. + """ + global _localtime + if _localtime is None: + tz = settings.TIME_ZONE + _localtime = pytz.timezone(tz) if pytz else LocalTimezone() + return _localtime + +# This function exists for consistency with get_current_timezone_name +def get_default_timezone_name(): + """ + Returns the name of the default time zone. + """ + return _get_timezone_name(get_default_timezone()) + +_active = local() + +def get_current_timezone(): + """ + Returns the currently active time zone as a tzinfo instance. + """ + return getattr(_active, "value", get_default_timezone()) + +def get_current_timezone_name(): + """ + Returns the name of the currently active time zone. + """ + return _get_timezone_name(get_current_timezone()) + +def _get_timezone_name(timezone): + """ + Returns the name of ``timezone``. + """ + try: + # for pytz timezones + return timezone.zone + except AttributeError: + # for regular tzinfo objects + local_now = datetime.now(timezone) + return timezone.tzname(local_now) + +# Timezone selection functions. + +# These functions don't change os.environ['TZ'] and call time.tzset() +# because it isn't thread safe. + +def activate(timezone): + """ + Sets the time zone for the current thread. + + The ``timezone`` argument must be an instance of a tzinfo subclass or a + time zone name. If it is a time zone name, pytz is required. + """ + if isinstance(timezone, tzinfo): + _active.value = timezone + elif isinstance(timezone, basestring) and pytz is not None: + _active.value = pytz.timezone(timezone) + else: + raise ValueError("Invalid timezone: %r" % timezone) + +def deactivate(): + """ + Unsets the time zone for the current thread. + + Django will then use the time zone defined by settings.TIME_ZONE. + """ + if hasattr(_active, "value"): + del _active.value + +class override(object): + """ + Temporarily set the time zone for the current thread. + + This is a context manager that uses ``~django.utils.timezone.activate()`` + to set the timezone on entry, and restores the previously active timezone + on exit. + + The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a + time zone name, or ``None``. If is it a time zone name, pytz is required. + If it is ``None``, Django enables the default time zone. + """ + def __init__(self, timezone): + self.timezone = timezone + self.old_timezone = getattr(_active, 'value', None) + + def __enter__(self): + if self.timezone is None: + deactivate() + else: + activate(self.timezone) + + def __exit__(self, exc_type, exc_value, traceback): + if self.old_timezone is not None: + _active.value = self.old_timezone + else: + del _active.value + + +# Utilities + +def aslocaltime(value, use_tz=None): + """ + Checks if value is a datetime and converts it to local time if necessary. + + If use_tz is provided and is not None, that will force the value to + be converted (or not), overriding the value of settings.USE_TZ. + """ + if (isinstance(value, datetime) + and (settings.USE_TZ if use_tz is None else use_tz) + and not is_naive(value) + and getattr(value, 'convert_to_local_time', True)): + timezone = get_current_timezone() + value = value.astimezone(timezone) + if hasattr(timezone, 'normalize'): + # available for pytz time zones + value = timezone.normalize(value) + return value + +def now(): + """ + Returns an aware or naive datetime.datetime, depending on settings.USE_TZ. + """ + if settings.USE_TZ: + # timeit shows that datetime.now(tz=utc) is 24% slower + return datetime.utcnow().replace(tzinfo=utc) + else: + return datetime.now() + +def is_aware(value): + """ + Determines if a given datetime.datetime is aware. + + The logic is described in Python's docs: + http://docs.python.org/library/datetime.html#datetime.tzinfo + """ + return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None + +def is_naive(value): + """ + Determines if a given datetime.datetime is naive. + + The logic is described in Python's docs: + http://docs.python.org/library/datetime.html#datetime.tzinfo + """ + return value.tzinfo is None or value.tzinfo.utcoffset(value) is None + +def make_aware(value, timezone): + """ + Makes a naive datetime.datetime in a given time zone aware. + """ + if hasattr(timezone, 'localize'): + # available for pytz time zones + return timezone.localize(value, is_dst=None) + else: + # may be wrong around DST changes + return value.replace(tzinfo=timezone) + +def make_naive(value, timezone): + """ + Makes an aware datetime.datetime naive in a given time zone. + """ + value = value.astimezone(timezone) + if hasattr(timezone, 'normalize'): + # available for pytz time zones + return timezone.normalize(value) + return value.replace(tzinfo=None) diff --git a/django/utils/tzinfo.py b/django/utils/tzinfo.py index daffffb496..a07b635a99 100644 --- a/django/utils/tzinfo.py +++ b/django/utils/tzinfo.py @@ -2,8 +2,14 @@ import time from datetime import timedelta, tzinfo + from django.utils.encoding import smart_unicode, smart_str, DEFAULT_LOCALE_ENCODING +# Python's doc say: "A tzinfo subclass must have an __init__() method that can +# be called with no arguments". FixedOffset and LocalTimezone don't honor this +# requirement. Defining __getinitargs__ is sufficient to fix copy/deepcopy as +# well as pickling/unpickling. + class FixedOffset(tzinfo): "Fixed offset in minutes east from UTC." def __init__(self, offset): @@ -19,6 +25,9 @@ class FixedOffset(tzinfo): def __repr__(self): return self.__name + def __getinitargs__(self): + return self.__offset, + def utcoffset(self, dt): return self.__offset @@ -28,15 +37,25 @@ class FixedOffset(tzinfo): def dst(self, dt): return timedelta(0) +# This implementation is used for display purposes. It uses an approximation +# for DST computations on dates >= 2038. + +# A similar implementation exists in django.utils.timezone. It's used for +# timezone support (when USE_TZ = True) and focuses on correctness. + class LocalTimezone(tzinfo): "Proxy timezone information from time module." def __init__(self, dt): tzinfo.__init__(self) + self.__dt = dt self._tzname = self.tzname(dt) def __repr__(self): return smart_str(self._tzname) + def __getinitargs__(self): + return self.__dt, + def utcoffset(self, dt): if self._isdst(dt): return timedelta(seconds=-time.altzone) |
