# HG changeset patch # User cmlenz # Date 1180954298 0 # Node ID 3956bb7ff93bdfc85f739dd0b82dd21649c5939b # Parent 9a00ac84004c36fa4a960b3e7d5bf6cf2827825a More work on timezones. diff --git a/babel/dates.py b/babel/dates.py --- a/babel/dates.py +++ b/babel/dates.py @@ -21,10 +21,10 @@ * ``LANG`` """ -from datetime import date, datetime, time +from datetime import date, datetime, time, timedelta, tzinfo from babel.core import Locale -from babel.util import default_locale +from babel.util import default_locale, UTC __all__ = ['format_date', 'format_datetime', 'format_time', 'parse_date', 'parse_datetime', 'parse_time'] @@ -157,7 +157,7 @@ If you don't want to use the locale default formats, you can specify a custom date pattern: - >>> format_time(d, "EEE, MMM d, ''yy", locale='en') + >>> format_date(d, "EEE, MMM d, ''yy", locale='en') u"Sun, Apr 1, '07" :param date: the ``date`` or ``datetime`` object @@ -179,12 +179,13 @@ pattern = parse_pattern(format) return parse_pattern(format).apply(date, locale) -def format_datetime(datetime, format='medium', locale=LC_TIME): +def format_datetime(datetime, format='medium', tzinfo=UTC, locale=LC_TIME): """Returns a date formatted according to the given pattern. :param datetime: the ``date`` object :param format: one of "full", "long", "medium", or "short", or a custom date/time pattern + :param tzinfo: the timezone to apply to the time for display :param locale: a `Locale` object or a locale identifier :rtype: `unicode` """ @@ -194,7 +195,7 @@ pattern = parse_pattern(format) return parse_pattern(format).apply(datetime, locale) -def format_time(time, format='medium', locale=LC_TIME): +def format_time(time, format='medium', tzinfo=UTC, locale=LC_TIME): """Returns a time formatted according to the given pattern. >>> t = time(15, 30) @@ -209,9 +210,18 @@ >>> format_time(t, "hh 'o''clock' a", locale='en') u"03 o'clock PM" + For any pattern requiring the display of the time-zone, the third-party + ``pytz`` package is needed to explicitly specify the time-zone: + + >>> from pytz import timezone + >>> cet = timezone('Europe/Berlin') + >>> format_time(t, format='full', tzinfo=cet, locale='de_DE') + u'15:30 Uhr MEZ' + :param time: the ``time`` or ``datetime`` object :param format: one of "full", "long", "medium", or "short", or a custom date/time pattern + :param tzinfo: the time-zone to apply to the time for display :param locale: a `Locale` object or a locale identifier :rtype: `unicode` @@ -224,6 +234,8 @@ time = datetime.fromtimestamp(time).time() elif isinstance(time, datetime): time = time.time() + if time.tzinfo is None: + time = time.replace(tzinfo=tzinfo) locale = Locale.parse(locale) if format in ('full', 'long', 'medium', 'short'): format = get_time_format(format, locale=locale) @@ -263,6 +275,8 @@ def __init__(self, value, locale): assert isinstance(value, (date, datetime, time)) + if isinstance(value, (datetime, time)) and value.tzinfo is None: + value = value.replace(tzinfo=UTC) self.value = value self.locale = Locale.parse(locale) @@ -296,6 +310,8 @@ return self.format(self.value.minute, num) elif char == 's': return self.format(self.value.second, num) + elif char in ('z', 'Z', 'v'): + return self.format_timezone(char, num) else: raise KeyError('Unsupported date/time field %r' % char) @@ -336,6 +352,31 @@ period = {0: 'am', 1: 'pm'}[int(self.value.hour > 12)] return get_period_names(locale=self.locale)[period] + def format_timezone(self, char, num): + if char == 'z': + zone = self.value.tzinfo.zone + if num < 4: + return self.locale.time_zones[zone]['short'][ + self.value.dst() and 'daylight' or 'standard' + ] + else: + return self.locale.time_zones[zone]['long'][ + self.value.dst() and 'daylight' or 'standard' + ] + + elif char == 'Z': + offset = self.value.utcoffset() + hours, seconds = divmod(offset.seconds, 3600) + minutes = seconds // 60 + sign = '+' + if offset.seconds < 0: + sign = '-' + pattern = {3: '%s%02d%02d', 4: 'GMT %s%02d:%02d'}[max(3, num)] + return pattern % (sign, hours, minutes) + + elif char == 'v': + raise NotImplementedError + def format(self, value, length): return ('%%0%dd' % length) % value diff --git a/babel/tests/dates.py b/babel/tests/dates.py --- a/babel/tests/dates.py +++ b/babel/tests/dates.py @@ -15,6 +15,8 @@ import doctest import unittest +from pytz import timezone + from babel import dates @@ -54,6 +56,19 @@ fmt = dates.DateTimeFormat(d, locale='dv_MV') self.assertEqual('4', fmt['c']) # friday is first day of week + def test_timezone_rfc822(self): + tz = timezone('Europe/Berlin') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='de_DE') + self.assertEqual('+0100', fmt['Z']) + + def test_timezone_gmt(self): + tz = timezone('Europe/Berlin') + t = time(15, 30, tzinfo=tz) + fmt = dates.DateTimeFormat(t, locale='de_DE') + self.assertEqual('GMT +01:00', fmt['ZZZZ']) + + class FormatDateTestCase(unittest.TestCase): diff --git a/babel/util.py b/babel/util.py --- a/babel/util.py +++ b/babel/util.py @@ -13,10 +13,11 @@ """Various utility classes and functions.""" +from datetime import tzinfo import os import re -__all__ = ['default_locale', 'extended_glob', 'LazyProxy'] +__all__ = ['default_locale', 'extended_glob', 'relpath', 'LazyProxy', 'UTC'] __docformat__ = 'restructuredtext en' def default_locale(kind=None): @@ -75,6 +76,7 @@ if regex.match(filepath): yield filepath + class LazyProxy(object): """Class for proxy objects that delegate to a specified function to evaluate the actual object. @@ -209,6 +211,11 @@ relpath = os.path.relpath except AttributeError: def relpath(path, start='.'): + """Compute the relative path to one path from another. + + :return: the relative path + :rtype: `basestring` + """ start_list = os.path.abspath(start).split(os.sep) path_list = os.path.abspath(path).split(os.sep) @@ -217,3 +224,32 @@ rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] return os.path.join(*rel_list) + +try: + from pytz import UTC +except ImportError: + ZERO = timedelta(0) + + class UTC(tzinfo): + """Simple `tzinfo` implementation for UTC.""" + + def __repr__(self): + return '' + + def __str__(self): + return 'UTC' + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return 'UTC' + + def dst(self, dt): + return ZERO + + UTC = UTC() + """`tzinfo` object for UTC (Universal Time). + + :type: `tzinfo` + """ diff --git a/doc/formatting.txt b/doc/formatting.txt --- a/doc/formatting.txt +++ b/doc/formatting.txt @@ -192,6 +192,35 @@ +----------+--------+--------------------------------------------------------+ +Time-zone Support +----------------- + +Many of the verbose default time formats include the time-zone, but the +time-zone is not by default available for the Python ``datetime`` and ``time`` +objects. The standard library includes only the abstract ``tzinfo`` class, +which you need appropriate implementations for to actually use in your +application. Babel includes a ``tzinfo`` implementation for UTC (Universal +Time). For actual time-zones, it is strongly recommended that you use the +third-party package `pytz`_, which includes the definitions of practically all +of the time-zones used on the world, as well as important functions for +reliably converting from UTC to local time, and vice versa:: + + >>> from datetime import time + >>> t = time(15, 30) + + >>> from pytz import timezone + >>> cet = timezone('Europe/Berlin') + >>> format_time(t, 'H:mm Z', tzinfo=cet, locale='de_DE') + u'15:30 +0100' + +The recommended approach to deal with different time-zones in a Python +application is to always use UTC internally, and only convert from/to the users +time-zone when accepting user input and displaying date/time data, respectively. + + .. _`pytz`: http://pytz.sourceforge.net/ + + + Parsing Dates -------------