diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -667,6 +667,21 @@ self = object.__new__(cls) self.__setstate(year) return self + if isinstance(year, str) and month is None: + if len(year) == 8: + y = year[:4] + m = year[4:6] + d = year[6:8] + elif len(year) == 10 and year[4] == year[7] == '-': + y,m,d = year.split('-') + else: + raise ValueError('invalid date string %r' % year) + try: + year = int(y) + month = int(m) + day = int(d) + except ValueError: + raise ValueError('invalid date string') _check_date_fields(year, month, day) self = object.__new__(cls) self._year = year @@ -1024,6 +1039,48 @@ # Pickle support self.__setstate(hour, minute or None) return self + if isinstance(hour, str): + if hour.endswith('Z'): + hour = hour[:-1] + tzinfo = timezone.utc + idx = None + if hour.count('-') == 1: + idx = hour.index('-') + if hour.count('+') == 1: + idx = hour.index('+') + if idx is not None: + tzinfo = timezone(hour[idx:]) + hour = hour[:idx] + idx = None + if hour.count('.'): + idx = hour.index('.') + if hour.count(','): + idx = hour.index(',') + if idx is not None: + microsecond = int(hour[idx+1:]) + hour = hour[:idx] + if len(hour) == 2: + hour = int(hour) + elif len(hour) == 4: + minute = int(hour[2:]) + hour = int(hour[:2]) + elif len(hour) == 5: + if hour[2] == ':': + minute = int(hour[3:]) + hour = int(hour[:2]) + else: + raise ValueError('invalid time string') + elif len(hour) == 6: + second = int(hour[4:]) + minute = int(hour[2:4]) + hour = int(hour[:2]) + elif len(hour) == 8: + if hour[2] == hour[5] == ':': + second = int(hour[6:]) + minute = int(hour[3:5]) + hour = int(hour[:2]) + else: + raise ValueError('invalid time string') _check_tzinfo_arg(tzinfo) _check_time_fields(hour, minute, second, microsecond) self._hour = hour @@ -1315,6 +1372,15 @@ self = date.__new__(cls, year[:4]) self.__setstate(year, month) return self + if isinstance(year, str): + idx = None + if year.count('T') == 1: + idx = year.index('T') + if year.count(' ') == 1: + idx = year.index(' ') + if idx is None: + return date.__new__(cls, year) + return cls.combine(date(year[:idx]), time(year[idx+1:])) _check_tzinfo_arg(tzinfo) _check_time_fields(hour, minute, second, microsecond) self = date.__new__(cls, year, month, day) @@ -1822,8 +1888,22 @@ # Sentinel value to disallow None _Omitted = object() def __new__(cls, offset, name=_Omitted): + if isinstance(offset, str): + if name is not cls._Omitted: + raise ValueError("name with str offset is not allowed") + if offset == 'Z': + return cls.utc + elif len(offset) == 5: + return cls(timedelta(hours=int(offset[:3]), + minutes=int(offset[3:]))) + elif len(offset) == 6: + return cls(timedelta(hours=int(offset[:3]), + minutes=int(offset[4:]))) + else: + raise ValueError("invalid timezone string") + if not isinstance(offset, timedelta): - raise TypeError("offset must be a timedelta") + raise TypeError("offset must be a timedelta or str") if name is cls._Omitted: if not offset: return cls.utc diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -186,6 +186,13 @@ with self.assertRaises(TypeError): timezone(ZERO, 42) with self.assertRaises(TypeError): timezone(ZERO, 'ABC', 'extra') + # string offset + self.assertIs(timezone.utc, timezone('Z')) + self.assertIs(timezone.utc, timezone('+0000')) + self.assertIs(timezone.utc, timezone('-0000')) + self.assertIs(timezone.utc, timezone('+00:00')) + self.assertIs(timezone.utc, timezone('-00:00')) + def test_inheritance(self): self.assertIsInstance(timezone.utc, tzinfo) self.assertIsInstance(self.EST, tzinfo) @@ -756,6 +763,13 @@ # few tests that TestDateTime overrides. theclass = date + def test_iso_constructor(self): + dt = self.theclass(2002, 3, 1) + self.assertEqual(self.theclass(dt.isoformat()), dt) + dt = self.theclass(1, 1, 1) + self.assertEqual(self.theclass(dt.isoformat()), dt) + dt = self.theclass(9999, 9, 9) + self.assertEqual(self.theclass(dt.isoformat()), dt) def test_basic_attributes(self): dt = self.theclass(2002, 3, 1) @@ -2155,34 +2169,42 @@ t = self.theclass(4, 5, 1, 123) self.assertEqual(t.isoformat(), "04:05:01.000123") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass() self.assertEqual(t.isoformat(), "00:00:00") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass(microsecond=1) self.assertEqual(t.isoformat(), "00:00:00.000001") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass(microsecond=10) self.assertEqual(t.isoformat(), "00:00:00.000010") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass(microsecond=100) self.assertEqual(t.isoformat(), "00:00:00.000100") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass(microsecond=1000) self.assertEqual(t.isoformat(), "00:00:00.001000") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass(microsecond=10000) self.assertEqual(t.isoformat(), "00:00:00.010000") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) t = self.theclass(microsecond=100000) self.assertEqual(t.isoformat(), "00:00:00.100000") self.assertEqual(t.isoformat(), str(t)) + self.assertEqual(self.theclass(t.isoformat()), t) def test_1653736(self): # verify it doesn't accept extra keyword arguments @@ -2323,11 +2345,39 @@ def test_backdoor_resistance(self): # see TestDate.test_backdoor_resistance(). - base = '2:59.0' - for hour_byte in ' ', '9', chr(24), '\xff': + base = b'2:59.0' + for hour_byte in b' ', b'9', bytes([24]), b'\xff': self.assertRaises(TypeError, self.theclass, hour_byte + base[1:]) + def test_from_string(self): + s = "00" + self.assertEqual(self.theclass(s), self.theclass(0)) + s = "00:00" + self.assertEqual(self.theclass(s), self.theclass(0)) + s = "00:00:00" + self.assertEqual(self.theclass(s), self.theclass(0)) + s = "00:00:00.000000" + self.assertEqual(self.theclass(s), self.theclass(0)) + tz = timezone.utc + s = "00Z" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00:00Z" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00:00:00Z" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00:00:00.000000Z" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00+0000" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00:00+0000" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00:00:00+0000" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + s = "00:00:00.000000+0000" + self.assertEqual(self.theclass(s), self.theclass(0, tzinfo=tz)) + + # A mixin for classes with a tzinfo= argument. Subclasses must define # theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) # must be legit (which is true for time and datetime).