diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -421,16 +421,30 @@ Other constructors, all class methods: .. classmethod:: date.fromordinal(ordinal) Return the date corresponding to the proleptic Gregorian ordinal, where January 1 of year 1 has ordinal 1. :exc:`ValueError` is raised unless ``1 <= ordinal <= date.max.toordinal()``. For any date *d*, ``date.fromordinal(d.toordinal()) == d``. + +Other constructors, all static methods: + +.. staticmethod:: date.strptime(date_string, format) + + Return a :class:`date` corresponding to *date_string*, parsed according to + *format*. This is equivalent to ``date(*(time.strptime(date_string, + format)[0:3]))``. :exc:`ValueError` is raised if the *date_string* and + format can't be parsed by `time.strptime`, or if it returns a value where + the time part is nonzero. + + .. versionadded:: 3.4 + + Class attributes: .. attribute:: date.min The earliest representable date, ``date(MINYEAR, 1, 1)``. .. attribute:: date.max @@ -778,17 +792,17 @@ Other constructors, all class methods: are ignored. .. classmethod:: datetime.strptime(date_string, format) Return a :class:`.datetime` corresponding to *date_string*, parsed according to *format*. This is equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``. :exc:`ValueError` is raised if the date_string and format - can't be parsed by :func:`time.strptime` or if it returns a value which isn't a + cannot be parsed by :func:`time.strptime` or if it returns a value which isn't a time tuple. See section :ref:`strftime-strptime-behavior`. Class attributes: .. attribute:: datetime.min @@ -1273,16 +1287,30 @@ day, and subject to adjustment via a :cl * ``0 <= hour < 24`` * ``0 <= minute < 60`` * ``0 <= second < 60`` * ``0 <= microsecond < 1000000``. If an argument outside those ranges is given, :exc:`ValueError` is raised. All default to ``0`` except *tzinfo*, which defaults to :const:`None`. + +Other constructors, all static methods: + +.. staticmethod:: time.strptime(date_string, format) + + Return a :class:`time` corresponding to *date_string*, parsed according to + *format*. This is equivalent to ``time(*(time.strptime(date_string, + format)[4:6]))``. :exc:`ValueError` is raised if the date string and + format can't be parsed by `time.strptime`, if it returns a value which + isn't a time tuple, or if the time part is nonzero. + + .. versionadded:: 3.4 + + Class attributes: .. attribute:: time.min The earliest representable :class:`.time`, ``time(0, 0, 0, 0)``. @@ -1733,23 +1761,23 @@ control of an explicit format string. B acts like the :mod:`time` module's ``time.strftime(fmt, d.timetuple())`` although not all objects support a :meth:`timetuple` method. Conversely, the :meth:`datetime.strptime` class method creates a :class:`.datetime` object from a string representing a date and time and a corresponding format string. ``datetime.strptime(date_string, format)`` is equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``. -For :class:`.time` objects, the format codes for year, month, and day should not -be used, as time objects have no such values. If they're used anyway, ``1900`` -is substituted for the year, and ``1`` for the month and day. - -For :class:`date` objects, the format codes for hours, minutes, seconds, and -microseconds should not be used, as :class:`date` objects have no such -values. If they're used anyway, ``0`` is substituted for them. +The :meth:`date.strptime` static method creates a :class:`date` object from a +string representing a date and a corresponding format string. :exc:`ValueError` +raised if the format codes for hours, minutes, seconds, and microseconds are used. + +The :meth:`.time.strptime` static method creates a :class:`.time` object from a +string representing a time and a corresponding format string. :exc:`ValueError` +raised if the format codes for year, month, and day are used. For a naive object, the ``%z`` and ``%Z`` format codes are replaced by empty strings. For an aware object: ``%z`` :meth:`utcoffset` is transformed into a 5-character string of the form +HHMM or diff --git a/Lib/_strptime.py b/Lib/_strptime.py --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -12,16 +12,17 @@ FUNCTIONS: """ import time import locale import calendar from re import compile as re_compile from re import IGNORECASE, ASCII from re import escape as re_escape from datetime import (date as datetime_date, + datetime as datetime_datetime, timedelta as datetime_timedelta, timezone as datetime_timezone) try: from _thread import allocate_lock as _thread_allocate_lock except ImportError: from _dummy_thread import allocate_lock as _thread_allocate_lock __all__ = [] @@ -483,16 +484,20 @@ def _strptime(data_string, format="%a %b # use the default of 1900 for computations. We set it back to ensure # that February 29th is smaller than March 1st. year = 1900 return (year, month, day, hour, minute, second, weekday, julian, tz, tzname, gmtoff), fraction +date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', + '%w', '%W', '%x', '%y', '%Y',) +time_specs = ('%T', '%R', '%H', '%I', '%M', '%S', '%f', '%i', '%s',) + def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a time struct based on the input string and the format string.""" tt = _strptime(data_string, format)[0] return time.struct_time(tt[:time._STRUCT_TM_ITEMS]) def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): """Return a class cls instance based on the input string and the @@ -504,8 +509,37 @@ def _strptime_datetime(cls, data_string, tzdelta = datetime_timedelta(seconds=gmtoff) if tzname: tz = datetime_timezone(tzdelta, tzname) else: tz = datetime_timezone(tzdelta) args += (tz,) return cls(*args) + +def _strptime_datetime_date(data_string, format): + """Return a date based on the input string and the format string.""" + if not format: + raise ValueError("Date format is not valid.") + msg = "'{!s}' {} not valid in date format specification." + if _check_invalid_datetime_specs(format, time_specs, msg): + _date = _strptime_datetime(datetime_datetime, data_string, format) + return _date.date() + +def _strptime_datetime_time(data_string, format): + """Return a time based on the input string and the format string.""" + if not format: + raise ValueError("Date format is not valid.") + msg = "'{!s}' {} not valid in time format specification." + if _check_invalid_datetime_specs(format, date_specs, msg): + _time = _strptime_datetime(datetime_datetime, data_string, format) + return _time.time() + +def _check_invalid_datetime_specs(fmt, specs, msg): + found_invalid_specs = [] + for spec in specs: + if spec in fmt: + found_invalid_specs.append(spec) + if found_invalid_specs: + suffix = "are" if len(found_invalid_specs) > 1 else "is" + raise ValueError(msg.format(", ".join(found_invalid_specs), + suffix)) + return True diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -628,16 +628,17 @@ class date: """Concrete date type. Constructors: __new__() fromtimestamp() today() fromordinal() + strptime() Operators: __repr__, __str__ __cmp__, __hash__ __add__, __radd__, __sub__ (add/radd only with timedelta arg) Methods: @@ -693,16 +694,24 @@ class date: """Contruct a date from a proleptic Gregorian ordinal. January 1 of year 1 is day 1. Only the year, month and day are non-zero in the result. """ y, m, d = _ord2ymd(n) return cls(y, m, d) + @staticmethod + def strptime(date_string, format): + """Return a new date instance parsed from a string for the + given format. + """ + import _strptime + return _strptime._strptime_datetime_date(date_string, format) + # Conversions to string def __repr__(self): """Convert to formal string, for repr(). >>> dt = datetime(2010, 1, 1) >>> repr(dt) 'datetime.datetime(2010, 1, 1, 0, 0)' @@ -987,16 +996,17 @@ class tzinfo: _tzinfo_class = tzinfo class time: """Time with time zone. Constructors: __new__() + strptime() Operators: __repr__, __str__ __cmp__, __hash__ Methods: @@ -1028,16 +1038,24 @@ class time: _check_time_fields(hour, minute, second, microsecond) self._hour = hour self._minute = minute self._second = second self._microsecond = microsecond self._tzinfo = tzinfo return self + @staticmethod + def strptime(time_string, format): + """string, format -> new time instance parsed from a string + (like time.strptime()). + """ + import _strptime + return _strptime._strptime_datetime_time(time_string, format) + # Read-only field accessors @property def hour(self): """hour (0-23)""" return self._hour @property def minute(self): @@ -1594,17 +1612,17 @@ class datetime(date): return s def __str__(self): "Convert to string, for str()." return self.isoformat(sep=' ') @classmethod def strptime(cls, date_string, format): - 'string, format -> new datetime parsed from a string (like time.strptime()).' + """string, format -> new datetime parsed from a string.""" import _strptime return _strptime._strptime_datetime(cls, date_string, format) def utcoffset(self): """Return the timezone offset in minutes east of UTC (negative west of UTC).""" if self._tzinfo is None: return None diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -745,16 +745,33 @@ class TestDateOnly(unittest.TestCase): self.assertEqual(dt2, dt + days) dt2 = delta + dt self.assertEqual(dt2, dt + days) dt2 = dt - delta self.assertEqual(dt2, dt - days) + def test_strptime_valid_format(self): + tests = [(('2004-12-01', '%Y-%m-%d'), + date(2004, 12, 1)), + (('2004', '%Y'), date(2004, 1, 1)),] + for (date_string, date_format), expected in tests: + self.assertEqual(expected, date.strptime(date_string, date_format)) + + def test_strptime_invalid_format(self): + tests = [('2004-12-01 13:02:47.197', + '%Y-%m-%d %H:%M:%S.%f'), + ('01', '%M'), + ('00', '%H'),] + for test in tests: + with self.assertRaises(ValueError): + date.strptime(test[0], test[1]) + + class SubclassDate(date): sub_var = 1 class TestDate(HarmlessMixedComparison, unittest.TestCase): # Tests here should pass for both dates and datetimes, except for a # few tests that TestDateTime overrides. theclass = date @@ -2192,16 +2209,32 @@ class TestTime(HarmlessMixedComparison, self.assertRaises(TypeError, t.isoformat, foo=3) def test_strftime(self): t = self.theclass(1, 2, 3, 4) self.assertEqual(t.strftime('%H %M %S %f'), "01 02 03 000004") # A naive object replaces %z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") + def test_strptime_invalid(self): + tests = [('2004-12-01 13:02:47.197', + '%Y-%m-%d %H:%M:%S.%f'), + ('2004-12-01', '%Y-%m-%d'),] + for date_string, date_format in tests: + with self.assertRaises(ValueError): + time.strptime(date_string, date_format) + + def test_strptime_valid(self): + string = '13:02:47.197' + format = '%H:%M:%S.%f' + result, frac = _strptime._strptime(string, format) + expected = self.theclass(*(result[3:6]) + (frac,)) + got = time.strptime(string, format) + self.assertEqual(expected, got) + def test_format(self): t = self.theclass(1, 2, 3, 4) self.assertEqual(t.__format__(''), str(t)) # check that a derived class's __str__() gets called class A(self.theclass): def __str__(self): return 'A'