Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(1)

Delta Between Two Patch Sets: Lib/test/datetimetester.py

Issue 15873: "datetime" cannot parse ISO 8601 dates and times
Left Patch Set: Created 4 years ago
Right Patch Set: Created 3 years, 6 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « Lib/datetime.py ('k') | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 """Test date/time type. 1 """Test date/time type.
2 2
3 See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases 3 See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases
4 """ 4 """
5 from test.support import is_resource_enabled
6
7 import itertools
8 import bisect
5 9
6 import copy 10 import copy
7 import decimal 11 import decimal
8 import sys 12 import sys
13 import os
9 import pickle 14 import pickle
10 import random 15 import random
16 import struct
11 import unittest 17 import unittest
18 import sysconfig
19
20 from array import array
12 21
13 from operator import lt, le, gt, ge, eq, ne, truediv, floordiv, mod 22 from operator import lt, le, gt, ge, eq, ne, truediv, floordiv, mod
14 23
15 from test import support 24 from test import support
16 25
17 import datetime as datetime_module 26 import datetime as datetime_module
18 from datetime import MINYEAR, MAXYEAR 27 from datetime import MINYEAR, MAXYEAR
19 from datetime import timedelta 28 from datetime import timedelta
20 from datetime import tzinfo 29 from datetime import tzinfo
21 from datetime import time 30 from datetime import time
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
109 return self.__offset 118 return self.__offset
110 def tzname(self, dt): 119 def tzname(self, dt):
111 return self.__name 120 return self.__name
112 def dst(self, dt): 121 def dst(self, dt):
113 return self.__dstoffset 122 return self.__dstoffset
114 123
115 class PicklableFixedOffset(FixedOffset): 124 class PicklableFixedOffset(FixedOffset):
116 125
117 def __init__(self, offset=None, name=None, dstoffset=None): 126 def __init__(self, offset=None, name=None, dstoffset=None):
118 FixedOffset.__init__(self, offset, name, dstoffset) 127 FixedOffset.__init__(self, offset, name, dstoffset)
128
129 def __getstate__(self):
130 return self.__dict__
119 131
120 class _TZInfo(tzinfo): 132 class _TZInfo(tzinfo):
121 def utcoffset(self, datetime_module): 133 def utcoffset(self, datetime_module):
122 return random.random() 134 return random.random()
123 135
124 class TestTZInfo(unittest.TestCase): 136 class TestTZInfo(unittest.TestCase):
125 137
126 def test_refcnt_crash_bug_22044(self): 138 def test_refcnt_crash_bug_22044(self):
127 tz1 = _TZInfo() 139 tz1 = _TZInfo()
128 dt1 = datetime(2014, 7, 21, 11, 32, 3, 0, tz1) 140 dt1 = datetime(2014, 7, 21, 11, 32, 3, 0, tz1)
(...skipping 962 matching lines...) Expand 10 before | Expand all | Expand 10 after
1091 d = self.theclass.fromtimestamp(ts) 1103 d = self.theclass.fromtimestamp(ts)
1092 self.assertEqual(d.year, year) 1104 self.assertEqual(d.year, year)
1093 self.assertEqual(d.month, month) 1105 self.assertEqual(d.month, month)
1094 self.assertEqual(d.day, day) 1106 self.assertEqual(d.day, day)
1095 1107
1096 def test_fromisoformat(self): 1108 def test_fromisoformat(self):
1097 self.assertEqual(self.theclass.fromisoformat('2014-12-31'), 1109 self.assertEqual(self.theclass.fromisoformat('2014-12-31'),
1098 self.theclass(2014, 12, 31)) 1110 self.theclass(2014, 12, 31))
1099 self.assertEqual(self.theclass.fromisoformat('4095-07-31'), 1111 self.assertEqual(self.theclass.fromisoformat('4095-07-31'),
1100 self.theclass(4095, 7, 31)) 1112 self.theclass(4095, 7, 31))
1101 1113
1102 with self.assertRaises(ValueError): 1114 with self.assertRaises(ValueError):
1103 self.theclass.fromisoformat('2014-12-011') 1115 self.theclass.fromisoformat('2014-12-011')
1116 with self.assertRaises(ValueError):
1104 self.theclass.fromisoformat('20141211') 1117 self.theclass.fromisoformat('20141211')
1118 with self.assertRaises(ValueError):
1105 self.theclass.fromisoformat('043-12-01') 1119 self.theclass.fromisoformat('043-12-01')
1106 1120
1107 def test_insane_fromtimestamp(self): 1121 def test_insane_fromtimestamp(self):
1108 # It's possible that some platform maps time_t to double, 1122 # It's possible that some platform maps time_t to double,
1109 # and that this test will fail there. This test should 1123 # and that this test will fail there. This test should
1110 # exempt such platforms (provided they return reasonable 1124 # exempt such platforms (provided they return reasonable
1111 # results!). 1125 # results!).
1112 for insane in -1e200, 1e200: 1126 for insane in -1e200, 1e200:
1113 self.assertRaises(OverflowError, self.theclass.fromtimestamp, 1127 self.assertRaises(OverflowError, self.theclass.fromtimestamp,
1114 insane) 1128 insane)
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
1229 1243
1230 # A naive object replaces %z and %Z w/ empty strings. 1244 # A naive object replaces %z and %Z w/ empty strings.
1231 self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") 1245 self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''")
1232 1246
1233 #make sure that invalid format specifiers are handled correctly 1247 #make sure that invalid format specifiers are handled correctly
1234 #self.assertRaises(ValueError, t.strftime, "%e") 1248 #self.assertRaises(ValueError, t.strftime, "%e")
1235 #self.assertRaises(ValueError, t.strftime, "%") 1249 #self.assertRaises(ValueError, t.strftime, "%")
1236 #self.assertRaises(ValueError, t.strftime, "%#") 1250 #self.assertRaises(ValueError, t.strftime, "%#")
1237 1251
1238 #oh well, some systems just ignore those invalid ones. 1252 #oh well, some systems just ignore those invalid ones.
1239 #at least, excercise them to make sure that no crashes 1253 #at least, exercise them to make sure that no crashes
1240 #are generated 1254 #are generated
1241 for f in ["%e", "%", "%#"]: 1255 for f in ["%e", "%", "%#"]:
1242 try: 1256 try:
1243 t.strftime(f) 1257 t.strftime(f)
1244 except ValueError: 1258 except ValueError:
1245 pass 1259 pass
1246 1260
1247 #check that this standard extension works 1261 #check that this standard extension works
1248 t.strftime("%f") 1262 t.strftime("%f")
1249 1263
(...skipping 310 matching lines...) Expand 10 before | Expand all | Expand 10 after
1560 dt2 = eval(s) 1574 dt2 = eval(s)
1561 self.assertEqual(dt, dt2) 1575 self.assertEqual(dt, dt2)
1562 1576
1563 # Verify identity via reconstructing from pieces. 1577 # Verify identity via reconstructing from pieces.
1564 dt2 = self.theclass(dt.year, dt.month, dt.day, 1578 dt2 = self.theclass(dt.year, dt.month, dt.day,
1565 dt.hour, dt.minute, dt.second, 1579 dt.hour, dt.minute, dt.second,
1566 dt.microsecond) 1580 dt.microsecond)
1567 self.assertEqual(dt, dt2) 1581 self.assertEqual(dt, dt2)
1568 1582
1569 def test_isoformat(self): 1583 def test_isoformat(self):
1570 t = self.theclass(2, 3, 2, 4, 5, 1, 123) 1584 t = self.theclass(1, 2, 3, 4, 5, 1, 123)
1571 self.assertEqual(t.isoformat(), "0002-03-02T04:05:01.000123") 1585 self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123")
1572 self.assertEqual(t.isoformat('T'), "0002-03-02T04:05:01.000123") 1586 self.assertEqual(t.isoformat('T'), "0001-02-03T04:05:01.000123")
1573 self.assertEqual(t.isoformat(' '), "0002-03-02 04:05:01.000123") 1587 self.assertEqual(t.isoformat(' '), "0001-02-03 04:05:01.000123")
1574 self.assertEqual(t.isoformat('\x00'), "0002-03-02\x0004:05:01.000123") 1588 self.assertEqual(t.isoformat('\x00'), "0001-02-03\x0004:05:01.000123")
1589 self.assertEqual(t.isoformat(timespec='hours'), "0001-02-03T04")
1590 self.assertEqual(t.isoformat(timespec='minutes'), "0001-02-03T04:05")
1591 self.assertEqual(t.isoformat(timespec='seconds'), "0001-02-03T04:05:01")
1592 self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05 :01.000")
1593 self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05 :01.000123")
1594 self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01.0001 23")
1595 self.assertEqual(t.isoformat(sep=' ', timespec='minutes'), "0001-02-03 0 4:05")
1596 self.assertRaises(ValueError, t.isoformat, timespec='foo')
1575 # str is ISO format with the separator forced to a blank. 1597 # str is ISO format with the separator forced to a blank.
1576 self.assertEqual(str(t), "0002-03-02 04:05:01.000123") 1598 self.assertEqual(str(t), "0001-02-03 04:05:01.000123")
1599
1600 t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc)
1601 self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05 :01.999+00:00")
1602
1603 t = self.theclass(1, 2, 3, 4, 5, 1, 999500)
1604 self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05 :01.999")
1605
1606 t = self.theclass(1, 2, 3, 4, 5, 1)
1607 self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01")
1608 self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05 :01.000")
1609 self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05 :01.000000")
1577 1610
1578 t = self.theclass(2, 3, 2) 1611 t = self.theclass(2, 3, 2)
1579 self.assertEqual(t.isoformat(), "0002-03-02T00:00:00") 1612 self.assertEqual(t.isoformat(), "0002-03-02T00:00:00")
1580 self.assertEqual(t.isoformat('T'), "0002-03-02T00:00:00") 1613 self.assertEqual(t.isoformat('T'), "0002-03-02T00:00:00")
1581 self.assertEqual(t.isoformat(' '), "0002-03-02 00:00:00") 1614 self.assertEqual(t.isoformat(' '), "0002-03-02 00:00:00")
1582 # str is ISO format with the separator forced to a blank. 1615 # str is ISO format with the separator forced to a blank.
1583 self.assertEqual(str(t), "0002-03-02 00:00:00") 1616 self.assertEqual(str(t), "0002-03-02 00:00:00")
1617 # ISO format with timezone
1618 tz = FixedOffset(timedelta(seconds=16), 'XXX')
1619 t = self.theclass(2, 3, 2, tzinfo=tz)
1620 self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16")
1584 1621
1585 def test_format(self): 1622 def test_format(self):
1586 dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) 1623 dt = self.theclass(2007, 9, 10, 4, 5, 1, 123)
1587 self.assertEqual(dt.__format__(''), str(dt)) 1624 self.assertEqual(dt.__format__(''), str(dt))
1588 1625
1589 with self.assertRaisesRegex(TypeError, 'must be str, not int'): 1626 with self.assertRaisesRegex(TypeError, 'must be str, not int'):
1590 dt.__format__(123) 1627 dt.__format__(123)
1591 1628
1592 # check that a derived class's __str__() gets called 1629 # check that a derived class's __str__() gets called
1593 class A(self.theclass): 1630 class A(self.theclass):
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after
1693 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, -1) 1730 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, -1)
1694 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 60) 1731 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 60)
1695 # bad microseconds 1732 # bad microseconds
1696 self.theclass(2000, 1, 31, 23, 59, 59, 0) # no exception 1733 self.theclass(2000, 1, 31, 23, 59, 59, 0) # no exception
1697 self.theclass(2000, 1, 31, 23, 59, 59, 999999) # no exception 1734 self.theclass(2000, 1, 31, 23, 59, 59, 999999) # no exception
1698 self.assertRaises(ValueError, self.theclass, 1735 self.assertRaises(ValueError, self.theclass,
1699 2000, 1, 31, 23, 59, 59, -1) 1736 2000, 1, 31, 23, 59, 59, -1)
1700 self.assertRaises(ValueError, self.theclass, 1737 self.assertRaises(ValueError, self.theclass,
1701 2000, 1, 31, 23, 59, 59, 1738 2000, 1, 31, 23, 59, 59,
1702 1000000) 1739 1000000)
1740 # Positional fold:
1741 self.assertRaises(TypeError, self.theclass,
1742 2000, 1, 31, 23, 59, 59, 0, None, 1)
1703 1743
1704 def test_hash_equality(self): 1744 def test_hash_equality(self):
1705 d = self.theclass(2000, 12, 31, 23, 30, 17) 1745 d = self.theclass(2000, 12, 31, 23, 30, 17)
1706 e = self.theclass(2000, 12, 31, 23, 30, 17) 1746 e = self.theclass(2000, 12, 31, 23, 30, 17)
1707 self.assertEqual(d, e) 1747 self.assertEqual(d, e)
1708 self.assertEqual(hash(d), hash(e)) 1748 self.assertEqual(hash(d), hash(e))
1709 1749
1710 dic = {d: 1} 1750 dic = {d: 1}
1711 dic[e] = 2 1751 dic[e] = 2
1712 self.assertEqual(len(dic), 1) 1752 self.assertEqual(len(dic), 1)
(...skipping 175 matching lines...) Expand 10 before | Expand all | Expand 10 after
1888 1928
1889 # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in 1929 # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
1890 # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0). 1930 # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
1891 @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') 1931 @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
1892 def test_timestamp_naive(self): 1932 def test_timestamp_naive(self):
1893 t = self.theclass(1970, 1, 1) 1933 t = self.theclass(1970, 1, 1)
1894 self.assertEqual(t.timestamp(), 18000.0) 1934 self.assertEqual(t.timestamp(), 18000.0)
1895 t = self.theclass(1970, 1, 1, 1, 2, 3, 4) 1935 t = self.theclass(1970, 1, 1, 1, 2, 3, 4)
1896 self.assertEqual(t.timestamp(), 1936 self.assertEqual(t.timestamp(),
1897 18000.0 + 3600 + 2*60 + 3 + 4*1e-6) 1937 18000.0 + 3600 + 2*60 + 3 + 4*1e-6)
1898 # Missing hour may produce platform-dependent result 1938 # Missing hour
1899 t = self.theclass(2012, 3, 11, 2, 30) 1939 t0 = self.theclass(2012, 3, 11, 2, 30)
1900 self.assertIn(self.theclass.fromtimestamp(t.timestamp()), 1940 t1 = t0.replace(fold=1)
1901 [t - timedelta(hours=1), t + timedelta(hours=1)]) 1941 self.assertEqual(self.theclass.fromtimestamp(t1.timestamp()),
1942 t0 - timedelta(hours=1))
1943 self.assertEqual(self.theclass.fromtimestamp(t0.timestamp()),
1944 t1 + timedelta(hours=1))
1902 # Ambiguous hour defaults to DST 1945 # Ambiguous hour defaults to DST
1903 t = self.theclass(2012, 11, 4, 1, 30) 1946 t = self.theclass(2012, 11, 4, 1, 30)
1904 self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t) 1947 self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t)
1905 1948
1906 # Timestamp may raise an overflow error on some platforms 1949 # Timestamp may raise an overflow error on some platforms
1907 for t in [self.theclass(1,1,1), self.theclass(9999,12,12)]: 1950 # XXX: Do we care to support the first and last year?
1951 for t in [self.theclass(2,1,1), self.theclass(9998,12,12)]:
1908 try: 1952 try:
1909 s = t.timestamp() 1953 s = t.timestamp()
1910 except OverflowError: 1954 except OverflowError:
1911 pass 1955 pass
1912 else: 1956 else:
1913 self.assertEqual(self.theclass.fromtimestamp(s), t) 1957 self.assertEqual(self.theclass.fromtimestamp(s), t)
1914 1958
1915 def test_timestamp_aware(self): 1959 def test_timestamp_aware(self):
1916 t = self.theclass(1970, 1, 1, tzinfo=timezone.utc) 1960 t = self.theclass(1970, 1, 1, tzinfo=timezone.utc)
1917 self.assertEqual(t.timestamp(), 0.0) 1961 self.assertEqual(t.timestamp(), 0.0)
1918 t = self.theclass(1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone.utc) 1962 t = self.theclass(1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone.utc)
1919 self.assertEqual(t.timestamp(), 1963 self.assertEqual(t.timestamp(),
1920 3600 + 2*60 + 3 + 4*1e-6) 1964 3600 + 2*60 + 3 + 4*1e-6)
1921 t = self.theclass(1970, 1, 1, 1, 2, 3, 4, 1965 t = self.theclass(1970, 1, 1, 1, 2, 3, 4,
1922 tzinfo=timezone(timedelta(hours=-5), 'EST')) 1966 tzinfo=timezone(timedelta(hours=-5), 'EST'))
1923 self.assertEqual(t.timestamp(), 1967 self.assertEqual(t.timestamp(),
1924 18000 + 3600 + 2*60 + 3 + 4*1e-6) 1968 18000 + 3600 + 2*60 + 3 + 4*1e-6)
1925 1969
1970 @support.run_with_tz('MSK-03') # Something east of Greenwich
1926 def test_microsecond_rounding(self): 1971 def test_microsecond_rounding(self):
1927 for fts in [self.theclass.fromtimestamp, 1972 for fts in [self.theclass.fromtimestamp,
1928 self.theclass.utcfromtimestamp]: 1973 self.theclass.utcfromtimestamp]:
1929 zero = fts(0) 1974 zero = fts(0)
1930 self.assertEqual(zero.second, 0) 1975 self.assertEqual(zero.second, 0)
1931 self.assertEqual(zero.microsecond, 0) 1976 self.assertEqual(zero.microsecond, 0)
1932 one = fts(1e-6) 1977 one = fts(1e-6)
1933 try: 1978 try:
1934 minus_one = fts(-1e-6) 1979 minus_one = fts(-1e-6)
1935 except OSError: 1980 except OSError:
(...skipping 154 matching lines...) Expand 10 before | Expand all | Expand 10 after
2090 dt = combine(time=t, date=d) 2135 dt = combine(time=t, date=d)
2091 self.assertEqual(dt, expected) 2136 self.assertEqual(dt, expected)
2092 2137
2093 self.assertEqual(d, dt.date()) 2138 self.assertEqual(d, dt.date())
2094 self.assertEqual(t, dt.time()) 2139 self.assertEqual(t, dt.time())
2095 self.assertEqual(dt, combine(dt.date(), dt.time())) 2140 self.assertEqual(dt, combine(dt.date(), dt.time()))
2096 2141
2097 self.assertRaises(TypeError, combine) # need an arg 2142 self.assertRaises(TypeError, combine) # need an arg
2098 self.assertRaises(TypeError, combine, d) # need two args 2143 self.assertRaises(TypeError, combine, d) # need two args
2099 self.assertRaises(TypeError, combine, t, d) # args reversed 2144 self.assertRaises(TypeError, combine, t, d) # args reversed
2100 self.assertRaises(TypeError, combine, d, t, 1) # too many args 2145 self.assertRaises(TypeError, combine, d, t, 1) # wrong tzinfo type
2146 self.assertRaises(TypeError, combine, d, t, 1, 2) # too many args
2101 self.assertRaises(TypeError, combine, "date", "time") # wrong types 2147 self.assertRaises(TypeError, combine, "date", "time") # wrong types
2102 self.assertRaises(TypeError, combine, d, "time") # wrong type 2148 self.assertRaises(TypeError, combine, d, "time") # wrong type
2103 self.assertRaises(TypeError, combine, "date", t) # wrong type 2149 self.assertRaises(TypeError, combine, "date", t) # wrong type
2150
2151 # tzinfo= argument
2152 dt = combine(d, t, timezone.utc)
2153 self.assertIs(dt.tzinfo, timezone.utc)
2154 dt = combine(d, t, tzinfo=timezone.utc)
2155 self.assertIs(dt.tzinfo, timezone.utc)
2156 t = time()
2157 dt = combine(dt, t)
2158 self.assertEqual(dt.date(), d)
2159 self.assertEqual(dt.time(), t)
2104 2160
2105 def test_replace(self): 2161 def test_replace(self):
2106 cls = self.theclass 2162 cls = self.theclass
2107 args = [1, 2, 3, 4, 5, 6, 7] 2163 args = [1, 2, 3, 4, 5, 6, 7]
2108 base = cls(*args) 2164 base = cls(*args)
2109 self.assertEqual(base, base.replace()) 2165 self.assertEqual(base, base.replace())
2110 2166
2111 i = 0 2167 i = 0
2112 for name, newval in (("year", 2), 2168 for name, newval in (("year", 2),
2113 ("month", 3), 2169 ("month", 3),
2114 ("day", 4), 2170 ("day", 4),
2115 ("hour", 5), 2171 ("hour", 5),
2116 ("minute", 6), 2172 ("minute", 6),
2117 ("second", 7), 2173 ("second", 7),
2118 ("microsecond", 8)): 2174 ("microsecond", 8)):
2119 newargs = args[:] 2175 newargs = args[:]
2120 newargs[i] = newval 2176 newargs[i] = newval
2121 expected = cls(*newargs) 2177 expected = cls(*newargs)
2122 got = base.replace(**{name: newval}) 2178 got = base.replace(**{name: newval})
2123 self.assertEqual(expected, got) 2179 self.assertEqual(expected, got)
2124 i += 1 2180 i += 1
2125 2181
2126 # Out of bounds. 2182 # Out of bounds.
2127 base = cls(2000, 2, 29) 2183 base = cls(2000, 2, 29)
2128 self.assertRaises(ValueError, base.replace, year=2001) 2184 self.assertRaises(ValueError, base.replace, year=2001)
2129 2185
2130 def test_astimezone(self): 2186 def test_astimezone(self):
2187 return # The rest is no longer applicable
2131 # Pretty boring! The TZ test is more interesting here. astimezone() 2188 # Pretty boring! The TZ test is more interesting here. astimezone()
2132 # simply can't be applied to a naive object. 2189 # simply can't be applied to a naive object.
2133 dt = self.theclass.now() 2190 dt = self.theclass.now()
2134 f = FixedOffset(44, "") 2191 f = FixedOffset(44, "")
2135 self.assertRaises(ValueError, dt.astimezone) # naive 2192 self.assertRaises(ValueError, dt.astimezone) # naive
2136 self.assertRaises(TypeError, dt.astimezone, f, f) # too many args 2193 self.assertRaises(TypeError, dt.astimezone, f, f) # too many args
2137 self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type 2194 self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
2138 self.assertRaises(ValueError, dt.astimezone, f) # naive 2195 self.assertRaises(ValueError, dt.astimezone, f) # naive
2139 self.assertRaises(ValueError, dt.astimezone, tz=f) # naive 2196 self.assertRaises(ValueError, dt.astimezone, tz=f) # naive
2140 2197
(...skipping 197 matching lines...) Expand 10 before | Expand all | Expand 10 after
2338 self.assertEqual(t.isoformat(), str(t)) 2395 self.assertEqual(t.isoformat(), str(t))
2339 2396
2340 t = self.theclass(microsecond=10000) 2397 t = self.theclass(microsecond=10000)
2341 self.assertEqual(t.isoformat(), "00:00:00.010000") 2398 self.assertEqual(t.isoformat(), "00:00:00.010000")
2342 self.assertEqual(t.isoformat(), str(t)) 2399 self.assertEqual(t.isoformat(), str(t))
2343 2400
2344 t = self.theclass(microsecond=100000) 2401 t = self.theclass(microsecond=100000)
2345 self.assertEqual(t.isoformat(), "00:00:00.100000") 2402 self.assertEqual(t.isoformat(), "00:00:00.100000")
2346 self.assertEqual(t.isoformat(), str(t)) 2403 self.assertEqual(t.isoformat(), str(t))
2347 2404
2405 t = self.theclass(hour=12, minute=34, second=56, microsecond=123456)
2406 self.assertEqual(t.isoformat(timespec='hours'), "12")
2407 self.assertEqual(t.isoformat(timespec='minutes'), "12:34")
2408 self.assertEqual(t.isoformat(timespec='seconds'), "12:34:56")
2409 self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.123")
2410 self.assertEqual(t.isoformat(timespec='microseconds'), "12:34:56.123456" )
2411 self.assertEqual(t.isoformat(timespec='auto'), "12:34:56.123456")
2412 self.assertRaises(ValueError, t.isoformat, timespec='monkey')
2413
2414 t = self.theclass(hour=12, minute=34, second=56, microsecond=999500)
2415 self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.999")
2416
2417 t = self.theclass(hour=12, minute=34, second=56, microsecond=0)
2418 self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.000")
2419 self.assertEqual(t.isoformat(timespec='microseconds'), "12:34:56.000000" )
2420 self.assertEqual(t.isoformat(timespec='auto'), "12:34:56")
2421
2348 def test_fromisoformat(self): 2422 def test_fromisoformat(self):
2349 # basic 2423 # basic
2350 self.assertEqual(self.theclass.fromisoformat('04:05:01.000123'), 2424 self.assertEqual(self.theclass.fromisoformat('04:05:01.000123'),
2351 self.theclass(4, 5, 1, 123)) 2425 self.theclass(4, 5, 1, 123))
2352 self.assertEqual(self.theclass.fromisoformat('00:00:00'), 2426 self.assertEqual(self.theclass.fromisoformat('00:00:00'),
2353 self.theclass(0, 0, 0)) 2427 self.theclass(0, 0, 0))
2354 # usec, rounding high 2428 # usec, rounding high
2355 self.assertEqual(self.theclass.fromisoformat('10:20:30.40000059'), 2429 self.assertEqual(self.theclass.fromisoformat('10:20:30.40000059'),
2356 self.theclass(10, 20, 30, 400001)) 2430 self.theclass(10, 20, 30, 400001))
2357 # usec, rounding low + long digits we don't care about 2431 # usec, rounding low + long digits we don't care about
2358 self.assertEqual(self.theclass.fromisoformat('10:20:30.400003434'), 2432 self.assertEqual(self.theclass.fromisoformat('10:20:30.400003434'),
2359 self.theclass(10, 20, 30, 400003)) 2433 self.theclass(10, 20, 30, 400003))
2360 with self.assertRaises(ValueError): 2434 with self.assertRaises(ValueError):
2361 self.theclass.fromisoformat('12:00AM') 2435 self.theclass.fromisoformat('12:00AM')
2436 with self.assertRaises(ValueError):
2362 self.theclass.fromisoformat('120000') 2437 self.theclass.fromisoformat('120000')
2438 with self.assertRaises(ValueError):
2363 self.theclass.fromisoformat('1:00') 2439 self.theclass.fromisoformat('1:00')
2440 with self.assertRaises(ValueError):
2441 self.theclass.fromisoformat('17:54:43.')
2364 2442
2365 def tz(h, m): 2443 def tz(h, m):
2366 return timezone(timedelta(hours=h, minutes=m)) 2444 return timezone(timedelta(hours=h, minutes=m))
2367 2445
2368 self.assertEqual(self.theclass.fromisoformat('00:00:00Z'), 2446 self.assertEqual(self.theclass.fromisoformat('00:00:00Z'),
2369 self.theclass(0, 0, 0, tzinfo=timezone.utc)) 2447 self.theclass(0, 0, 0, tzinfo=timezone.utc))
2370 # lowercase UTC timezone. Uncommon but tolerated (rfc 3339) 2448 # lowercase UTC timezone. Uncommon but tolerated (rfc 3339)
2371 self.assertEqual(self.theclass.fromisoformat('00:00:00z'), 2449 self.assertEqual(self.theclass.fromisoformat('00:00:00z'),
2372 self.theclass(0, 0, 0, tzinfo=timezone.utc)) 2450 self.theclass(0, 0, 0, tzinfo=timezone.utc))
2373 self.assertEqual(self.theclass.fromisoformat('00:00:00-00:00'), 2451 self.assertEqual(self.theclass.fromisoformat('00:00:00-00:00'),
(...skipping 148 matching lines...) Expand 10 before | Expand all | Expand 10 after
2522 # see TestDate.test_backdoor_resistance(). 2600 # see TestDate.test_backdoor_resistance().
2523 base = '2:59.0' 2601 base = '2:59.0'
2524 for hour_byte in ' ', '9', chr(24), '\xff': 2602 for hour_byte in ' ', '9', chr(24), '\xff':
2525 self.assertRaises(TypeError, self.theclass, 2603 self.assertRaises(TypeError, self.theclass,
2526 hour_byte + base[1:]) 2604 hour_byte + base[1:])
2527 # Good bytes, but bad tzinfo: 2605 # Good bytes, but bad tzinfo:
2528 with self.assertRaisesRegex(TypeError, '^bad tzinfo state arg$'): 2606 with self.assertRaisesRegex(TypeError, '^bad tzinfo state arg$'):
2529 self.theclass(bytes([1] * len(base)), 'EST') 2607 self.theclass(bytes([1] * len(base)), 'EST')
2530 2608
2531 # A mixin for classes with a tzinfo= argument. Subclasses must define 2609 # A mixin for classes with a tzinfo= argument. Subclasses must define
2532 # theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) 2610 # theclass as a class attribute, and theclass(1, 1, 1, tzinfo=whatever)
2533 # must be legit (which is true for time and datetime). 2611 # must be legit (which is true for time and datetime).
2534 class TZInfoBase: 2612 class TZInfoBase:
2535 2613
2536 def test_argument_passing(self): 2614 def test_argument_passing(self):
2537 cls = self.theclass 2615 cls = self.theclass
2538 # A datetime passes itself on, a time passes None. 2616 # A datetime passes itself on, a time passes None.
2539 class introspective(tzinfo): 2617 class introspective(tzinfo):
2540 def tzname(self, dt): return dt and "real" or "none" 2618 def tzname(self, dt): return dt and "real" or "none"
2541 def utcoffset(self, dt): 2619 def utcoffset(self, dt):
2542 return timedelta(minutes = dt and 42 or -42) 2620 return timedelta(minutes = dt and 42 or -42)
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
2628 self.assertRaises(TypeError, t.tzname) 2706 self.assertRaises(TypeError, t.tzname)
2629 2707
2630 # Offset out of range. 2708 # Offset out of range.
2631 class C6(tzinfo): 2709 class C6(tzinfo):
2632 def utcoffset(self, dt): return timedelta(hours=-24) 2710 def utcoffset(self, dt): return timedelta(hours=-24)
2633 def dst(self, dt): return timedelta(hours=24) 2711 def dst(self, dt): return timedelta(hours=24)
2634 t = cls(1, 1, 1, tzinfo=C6()) 2712 t = cls(1, 1, 1, tzinfo=C6())
2635 self.assertRaises(ValueError, t.utcoffset) 2713 self.assertRaises(ValueError, t.utcoffset)
2636 self.assertRaises(ValueError, t.dst) 2714 self.assertRaises(ValueError, t.dst)
2637 2715
2638 # Not a whole number of minutes. 2716 # Not a whole number of seconds.
2639 class C7(tzinfo): 2717 class C7(tzinfo):
2640 def utcoffset(self, dt): return timedelta(seconds=61) 2718 def utcoffset(self, dt): return timedelta(microseconds=61)
2641 def dst(self, dt): return timedelta(microseconds=-81) 2719 def dst(self, dt): return timedelta(microseconds=-81)
2642 t = cls(1, 1, 1, tzinfo=C7()) 2720 t = cls(1, 1, 1, tzinfo=C7())
2643 self.assertRaises(ValueError, t.utcoffset) 2721 self.assertRaises(ValueError, t.utcoffset)
2644 self.assertRaises(ValueError, t.dst) 2722 self.assertRaises(ValueError, t.dst)
2645 2723
2646 def test_aware_compare(self): 2724 def test_aware_compare(self):
2647 cls = self.theclass 2725 cls = self.theclass
2648 2726
2649 # Ensure that utcoffset() gets ignored if the comparands have 2727 # Ensure that utcoffset() gets ignored if the comparands have
2650 # the same tzinfo member. 2728 # the same tzinfo member.
(...skipping 821 matching lines...) Expand 10 before | Expand all | Expand 10 after
3472 def test_astimezone_default_eastern(self): 3550 def test_astimezone_default_eastern(self):
3473 dt = self.theclass(2012, 11, 4, 6, 30, tzinfo=timezone.utc) 3551 dt = self.theclass(2012, 11, 4, 6, 30, tzinfo=timezone.utc)
3474 local = dt.astimezone() 3552 local = dt.astimezone()
3475 self.assertEqual(dt, local) 3553 self.assertEqual(dt, local)
3476 self.assertEqual(local.strftime("%z %Z"), "-0500 EST") 3554 self.assertEqual(local.strftime("%z %Z"), "-0500 EST")
3477 dt = self.theclass(2012, 11, 4, 5, 30, tzinfo=timezone.utc) 3555 dt = self.theclass(2012, 11, 4, 5, 30, tzinfo=timezone.utc)
3478 local = dt.astimezone() 3556 local = dt.astimezone()
3479 self.assertEqual(dt, local) 3557 self.assertEqual(dt, local)
3480 self.assertEqual(local.strftime("%z %Z"), "-0400 EDT") 3558 self.assertEqual(local.strftime("%z %Z"), "-0400 EDT")
3481 3559
3560 @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
3561 def test_astimezone_default_near_fold(self):
3562 # Issue #26616.
3563 u = datetime(2015, 11, 1, 5, tzinfo=timezone.utc)
3564 t = u.astimezone()
3565 s = t.astimezone()
3566 self.assertEqual(t.tzinfo, s.tzinfo)
3567
3482 def test_aware_subtract(self): 3568 def test_aware_subtract(self):
3483 cls = self.theclass 3569 cls = self.theclass
3484 3570
3485 # Ensure that utcoffset() is ignored when the operands have the 3571 # Ensure that utcoffset() is ignored when the operands have the
3486 # same tzinfo member. 3572 # same tzinfo member.
3487 class OperandDependentOffset(tzinfo): 3573 class OperandDependentOffset(tzinfo):
3488 def utcoffset(self, t): 3574 def utcoffset(self, t):
3489 if t.minute < 10: 3575 if t.minute < 10:
3490 # d0 and d1 equal after adjustment 3576 # d0 and d1 equal after adjustment
3491 return timedelta(minutes=t.minute) 3577 return timedelta(minutes=t.minute)
(...skipping 503 matching lines...) Expand 10 before | Expand all | Expand 10 after
3995 datetime(10, 10, 10.) 4081 datetime(10, 10, 10.)
3996 with self.assertRaises(TypeError): 4082 with self.assertRaises(TypeError):
3997 datetime(10, 10, 10, 10.) 4083 datetime(10, 10, 10, 10.)
3998 with self.assertRaises(TypeError): 4084 with self.assertRaises(TypeError):
3999 datetime(10, 10, 10, 10, 10.) 4085 datetime(10, 10, 10, 10, 10.)
4000 with self.assertRaises(TypeError): 4086 with self.assertRaises(TypeError):
4001 datetime(10, 10, 10, 10, 10, 10.) 4087 datetime(10, 10, 10, 10, 10, 10.)
4002 with self.assertRaises(TypeError): 4088 with self.assertRaises(TypeError):
4003 datetime(10, 10, 10, 10, 10, 10, 10.) 4089 datetime(10, 10, 10, 10, 10, 10, 10.)
4004 4090
4091 #############################################################################
4092 # Local Time Disambiguation
4093
4094 # An experimental reimplementation of fromutc that respects the "fold" flag.
4095
4096 class tzinfo2(tzinfo):
4097
4098 def fromutc(self, dt):
4099 "datetime in UTC -> datetime in local time."
4100
4101 if not isinstance(dt, datetime):
4102 raise TypeError("fromutc() requires a datetime argument")
4103 if dt.tzinfo is not self:
4104 raise ValueError("dt.tzinfo is not self")
4105 # Returned value satisfies
4106 # dt + ldt.utcoffset() = ldt
4107 off0 = dt.replace(fold=0).utcoffset()
4108 off1 = dt.replace(fold=1).utcoffset()
4109 if off0 is None or off1 is None or dt.dst() is None:
4110 raise ValueError
4111 if off0 == off1:
4112 ldt = dt + off0
4113 off1 = ldt.utcoffset()
4114 if off0 == off1:
4115 return ldt
4116 # Now, we discovered both possible offsets, so
4117 # we can just try four possible solutions:
4118 for off in [off0, off1]:
4119 ldt = dt + off
4120 if ldt.utcoffset() == off:
4121 return ldt
4122 ldt = ldt.replace(fold=1)
4123 if ldt.utcoffset() == off:
4124 return ldt
4125
4126 raise ValueError("No suitable local time found")
4127
4128 # Reimplementing simplified US timezones to respect the "fold" flag:
4129
4130 class USTimeZone2(tzinfo2):
4131
4132 def __init__(self, hours, reprname, stdname, dstname):
4133 self.stdoffset = timedelta(hours=hours)
4134 self.reprname = reprname
4135 self.stdname = stdname
4136 self.dstname = dstname
4137
4138 def __repr__(self):
4139 return self.reprname
4140
4141 def tzname(self, dt):
4142 if self.dst(dt):
4143 return self.dstname
4144 else:
4145 return self.stdname
4146
4147 def utcoffset(self, dt):
4148 return self.stdoffset + self.dst(dt)
4149
4150 def dst(self, dt):
4151 if dt is None or dt.tzinfo is None:
4152 # An exception instead may be sensible here, in one or more of
4153 # the cases.
4154 return ZERO
4155 assert dt.tzinfo is self
4156
4157 # Find first Sunday in April.
4158 start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
4159 assert start.weekday() == 6 and start.month == 4 and start.day <= 7
4160
4161 # Find last Sunday in October.
4162 end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
4163 assert end.weekday() == 6 and end.month == 10 and end.day >= 25
4164
4165 # Can't compare naive to aware objects, so strip the timezone from
4166 # dt first.
4167 dt = dt.replace(tzinfo=None)
4168 if start + HOUR <= dt < end:
4169 # DST is in effect.
4170 return HOUR
4171 elif end <= dt < end + HOUR:
4172 # Fold (an ambiguous hour): use dt.fold to disambiguate.
4173 return ZERO if dt.fold else HOUR
4174 elif start <= dt < start + HOUR:
4175 # Gap (a non-existent hour): reverse the fold rule.
4176 return HOUR if dt.fold else ZERO
4177 else:
4178 # DST is off.
4179 return ZERO
4180
4181 Eastern2 = USTimeZone2(-5, "Eastern2", "EST", "EDT")
4182 Central2 = USTimeZone2(-6, "Central2", "CST", "CDT")
4183 Mountain2 = USTimeZone2(-7, "Mountain2", "MST", "MDT")
4184 Pacific2 = USTimeZone2(-8, "Pacific2", "PST", "PDT")
4185
4186 # Europe_Vilnius_1941 tzinfo implementation reproduces the following
4187 # 1941 transition from Olson's tzdist:
4188 #
4189 # Zone NAME GMTOFF RULES FORMAT [UNTIL]
4190 # ZoneEurope/Vilnius 1:00 - CET 1940 Aug 3
4191 # 3:00 - MSK 1941 Jun 24
4192 # 1:00 C-Eur CE%sT 1944 Aug
4193 #
4194 # $ zdump -v Europe/Vilnius | grep 1941
4195 # Europe/Vilnius Mon Jun 23 20:59:59 1941 UTC = Mon Jun 23 23:59:59 1941 MSK is dst=0 gmtoff=10800
4196 # Europe/Vilnius Mon Jun 23 21:00:00 1941 UTC = Mon Jun 23 23:00:00 1941 CEST i sdst=1 gmtoff=7200
4197
4198 class Europe_Vilnius_1941(tzinfo):
4199 def _utc_fold(self):
4200 return [datetime(1941, 6, 23, 21, tzinfo=self), # Mon Jun 23 21:00:00 1 941 UTC
4201 datetime(1941, 6, 23, 22, tzinfo=self)] # Mon Jun 23 22:00:00 1 941 UTC
4202
4203 def _loc_fold(self):
4204 return [datetime(1941, 6, 23, 23, tzinfo=self), # Mon Jun 23 23:00:00 1 941 MSK / CEST
4205 datetime(1941, 6, 24, 0, tzinfo=self)] # Mon Jun 24 00:00:00 1 941 CEST
4206
4207 def utcoffset(self, dt):
4208 fold_start, fold_stop = self._loc_fold()
4209 if dt < fold_start:
4210 return 3 * HOUR
4211 if dt < fold_stop:
4212 return (2 if dt.fold else 3) * HOUR
4213 # if dt >= fold_stop
4214 return 2 * HOUR
4215
4216 def dst(self, dt):
4217 fold_start, fold_stop = self._loc_fold()
4218 if dt < fold_start:
4219 return 0 * HOUR
4220 if dt < fold_stop:
4221 return (1 if dt.fold else 0) * HOUR
4222 # if dt >= fold_stop
4223 return 1 * HOUR
4224
4225 def tzname(self, dt):
4226 fold_start, fold_stop = self._loc_fold()
4227 if dt < fold_start:
4228 return 'MSK'
4229 if dt < fold_stop:
4230 return ('MSK', 'CEST')[dt.fold]
4231 # if dt >= fold_stop
4232 return 'CEST'
4233
4234 def fromutc(self, dt):
4235 assert dt.fold == 0
4236 assert dt.tzinfo is self
4237 if dt.year != 1941:
4238 raise NotImplementedError
4239 fold_start, fold_stop = self._utc_fold()
4240 if dt < fold_start:
4241 return dt + 3 * HOUR
4242 if dt < fold_stop:
4243 return (dt + 2 * HOUR).replace(fold=1)
4244 # if dt >= fold_stop
4245 return dt + 2 * HOUR
4246
4247
4248 class TestLocalTimeDisambiguation(unittest.TestCase):
4249
4250 def test_vilnius_1941_fromutc(self):
4251 Vilnius = Europe_Vilnius_1941()
4252
4253 gdt = datetime(1941, 6, 23, 20, 59, 59, tzinfo=timezone.utc)
4254 ldt = gdt.astimezone(Vilnius)
4255 self.assertEqual(ldt.strftime("%c %Z%z"),
4256 'Mon Jun 23 23:59:59 1941 MSK+0300')
4257 self.assertEqual(ldt.fold, 0)
4258 self.assertFalse(ldt.dst())
4259
4260 gdt = datetime(1941, 6, 23, 21, tzinfo=timezone.utc)
4261 ldt = gdt.astimezone(Vilnius)
4262 self.assertEqual(ldt.strftime("%c %Z%z"),
4263 'Mon Jun 23 23:00:00 1941 CEST+0200')
4264 self.assertEqual(ldt.fold, 1)
4265 self.assertTrue(ldt.dst())
4266
4267 gdt = datetime(1941, 6, 23, 22, tzinfo=timezone.utc)
4268 ldt = gdt.astimezone(Vilnius)
4269 self.assertEqual(ldt.strftime("%c %Z%z"),
4270 'Tue Jun 24 00:00:00 1941 CEST+0200')
4271 self.assertEqual(ldt.fold, 0)
4272 self.assertTrue(ldt.dst())
4273
4274 def test_vilnius_1941_toutc(self):
4275 Vilnius = Europe_Vilnius_1941()
4276
4277 ldt = datetime(1941, 6, 23, 22, 59, 59, tzinfo=Vilnius)
4278 gdt = ldt.astimezone(timezone.utc)
4279 self.assertEqual(gdt.strftime("%c %Z"),
4280 'Mon Jun 23 19:59:59 1941 UTC')
4281
4282 ldt = datetime(1941, 6, 23, 23, 59, 59, tzinfo=Vilnius)
4283 gdt = ldt.astimezone(timezone.utc)
4284 self.assertEqual(gdt.strftime("%c %Z"),
4285 'Mon Jun 23 20:59:59 1941 UTC')
4286
4287 ldt = datetime(1941, 6, 23, 23, 59, 59, tzinfo=Vilnius, fold=1)
4288 gdt = ldt.astimezone(timezone.utc)
4289 self.assertEqual(gdt.strftime("%c %Z"),
4290 'Mon Jun 23 21:59:59 1941 UTC')
4291
4292 ldt = datetime(1941, 6, 24, 0, tzinfo=Vilnius)
4293 gdt = ldt.astimezone(timezone.utc)
4294 self.assertEqual(gdt.strftime("%c %Z"),
4295 'Mon Jun 23 22:00:00 1941 UTC')
4296
4297
4298 def test_constructors(self):
4299 t = time(0, fold=1)
4300 dt = datetime(1, 1, 1, fold=1)
4301 self.assertEqual(t.fold, 1)
4302 self.assertEqual(dt.fold, 1)
4303 with self.assertRaises(TypeError):
4304 time(0, 0, 0, 0, None, 0)
4305
4306 def test_member(self):
4307 dt = datetime(1, 1, 1, fold=1)
4308 t = dt.time()
4309 self.assertEqual(t.fold, 1)
4310 t = dt.timetz()
4311 self.assertEqual(t.fold, 1)
4312
4313 def test_replace(self):
4314 t = time(0)
4315 dt = datetime(1, 1, 1)
4316 self.assertEqual(t.replace(fold=1).fold, 1)
4317 self.assertEqual(dt.replace(fold=1).fold, 1)
4318 self.assertEqual(t.replace(fold=0).fold, 0)
4319 self.assertEqual(dt.replace(fold=0).fold, 0)
4320 # Check that replacement of other fields does not change "fold".
4321 t = t.replace(fold=1, tzinfo=Eastern)
4322 dt = dt.replace(fold=1, tzinfo=Eastern)
4323 self.assertEqual(t.replace(tzinfo=None).fold, 1)
4324 self.assertEqual(dt.replace(tzinfo=None).fold, 1)
4325 # Check that fold is a keyword-only argument
4326 with self.assertRaises(TypeError):
4327 t.replace(1, 1, 1, None, 1)
4328 with self.assertRaises(TypeError):
4329 dt.replace(1, 1, 1, 1, 1, 1, 1, None, 1)
4330
4331 def test_comparison(self):
4332 t = time(0)
4333 dt = datetime(1, 1, 1)
4334 self.assertEqual(t, t.replace(fold=1))
4335 self.assertEqual(dt, dt.replace(fold=1))
4336
4337 def test_hash(self):
4338 t = time(0)
4339 dt = datetime(1, 1, 1)
4340 self.assertEqual(hash(t), hash(t.replace(fold=1)))
4341 self.assertEqual(hash(dt), hash(dt.replace(fold=1)))
4342
4343 @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
4344 def test_fromtimestamp(self):
4345 s = 1414906200
4346 dt0 = datetime.fromtimestamp(s)
4347 dt1 = datetime.fromtimestamp(s + 3600)
4348 self.assertEqual(dt0.fold, 0)
4349 self.assertEqual(dt1.fold, 1)
4350
4351 @support.run_with_tz('Australia/Lord_Howe')
4352 def test_fromtimestamp_lord_howe(self):
4353 tm = _time.localtime(1.4e9)
4354 if _time.strftime('%Z%z', tm) != 'LHST+1030':
4355 self.skipTest('Australia/Lord_Howe timezone is not supported on this platform')
4356 # $ TZ=Australia/Lord_Howe date -r 1428158700
4357 # Sun Apr 5 01:45:00 LHDT 2015
4358 # $ TZ=Australia/Lord_Howe date -r 1428160500
4359 # Sun Apr 5 01:45:00 LHST 2015
4360 s = 1428158700
4361 t0 = datetime.fromtimestamp(s)
4362 t1 = datetime.fromtimestamp(s + 1800)
4363 self.assertEqual(t0, t1)
4364 self.assertEqual(t0.fold, 0)
4365 self.assertEqual(t1.fold, 1)
4366
4367
4368 @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
4369 def test_timestamp(self):
4370 dt0 = datetime(2014, 11, 2, 1, 30)
4371 dt1 = dt0.replace(fold=1)
4372 self.assertEqual(dt0.timestamp() + 3600,
4373 dt1.timestamp())
4374
4375 @support.run_with_tz('Australia/Lord_Howe')
4376 def test_timestamp_lord_howe(self):
4377 tm = _time.localtime(1.4e9)
4378 if _time.strftime('%Z%z', tm) != 'LHST+1030':
4379 self.skipTest('Australia/Lord_Howe timezone is not supported on this platform')
4380 t = datetime(2015, 4, 5, 1, 45)
4381 s0 = t.replace(fold=0).timestamp()
4382 s1 = t.replace(fold=1).timestamp()
4383 self.assertEqual(s0 + 1800, s1)
4384
4385
4386 @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
4387 def test_astimezone(self):
4388 dt0 = datetime(2014, 11, 2, 1, 30)
4389 dt1 = dt0.replace(fold=1)
4390 # Convert both naive instances to aware.
4391 adt0 = dt0.astimezone()
4392 adt1 = dt1.astimezone()
4393 # Check that the first instance in DST zone and the second in STD
4394 self.assertEqual(adt0.tzname(), 'EDT')
4395 self.assertEqual(adt1.tzname(), 'EST')
4396 self.assertEqual(adt0 + HOUR, adt1)
4397 # Aware instances with fixed offset tzinfo's always have fold=0
4398 self.assertEqual(adt0.fold, 0)
4399 self.assertEqual(adt1.fold, 0)
4400
4401
4402 def test_pickle_fold(self):
4403 t = time(fold=1)
4404 dt = datetime(1, 1, 1, fold=1)
4405 for pickler, unpickler, proto in pickle_choices:
4406 for x in [t, dt]:
4407 s = pickler.dumps(x, proto)
4408 y = unpickler.loads(s)
4409 self.assertEqual(x, y)
4410 self.assertEqual((0 if proto < 4 else x.fold), y.fold)
4411
4412 def test_repr(self):
4413 t = time(fold=1)
4414 dt = datetime(1, 1, 1, fold=1)
4415 self.assertEqual(repr(t), 'datetime.time(0, 0, fold=1)')
4416 self.assertEqual(repr(dt),
4417 'datetime.datetime(1, 1, 1, 0, 0, fold=1)')
4418
4419 def test_dst(self):
4420 # Let's first establish that things work in regular times.
4421 dt_summer = datetime(2002, 10, 27, 1, tzinfo=Eastern2) - timedelta.resol ution
4422 dt_winter = datetime(2002, 10, 27, 2, tzinfo=Eastern2)
4423 self.assertEqual(dt_summer.dst(), HOUR)
4424 self.assertEqual(dt_winter.dst(), ZERO)
4425 # The disambiguation flag is ignored
4426 self.assertEqual(dt_summer.replace(fold=1).dst(), HOUR)
4427 self.assertEqual(dt_winter.replace(fold=1).dst(), ZERO)
4428
4429 # Pick local time in the fold.
4430 for minute in [0, 30, 59]:
4431 dt = datetime(2002, 10, 27, 1, minute, tzinfo=Eastern2)
4432 # With fold=0 (the default) it is in DST.
4433 self.assertEqual(dt.dst(), HOUR)
4434 # With fold=1 it is in STD.
4435 self.assertEqual(dt.replace(fold=1).dst(), ZERO)
4436
4437 # Pick local time in the gap.
4438 for minute in [0, 30, 59]:
4439 dt = datetime(2002, 4, 7, 2, minute, tzinfo=Eastern2)
4440 # With fold=0 (the default) it is in STD.
4441 self.assertEqual(dt.dst(), ZERO)
4442 # With fold=1 it is in DST.
4443 self.assertEqual(dt.replace(fold=1).dst(), HOUR)
4444
4445
4446 def test_utcoffset(self):
4447 # Let's first establish that things work in regular times.
4448 dt_summer = datetime(2002, 10, 27, 1, tzinfo=Eastern2) - timedelta.resol ution
4449 dt_winter = datetime(2002, 10, 27, 2, tzinfo=Eastern2)
4450 self.assertEqual(dt_summer.utcoffset(), -4 * HOUR)
4451 self.assertEqual(dt_winter.utcoffset(), -5 * HOUR)
4452 # The disambiguation flag is ignored
4453 self.assertEqual(dt_summer.replace(fold=1).utcoffset(), -4 * HOUR)
4454 self.assertEqual(dt_winter.replace(fold=1).utcoffset(), -5 * HOUR)
4455
4456 def test_fromutc(self):
4457 # Let's first establish that things work in regular times.
4458 u_summer = datetime(2002, 10, 27, 6, tzinfo=Eastern2) - timedelta.resolu tion
4459 u_winter = datetime(2002, 10, 27, 7, tzinfo=Eastern2)
4460 t_summer = Eastern2.fromutc(u_summer)
4461 t_winter = Eastern2.fromutc(u_winter)
4462 self.assertEqual(t_summer, u_summer - 4 * HOUR)
4463 self.assertEqual(t_winter, u_winter - 5 * HOUR)
4464 self.assertEqual(t_summer.fold, 0)
4465 self.assertEqual(t_winter.fold, 0)
4466
4467 # What happens in the fall-back fold?
4468 u = datetime(2002, 10, 27, 5, 30, tzinfo=Eastern2)
4469 t0 = Eastern2.fromutc(u)
4470 u += HOUR
4471 t1 = Eastern2.fromutc(u)
4472 self.assertEqual(t0, t1)
4473 self.assertEqual(t0.fold, 0)
4474 self.assertEqual(t1.fold, 1)
4475 # The tricky part is when u is in the local fold:
4476 u = datetime(2002, 10, 27, 1, 30, tzinfo=Eastern2)
4477 t = Eastern2.fromutc(u)
4478 self.assertEqual((t.day, t.hour), (26, 21))
4479 # .. or gets into the local fold after a standard time adjustment
4480 u = datetime(2002, 10, 27, 6, 30, tzinfo=Eastern2)
4481 t = Eastern2.fromutc(u)
4482 self.assertEqual((t.day, t.hour), (27, 1))
4483
4484 # What happens in the spring-forward gap?
4485 u = datetime(2002, 4, 7, 2, 0, tzinfo=Eastern2)
4486 t = Eastern2.fromutc(u)
4487 self.assertEqual((t.day, t.hour), (6, 21))
4488
4489 def test_mixed_compare_regular(self):
4490 t = datetime(2000, 1, 1, tzinfo=Eastern2)
4491 self.assertEqual(t, t.astimezone(timezone.utc))
4492 t = datetime(2000, 6, 1, tzinfo=Eastern2)
4493 self.assertEqual(t, t.astimezone(timezone.utc))
4494
4495 def test_mixed_compare_fold(self):
4496 t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2)
4497 t_fold_utc = t_fold.astimezone(timezone.utc)
4498 self.assertNotEqual(t_fold, t_fold_utc)
4499
4500 def test_mixed_compare_gap(self):
4501 t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2)
4502 t_gap_utc = t_gap.astimezone(timezone.utc)
4503 self.assertNotEqual(t_gap, t_gap_utc)
4504
4505 def test_hash_aware(self):
4506 t = datetime(2000, 1, 1, tzinfo=Eastern2)
4507 self.assertEqual(hash(t), hash(t.replace(fold=1)))
4508 t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2)
4509 t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2)
4510 self.assertEqual(hash(t_fold), hash(t_fold.replace(fold=1)))
4511 self.assertEqual(hash(t_gap), hash(t_gap.replace(fold=1)))
4512
4513 SEC = timedelta(0, 1)
4514
4515 def pairs(iterable):
4516 a, b = itertools.tee(iterable)
4517 next(b, None)
4518 return zip(a, b)
4519
4520 class ZoneInfo(tzinfo):
4521 zoneroot = '/usr/share/zoneinfo'
4522 def __init__(self, ut, ti):
4523 """
4524
4525 :param ut: array
4526 Array of transition point timestamps
4527 :param ti: list
4528 A list of (offset, isdst, abbr) tuples
4529 :return: None
4530 """
4531 self.ut = ut
4532 self.ti = ti
4533 self.lt = self.invert(ut, ti)
4534
4535 @staticmethod
4536 def invert(ut, ti):
4537 lt = (ut.__copy__(), ut.__copy__())
4538 if ut:
4539 offset = ti[0][0] // SEC
4540 lt[0][0] = max(-2**31, lt[0][0] + offset)
4541 lt[1][0] = max(-2**31, lt[1][0] + offset)
4542 for i in range(1, len(ut)):
4543 lt[0][i] += ti[i-1][0] // SEC
4544 lt[1][i] += ti[i][0] // SEC
4545 return lt
4546
4547 @classmethod
4548 def fromfile(cls, fileobj):
4549 if fileobj.read(4).decode() != "TZif":
4550 raise ValueError("not a zoneinfo file")
4551 fileobj.seek(32)
4552 counts = array('i')
4553 counts.fromfile(fileobj, 3)
4554 if sys.byteorder != 'big':
4555 counts.byteswap()
4556
4557 ut = array('i')
4558 ut.fromfile(fileobj, counts[0])
4559 if sys.byteorder != 'big':
4560 ut.byteswap()
4561
4562 type_indices = array('B')
4563 type_indices.fromfile(fileobj, counts[0])
4564
4565 ttis = []
4566 for i in range(counts[1]):
4567 ttis.append(struct.unpack(">lbb", fileobj.read(6)))
4568
4569 abbrs = fileobj.read(counts[2])
4570
4571 # Convert ttis
4572 for i, (gmtoff, isdst, abbrind) in enumerate(ttis):
4573 abbr = abbrs[abbrind:abbrs.find(0, abbrind)].decode()
4574 ttis[i] = (timedelta(0, gmtoff), isdst, abbr)
4575
4576 ti = [None] * len(ut)
4577 for i, idx in enumerate(type_indices):
4578 ti[i] = ttis[idx]
4579
4580 self = cls(ut, ti)
4581
4582 return self
4583
4584 @classmethod
4585 def fromname(cls, name):
4586 path = os.path.join(cls.zoneroot, name)
4587 with open(path, 'rb') as f:
4588 return cls.fromfile(f)
4589
4590 EPOCHORDINAL = date(1970, 1, 1).toordinal()
4591
4592 def fromutc(self, dt):
4593 """datetime in UTC -> datetime in local time."""
4594
4595 if not isinstance(dt, datetime):
4596 raise TypeError("fromutc() requires a datetime argument")
4597 if dt.tzinfo is not self:
4598 raise ValueError("dt.tzinfo is not self")
4599
4600 timestamp = ((dt.toordinal() - self.EPOCHORDINAL) * 86400
4601 + dt.hour * 3600
4602 + dt.minute * 60
4603 + dt.second)
4604
4605 if timestamp < self.ut[1]:
4606 tti = self.ti[0]
4607 fold = 0
4608 else:
4609 idx = bisect.bisect_right(self.ut, timestamp)
4610 assert self.ut[idx-1] <= timestamp
4611 assert idx == len(self.ut) or timestamp < self.ut[idx]
4612 tti_prev, tti = self.ti[idx-2:idx]
4613 # Detect fold
4614 shift = tti_prev[0] - tti[0]
4615 fold = (shift > timedelta(0, timestamp - self.ut[idx-1]))
4616 dt += tti[0]
4617 if fold:
4618 return dt.replace(fold=1)
4619 else:
4620 return dt
4621
4622 def _find_ti(self, dt, i):
4623 timestamp = ((dt.toordinal() - self.EPOCHORDINAL) * 86400
4624 + dt.hour * 3600
4625 + dt.minute * 60
4626 + dt.second)
4627 lt = self.lt[dt.fold]
4628 idx = bisect.bisect_right(lt, timestamp)
4629
4630 return self.ti[max(0, idx - 1)][i]
4631
4632 def utcoffset(self, dt):
4633 return self._find_ti(dt, 0)
4634
4635 def dst(self, dt):
4636 isdst = self._find_ti(dt, 1)
4637 # XXX: We cannot accurately determine the "save" value,
4638 # so let's return 1h whenever DST is in effect. Since
4639 # we don't use dst() in fromutc(), it is unlikely that
4640 # it will be needed for anything more than bool(dst()).
4641 return ZERO if isdst else HOUR
4642
4643 def tzname(self, dt):
4644 return self._find_ti(dt, 2)
4645
4646 @classmethod
4647 def zonenames(cls, zonedir=None):
4648 if zonedir is None:
4649 zonedir = cls.zoneroot
4650 for root, _, files in os.walk(zonedir):
4651 for f in files:
4652 p = os.path.join(root, f)
4653 with open(p, 'rb') as o:
4654 magic = o.read(4)
4655 if magic == b'TZif':
4656 yield p[len(zonedir) + 1:]
4657
4658 @classmethod
4659 def stats(cls, start_year=1):
4660 count = gap_count = fold_count = zeros_count = 0
4661 min_gap = min_fold = timedelta.max
4662 max_gap = max_fold = ZERO
4663 min_gap_datetime = max_gap_datetime = datetime.min
4664 min_gap_zone = max_gap_zone = None
4665 min_fold_datetime = max_fold_datetime = datetime.min
4666 min_fold_zone = max_fold_zone = None
4667 stats_since = datetime(start_year, 1, 1) # Starting from 1970 eliminates a lot of noise
4668 for zonename in cls.zonenames():
4669 count += 1
4670 tz = cls.fromname(zonename)
4671 for dt, shift in tz.transitions():
4672 if dt < stats_since:
4673 continue
4674 if shift > ZERO:
4675 gap_count += 1
4676 if (shift, dt) > (max_gap, max_gap_datetime):
4677 max_gap = shift
4678 max_gap_zone = zonename
4679 max_gap_datetime = dt
4680 if (shift, datetime.max - dt) < (min_gap, datetime.max - min _gap_datetime):
4681 min_gap = shift
4682 min_gap_zone = zonename
4683 min_gap_datetime = dt
4684 elif shift < ZERO:
4685 fold_count += 1
4686 shift = -shift
4687 if (shift, dt) > (max_fold, max_fold_datetime):
4688 max_fold = shift
4689 max_fold_zone = zonename
4690 max_fold_datetime = dt
4691 if (shift, datetime.max - dt) < (min_fold, datetime.max - mi n_fold_datetime):
4692 min_fold = shift
4693 min_fold_zone = zonename
4694 min_fold_datetime = dt
4695 else:
4696 zeros_count += 1
4697 trans_counts = (gap_count, fold_count, zeros_count)
4698 print("Number of zones: %5d" % count)
4699 print("Number of transitions: %5d = %d (gaps) + %d (folds) + %d (zeros)" %
4700 ((sum(trans_counts),) + trans_counts))
4701 print("Min gap: %16s at %s in %s" % (min_gap, min_gap_datetime, min_gap_zone))
4702 print("Max gap: %16s at %s in %s" % (max_gap, max_gap_datetime, max_gap_zone))
4703 print("Min fold: %16s at %s in %s" % (min_fold, min_fold_datetime , min_fold_zone))
4704 print("Max fold: %16s at %s in %s" % (max_fold, max_fold_datetime , max_fold_zone))
4705
4706
4707 def transitions(self):
4708 for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)):
4709 shift = ti[0] - prev_ti[0]
4710 yield datetime.utcfromtimestamp(t), shift
4711
4712 def nondst_folds(self):
4713 """Find all folds with the same value of isdst on both sides of the tran sition."""
4714 for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)):
4715 shift = ti[0] - prev_ti[0]
4716 if shift < ZERO and ti[1] == prev_ti[1]:
4717 yield datetime.utcfromtimestamp(t), -shift, prev_ti[2], ti[2]
4718
4719 @classmethod
4720 def print_all_nondst_folds(cls, same_abbr=False, start_year=1):
4721 count = 0
4722 for zonename in cls.zonenames():
4723 tz = cls.fromname(zonename)
4724 for dt, shift, prev_abbr, abbr in tz.nondst_folds():
4725 if dt.year < start_year or same_abbr and prev_abbr != abbr:
4726 continue
4727 count += 1
4728 print("%3d) %-30s %s %10s %5s -> %s" %
4729 (count, zonename, dt, shift, prev_abbr, abbr))
4730
4731 def folds(self):
4732 for t, shift in self.transitions():
4733 if shift < ZERO:
4734 yield t, -shift
4735
4736 def gaps(self):
4737 for t, shift in self.transitions():
4738 if shift > ZERO:
4739 yield t, shift
4740
4741 def zeros(self):
4742 for t, shift in self.transitions():
4743 if not shift:
4744 yield t
4745
4746
4747 class ZoneInfoTest(unittest.TestCase):
4748 zonename = 'America/New_York'
4749
4750 def setUp(self):
4751 self.sizeof_time_t = sysconfig.get_config_var('SIZEOF_TIME_T')
4752 if sys.platform == "win32":
4753 self.skipTest("Skipping zoneinfo tests on Windows")
4754 try:
4755 self.tz = ZoneInfo.fromname(self.zonename)
4756 except FileNotFoundError as err:
4757 self.skipTest("Skipping %s: %s" % (self.zonename, err))
4758
4759 def assertEquivDatetimes(self, a, b):
4760 self.assertEqual((a.replace(tzinfo=None), a.fold, id(a.tzinfo)),
4761 (b.replace(tzinfo=None), b.fold, id(b.tzinfo)))
4762
4763 def test_folds(self):
4764 tz = self.tz
4765 for dt, shift in tz.folds():
4766 for x in [0 * shift, 0.5 * shift, shift - timedelta.resolution]:
4767 udt = dt + x
4768 ldt = tz.fromutc(udt.replace(tzinfo=tz))
4769 self.assertEqual(ldt.fold, 1)
4770 adt = udt.replace(tzinfo=timezone.utc).astimezone(tz)
4771 self.assertEquivDatetimes(adt, ldt)
4772 utcoffset = ldt.utcoffset()
4773 self.assertEqual(ldt.replace(tzinfo=None), udt + utcoffset)
4774 # Round trip
4775 self.assertEquivDatetimes(ldt.astimezone(timezone.utc),
4776 udt.replace(tzinfo=timezone.utc))
4777
4778
4779 for x in [-timedelta.resolution, shift]:
4780 udt = dt + x
4781 udt = udt.replace(tzinfo=tz)
4782 ldt = tz.fromutc(udt)
4783 self.assertEqual(ldt.fold, 0)
4784
4785 def test_gaps(self):
4786 tz = self.tz
4787 for dt, shift in tz.gaps():
4788 for x in [0 * shift, 0.5 * shift, shift - timedelta.resolution]:
4789 udt = dt + x
4790 udt = udt.replace(tzinfo=tz)
4791 ldt = tz.fromutc(udt)
4792 self.assertEqual(ldt.fold, 0)
4793 adt = udt.replace(tzinfo=timezone.utc).astimezone(tz)
4794 self.assertEquivDatetimes(adt, ldt)
4795 utcoffset = ldt.utcoffset()
4796 self.assertEqual(ldt.replace(tzinfo=None), udt.replace(tzinfo=No ne) + utcoffset)
4797 # Create a local time inside the gap
4798 ldt = tz.fromutc(dt.replace(tzinfo=tz)) - shift + x
4799 self.assertLess(ldt.replace(fold=1).utcoffset(),
4800 ldt.replace(fold=0).utcoffset(),
4801 "At %s." % ldt)
4802
4803 for x in [-timedelta.resolution, shift]:
4804 udt = dt + x
4805 ldt = tz.fromutc(udt.replace(tzinfo=tz))
4806 self.assertEqual(ldt.fold, 0)
4807
4808 def test_system_transitions(self):
4809 if ('Riyadh8' in self.zonename or
4810 # From tzdata NEWS file:
4811 # The files solar87, solar88, and solar89 are no longer distributed.
4812 # They were a negative experiment - that is, a demonstration that
4813 # tz data can represent solar time only with some difficulty and err or.
4814 # Their presence in the distribution caused confusion, as Riyadh
4815 # civil time was generally not solar time in those years.
4816 self.zonename.startswith('right/')):
4817 self.skipTest("Skipping %s" % self.zonename)
4818 tz = self.tz
4819 TZ = os.environ.get('TZ')
4820 os.environ['TZ'] = self.zonename
4821 try:
4822 _time.tzset()
4823 for udt, shift in tz.transitions():
4824 if self.zonename == 'Europe/Tallinn' and udt.date() == date(1999 , 10, 31):
4825 print("Skip %s %s transition" % (self.zonename, udt))
4826 continue
4827 if self.sizeof_time_t == 4 and udt.year >= 2037:
4828 print("Skip %s %s transition for 32-bit time_t" % (self.zone name, udt))
4829 continue
4830 s0 = (udt - datetime(1970, 1, 1)) // SEC
4831 ss = shift // SEC # shift seconds
4832 for x in [-40 * 3600, -20*3600, -1, 0,
4833 ss - 1, ss + 20 * 3600, ss + 40 * 3600]:
4834 s = s0 + x
4835 sdt = datetime.fromtimestamp(s)
4836 tzdt = datetime.fromtimestamp(s, tz).replace(tzinfo=None)
4837 self.assertEquivDatetimes(sdt, tzdt)
4838 s1 = sdt.timestamp()
4839 self.assertEqual(s, s1)
4840 if ss > 0: # gap
4841 # Create local time inside the gap
4842 dt = datetime.fromtimestamp(s0) - shift / 2
4843 ts0 = dt.timestamp()
4844 ts1 = dt.replace(fold=1).timestamp()
4845 self.assertEqual(ts0, s0 + ss / 2)
4846 self.assertEqual(ts1, s0 - ss / 2)
4847 finally:
4848 if TZ is None:
4849 del os.environ['TZ']
4850 else:
4851 os.environ['TZ'] = TZ
4852 _time.tzset()
4853
4854
4855 class ZoneInfoCompleteTest(unittest.TestSuite):
4856 def __init__(self):
4857 tests = []
4858 if is_resource_enabled('tzdata'):
4859 for name in ZoneInfo.zonenames():
4860 Test = type('ZoneInfoTest[%s]' % name, (ZoneInfoTest,), {})
4861 Test.zonename = name
4862 for method in dir(Test):
4863 if method.startswith('test_'):
4864 tests.append(Test(method))
4865 super().__init__(tests)
4866
4867 # Iran had a sub-minute UTC offset before 1946.
4868 class IranTest(ZoneInfoTest):
4869 zonename = 'Asia/Tehran'
4870
4871 def load_tests(loader, standard_tests, pattern):
4872 standard_tests.addTest(ZoneInfoCompleteTest())
4873 return standard_tests
4874
4875
4005 if __name__ == "__main__": 4876 if __name__ == "__main__":
4006 unittest.main() 4877 unittest.main()
LEFTRIGHT

RSS Feeds Recent Issues | This issue
This is Rietveld 894c83f36cb7+