diff options
| author | Alexander Belopolsky <abalkin@users.noreply.github.com> | 2017-07-31 14:26:50 (GMT) |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-07-31 14:26:50 (GMT) |
| commit | 018d353c1c8c87767d2335cd884017c2ce12e045 (patch) | |
| tree | 36a485b724114e393901f3fa16d741cc63bd7cb2 /Lib | |
| parent | c6ea8974e2d939223bfd6d64ee13ec89c090d2e0 (diff) | |
| download | cpython-018d353c1c8c87767d2335cd884017c2ce12e045.zip cpython-018d353c1c8c87767d2335cd884017c2ce12e045.tar.gz cpython-018d353c1c8c87767d2335cd884017c2ce12e045.tar.bz2 | |
Closes issue bpo-5288: Allow tzinfo objects with sub-minute offsets. (#2896)
* Closes issue bpo-5288: Allow tzinfo objects with sub-minute offsets.
* bpo-5288: Implemented %z formatting of sub-minute offsets.
* bpo-5288: Removed mentions of the whole minute limitation on TZ offsets.
* bpo-5288: Removed one more mention of the whole minute limitation.
Thanks @csabella!
* Fix a formatting error in the docs
* Addressed review comments.
Thanks, @haypo.
Diffstat (limited to 'Lib')
| -rw-r--r-- | Lib/datetime.py | 51 | ||||
| -rw-r--r-- | Lib/test/datetimetester.py | 27 |
2 files changed, 47 insertions, 31 deletions
diff --git a/Lib/datetime.py b/Lib/datetime.py index 76a6f95..2f03847 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -206,10 +206,16 @@ def _wrap_strftime(object, format, timetuple): if offset.days < 0: offset = -offset sign = '-' - h, m = divmod(offset, timedelta(hours=1)) - assert not m % timedelta(minutes=1), "whole minute" - m //= timedelta(minutes=1) - zreplace = '%c%02d%02d' % (sign, h, m) + h, rest = divmod(offset, timedelta(hours=1)) + m, rest = divmod(rest, timedelta(minutes=1)) + s = rest.seconds + u = offset.microseconds + if u: + zreplace = '%c%02d%02d%02d.%06d' % (sign, h, m, s, u) + elif s: + zreplace = '%c%02d%02d%02d' % (sign, h, m, s) + else: + zreplace = '%c%02d%02d' % (sign, h, m) assert '%' not in zreplace newformat.append(zreplace) elif ch == 'Z': @@ -241,7 +247,7 @@ def _check_tzname(name): # offset is what it returned. # If offset isn't None or timedelta, raises TypeError. # If offset is None, returns None. -# Else offset is checked for being in range, and a whole # of minutes. +# Else offset is checked for being in range. # If it is, its integer value is returned. Else ValueError is raised. def _check_utc_offset(name, offset): assert name in ("utcoffset", "dst") @@ -250,9 +256,6 @@ def _check_utc_offset(name, offset): if not isinstance(offset, timedelta): raise TypeError("tzinfo.%s() must return None " "or timedelta, not '%s'" % (name, type(offset))) - if offset.microseconds: - raise ValueError("tzinfo.%s() must return a whole number " - "of seconds, got %s" % (name, offset)) if not -timedelta(1) < offset < timedelta(1): raise ValueError("%s()=%s, must be strictly between " "-timedelta(hours=24) and timedelta(hours=24)" % @@ -960,11 +963,11 @@ class tzinfo: raise NotImplementedError("tzinfo subclass must override tzname()") def utcoffset(self, dt): - "datetime -> minutes east of UTC (negative for west of UTC)" + "datetime -> timedelta, positive for east of UTC, negative for west of UTC" raise NotImplementedError("tzinfo subclass must override utcoffset()") def dst(self, dt): - """datetime -> DST offset in minutes east of UTC. + """datetime -> DST offset as timedelta, positive for east of UTC. Return 0 if DST not in effect. utcoffset() must include the DST offset. @@ -1262,8 +1265,8 @@ class time: # Timezone functions def utcoffset(self): - """Return the timezone offset in minutes east of UTC (negative west of - UTC).""" + """Return the timezone offset as timedelta, positive east of UTC + (negative west of UTC).""" if self._tzinfo is None: return None offset = self._tzinfo.utcoffset(None) @@ -1284,8 +1287,8 @@ class time: return name def dst(self): - """Return 0 if DST is not in effect, or the DST offset (in minutes - eastward) if DST is in effect. + """Return 0 if DST is not in effect, or the DST offset (as timedelta + positive eastward) if DST is in effect. This is purely informational; the DST offset has already been added to the UTC offset returned by utcoffset() if applicable, so there's no @@ -1714,7 +1717,7 @@ class datetime(date): return _strptime._strptime_datetime(cls, date_string, format) def utcoffset(self): - """Return the timezone offset in minutes east of UTC (negative west of + """Return the timezone offset as timedelta positive east of UTC (negative west of UTC).""" if self._tzinfo is None: return None @@ -1736,8 +1739,8 @@ class datetime(date): return name def dst(self): - """Return 0 if DST is not in effect, or the DST offset (in minutes - eastward) if DST is in effect. + """Return 0 if DST is not in effect, or the DST offset (as timedelta + positive eastward) if DST is in effect. This is purely informational; the DST offset has already been added to the UTC offset returned by utcoffset() if applicable, so there's no @@ -1962,9 +1965,6 @@ class timezone(tzinfo): raise ValueError("offset must be a timedelta " "strictly between -timedelta(hours=24) and " "timedelta(hours=24).") - if (offset.microseconds != 0 or offset.seconds % 60 != 0): - raise ValueError("offset must be a timedelta " - "representing a whole number of minutes") return cls._create(offset, name) @classmethod @@ -2053,8 +2053,15 @@ class timezone(tzinfo): else: sign = '+' hours, rest = divmod(delta, timedelta(hours=1)) - minutes = rest // timedelta(minutes=1) - return 'UTC{}{:02d}:{:02d}'.format(sign, hours, minutes) + minutes, rest = divmod(rest, timedelta(minutes=1)) + seconds = rest.seconds + microseconds = rest.microseconds + if microseconds: + return (f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}' + f'.{microseconds:06d}') + if seconds: + return f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}' + return f'UTC{sign}{hours:02d}:{minutes:02d}' timezone.utc = timezone._create(timedelta(0)) timezone.min = timezone._create(timezone._minoffset) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 2200888..29b70e1 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -255,14 +255,15 @@ class TestTimeZone(unittest.TestCase): self.assertEqual(timezone.min.utcoffset(None), -limit) self.assertEqual(timezone.max.utcoffset(None), limit) - def test_constructor(self): self.assertIs(timezone.utc, timezone(timedelta(0))) self.assertIsNot(timezone.utc, timezone(timedelta(0), 'UTC')) self.assertEqual(timezone.utc, timezone(timedelta(0), 'UTC')) + for subminute in [timedelta(microseconds=1), timedelta(seconds=1)]: + tz = timezone(subminute) + self.assertNotEqual(tz.utcoffset(None) % timedelta(minutes=1), 0) # invalid offsets - for invalid in [timedelta(microseconds=1), timedelta(1, 1), - timedelta(seconds=1), timedelta(1), -timedelta(1)]: + for invalid in [timedelta(1, 1), timedelta(1)]: self.assertRaises(ValueError, timezone, invalid) self.assertRaises(ValueError, timezone, -invalid) @@ -301,6 +302,15 @@ class TestTimeZone(unittest.TestCase): self.assertEqual('UTC-00:01', timezone(timedelta(minutes=-1)).tzname(None)) self.assertEqual('XYZ', timezone(-5 * HOUR, 'XYZ').tzname(None)) + # Sub-minute offsets: + self.assertEqual('UTC+01:06:40', timezone(timedelta(0, 4000)).tzname(None)) + self.assertEqual('UTC-01:06:40', + timezone(-timedelta(0, 4000)).tzname(None)) + self.assertEqual('UTC+01:06:40.000001', + timezone(timedelta(0, 4000, 1)).tzname(None)) + self.assertEqual('UTC-01:06:40.000001', + timezone(-timedelta(0, 4000, 1)).tzname(None)) + with self.assertRaises(TypeError): self.EST.tzname('') with self.assertRaises(TypeError): self.EST.tzname(5) @@ -2152,6 +2162,9 @@ class TestDateTime(TestDate): t = self.theclass(2004, 12, 31, 6, 22, 33, 47) self.assertEqual(t.strftime("%m %d %y %f %S %M %H %j"), "12 31 04 000047 33 22 06 366") + tz = timezone(-timedelta(hours=2, seconds=33, microseconds=123)) + t = t.replace(tzinfo=tz) + self.assertEqual(t.strftime("%z"), "-020033.000123") def test_extract(self): dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) @@ -2717,8 +2730,8 @@ class TZInfoBase: def utcoffset(self, dt): return timedelta(microseconds=61) def dst(self, dt): return timedelta(microseconds=-81) t = cls(1, 1, 1, tzinfo=C7()) - self.assertRaises(ValueError, t.utcoffset) - self.assertRaises(ValueError, t.dst) + self.assertEqual(t.utcoffset(), timedelta(microseconds=61)) + self.assertEqual(t.dst(), timedelta(microseconds=-81)) def test_aware_compare(self): cls = self.theclass @@ -4297,7 +4310,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): self.assertEqual(gdt.strftime("%c %Z"), 'Mon Jun 23 22:00:00 1941 UTC') - def test_constructors(self): t = time(0, fold=1) dt = datetime(1, 1, 1, fold=1) @@ -4372,7 +4384,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): self.assertEqual(t0.fold, 0) self.assertEqual(t1.fold, 1) - @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') def test_timestamp(self): dt0 = datetime(2014, 11, 2, 1, 30) @@ -4390,7 +4401,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): s1 = t.replace(fold=1).timestamp() self.assertEqual(s0 + 1800, s1) - @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') def test_astimezone(self): dt0 = datetime(2014, 11, 2, 1, 30) @@ -4406,7 +4416,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): self.assertEqual(adt0.fold, 0) self.assertEqual(adt1.fold, 0) - def test_pickle_fold(self): t = time(fold=1) dt = datetime(1, 1, 1, fold=1) |
