[date] Fix utcdatetime

Dates with a tzinfo get messed up by utcdatetime.

This fix import the UTC timezone from pytz, and use a copy of it if pytz is not present.

Closes #280794

authorChristophe de Vienne <christophe@unlish.com>
changeset65e0b3c67247
branchdefault
phasedraft
hiddenyes
parent revision#c7dbf7e25ee6 [coverage] Provides better tools to pause tracing
child revision<not specified>
files modified by this revision
date.py
test/unittest_date.py
# HG changeset patch
# User Christophe de Vienne <christophe@unlish.com>
# Date 1417098588 -3600
# Thu Nov 27 15:29:48 2014 +0100
# Node ID 65e0b3c672473b918a2fc23325967c49fe384401
# Parent c7dbf7e25ee6778e19b7683e6fd5253ad7bf201c
[date] Fix utcdatetime

Dates with a tzinfo get messed up by utcdatetime.

This fix import the UTC timezone from pytz, and use a copy of it if pytz is not
present.

Closes #280794

diff --git a/date.py b/date.py
@@ -22,11 +22,11 @@
1 
2  import math
3  import re
4  import sys
5  from locale import getlocale, LC_TIME
6 -from datetime import date, time, datetime, timedelta
7 +from datetime import date, time, datetime, timedelta, tzinfo
8  from time import strptime as time_strptime
9  from calendar import monthrange, timegm
10 
11  from six.moves import range
12 
@@ -88,10 +88,73 @@
13      'paques2012': '2012-04-09',
14      'ascension2012': '2012-05-17',
15      'pentecote2012': '2012-05-28',
16      }
17 
18 +
19 +try:
20 +    from pytz import UTC
21 +except ImportError:
22 +    # UTC tzinfo copied from pytz
23 +    ZERO = timedelta(0)
24 +    HOUR = timedelta(hours=1)
25 +
26 +    class UTC(tzinfo):
27 +        """UTC
28 +
29 +        Optimized UTC implementation. It unpickles using the single module
30 +        global instance defined beneath this class declaration.
31 +        """
32 +        zone = "UTC"
33 +
34 +        _utcoffset = ZERO
35 +        _dst = ZERO
36 +        _tzname = zone
37 +
38 +        def fromutc(self, dt):
39 +            if dt.tzinfo is None:
40 +                return self.localize(dt)
41 +            return super(utc.__class__, self).fromutc(dt)
42 +
43 +        def utcoffset(self, dt):
44 +            return ZERO
45 +
46 +        def tzname(self, dt):
47 +            return "UTC"
48 +
49 +        def dst(self, dt):
50 +            return ZERO
51 +
52 +        def __reduce__(self):
53 +            return _UTC, ()
54 +
55 +        def localize(self, dt, is_dst=False):
56 +            '''Convert naive time to local time'''
57 +            if dt.tzinfo is not None:
58 +                raise ValueError('Not naive datetime (tzinfo is already set)')
59 +            return dt.replace(tzinfo=self)
60 +
61 +        def normalize(self, dt, is_dst=False):
62 +            '''Correct the timezone information on the given datetime'''
63 +            if dt.tzinfo is self:
64 +                return dt
65 +            if dt.tzinfo is None:
66 +                raise ValueError('Naive time - no tzinfo set')
67 +            return dt.astimezone(self)
68 +
69 +        def __repr__(self):
70 +            return "<UTC>"
71 +
72 +        def __str__(self):
73 +            return "UTC"
74 +
75 +    UTC = utc = UTC()  # UTC is a singleton
76 +
77 +    def _UTC():
78 +        return utc
79 +    _UTC.__safe_for_unpickling__ = True
80 +
81  # XXX this implementation cries for multimethod dispatching
82 
83  def get_step(dateobj, nbdays=1):
84      # assume date is either a python datetime or a mx.DateTime object
85      if isinstance(dateobj, date):
@@ -312,11 +375,11 @@
86              return unicode(fmt) % fields
87 
88  def utcdatetime(dt):
89      if dt.tzinfo is None:
90          return dt
91 -    return datetime(*dt.utctimetuple()[:7])
92 +    return dt.astimezone(utc).replace(tzinfo=None)
93 
94  def utctime(dt):
95      if dt.tzinfo is None:
96          return dt
97      return (dt + dt.utcoffset() + dt.dst()).replace(tzinfo=None)
diff --git a/test/unittest_date.py b/test/unittest_date.py
@@ -20,11 +20,11 @@
98  """
99  from logilab.common.testlib import TestCase, unittest_main, tag
100 
101  from logilab.common.date import date_range, endOfMonth
102  from logilab.common.date import add_days_worked, nb_open_days, \
103 -         get_national_holidays, ustrftime, ticks2datetime
104 +         get_national_holidays, ustrftime, ticks2datetime, utc, utcdatetime
105 
106  from datetime import date, datetime, timedelta
107 
108  try:
109      from mx.DateTime import Date as mxDate, DateTime as mxDateTime, \
@@ -143,10 +143,18 @@
110          r = list(date_range(self.datecls(2006, 5, 6), self.datecls(2006, 8, 27),
111                              incmonth=True))
112          expected = [self.datecls(2006, 5, 6), self.datecls(2006, 6, 1), self.datecls(2006, 7, 1), self.datecls(2006, 8, 1)]
113          self.assertListEqual(expected, r)
114 
115 +    def test_utcdatetime(self):
116 +        if self.datetimecls is mxDateTime:
117 +            raise self.skipTest('standard datetime only test')
118 +        d = self.datetimecls(2014, 11, 26, 12, 00, 00, tzinfo=utc)
119 +        self.assertEqual(
120 +            utcdatetime(d),
121 +            self.datetimecls(2014, 11, 26, 12, 00, 00))
122 +
123 
124  class MxDateTC(DateTC):
125      datecls = mxDate
126      datetimecls = mxDateTime
127      timedeltacls = RelativeDateTime