diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index cb55afb7176..ae73e162ec2 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1760,3 +1760,10 @@ Notes: (5) For example, if :meth:`utcoffset` returns ``timedelta(hours=-3, minutes=-30)``, ``%z`` is replaced with the string ``'-0330'``. + +.. versionadded:: 3.2 + + When the ``%z`` directive is provided to the :meth:`strptime` + method, an aware :class:`datetime` object will be produced. The + ``tzinfo`` of the result will be set to a :class:`timezone` + instance. \ No newline at end of file diff --git a/Lib/_strptime.py b/Lib/_strptime.py index ee30b4216c2..728a9dc855d 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -16,7 +16,10 @@ 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 +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: @@ -204,6 +207,7 @@ class TimeRE(dict): #XXX: Does 'Y' need to worry about having less or more than # 4 digits? 'Y': r"(?P\d\d\d\d)", + 'z': r"(?P[+-]\d\d[0-5]\d)", 'A': self.__seqToRE(self.locale_time.f_weekday, 'A'), 'a': self.__seqToRE(self.locale_time.a_weekday, 'a'), 'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'), @@ -293,7 +297,9 @@ def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon): def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): - """Return a time struct based on the input string and the format string.""" + """Return a 2-tuple consisting of a time struct and an int containg + the number of microseconds based on the input string and the + format string.""" for index, arg in enumerate([data_string, format]): if not isinstance(arg, str): @@ -333,10 +339,12 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): if len(data_string) != found.end(): raise ValueError("unconverted data remains: %s" % data_string[found.end():]) + year = 1900 month = day = 1 hour = minute = second = fraction = 0 tz = -1 + tzoffset = None # Default to -1 to signify that values not known; not critical to have, # though week_of_year = -1 @@ -417,6 +425,11 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): else: # W starts week on Monday. week_of_year_start = 0 + elif group_key == 'z': + z = found_dict['z'] + tzoffset = int(z[1:3]) * 60 + int(z[3:5]) + if z.startswith("-"): + tzoffset = -tzoffset elif group_key == 'Z': # Since -1 is default value only need to worry about setting tz if # it can be something other than -1. @@ -453,9 +466,35 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): day = datetime_result.day if weekday == -1: weekday = datetime_date(year, month, day).weekday() - return (time.struct_time((year, month, day, - hour, minute, second, - weekday, julian, tz)), fraction) + # Add timezone info + tzname = found_dict.get("Z") + if tzoffset is not None: + gmtoff = tzoffset * 60 + else: + gmtoff = None + + return (year, month, day, + hour, minute, second, + weekday, julian, tz, gmtoff, tzname), fraction def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): - return _strptime(data_string, format)[0] + """Return a time struct based on the input string and the + format string.""" + tt = _strptime(data_string, format)[0] + return time.struct_time(tt[:9]) + +def _strptime_datetime(data_string, format="%a %b %d %H:%M:%S %Y"): + """Return a datetime instace based on the input string and the + format string.""" + tt, fraction = _strptime(data_string, format) + gmtoff, tzname = tt[-2:] + args = tt[:6] + (fraction,) + if gmtoff is not None: + tzdelta = datetime_timedelta(seconds=gmtoff) + if tzname: + tz = datetime_timezone(tzdelta, tzname) + else: + tz = datetime_timezone(tzdelta) + args += (tz,) + + return datetime_datetime(*args) diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index de7cf0f88ef..b320e1f8029 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -17,6 +17,7 @@ from datetime import tzinfo from datetime import time from datetime import timezone from datetime import date, datetime +import time as _time pickle_choices = [(pickle, pickle, proto) for proto in range(3)] assert len(pickle_choices) == 3 @@ -1731,11 +1732,41 @@ class TestDateTime(TestDate): string = '2004-12-01 13:02:47.197' format = '%Y-%m-%d %H:%M:%S.%f' - result, frac = _strptime._strptime(string, format) - expected = self.theclass(*(result[0:6]+(frac,))) + expected = _strptime._strptime_datetime(string, format) got = self.theclass.strptime(string, format) self.assertEqual(expected, got) + strptime = self.theclass.strptime + self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE) + self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE) + # Only local timezone and UTC are supported + for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'), + (-_time.timezone, _time.tzname[0])): + if tzseconds < 0: + sign = '-' + seconds = -tzseconds + else: + sign ='+' + seconds = tzseconds + hours, minutes = divmod(seconds//60, 60) + dtstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname) + dt = strptime(dtstr, "%z %Z") + self.assertEqual(dt.utcoffset(), timedelta(seconds=tzseconds)) + self.assertEqual(dt.tzname(), tzname) + # Can produce inconsistent datetime + dtstr, fmt = "+1234 UTC", "%z %Z" + dt = strptime(dtstr, fmt) + self.assertEqual(dt.utcoffset(), 12 * HOUR + 34 * MINUTE) + self.assertEqual(dt.tzname(), 'UTC') + # yet will roundtrip + self.assertEqual(dt.strftime(fmt), dtstr) + + # Produce naive datetime if no %z is provided + self.assertEqual(strptime("UTC", "%Z").tzinfo, None) + + with self.assertRaises(ValueError): strptime("-2400", "%z") + with self.assertRaises(ValueError): strptime("-000", "%z") + def test_more_timetuple(self): # This tests fields beyond those tested by the TestDate.test_timetuple. t = self.theclass(2004, 12, 31, 6, 22, 33) @@ -3196,6 +3227,7 @@ def first_sunday_on_or_after(dt): return dt ZERO = timedelta(0) +MINUTE = timedelta(minutes=1) HOUR = timedelta(hours=1) DAY = timedelta(days=1) # In the US, DST starts at 2am (standard time) on the first Sunday in April. diff --git a/Misc/NEWS b/Misc/NEWS index 9c5ea77ff37..41ac42dd6b2 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -1322,6 +1322,14 @@ Library Extension Modules ----------------- +- Issue #6641: The ``datetime.strptime`` method now supports the + ``%z`` directive. When the ``%z`` directive is present in the + format string, an aware ``datetime`` object is returned with + ``tzinfo`` bound to a ``datetime.timezone`` instance constructed + from the parsed offset. If both ``%z`` and ``%Z`` are present, the + data in ``%Z`` field is used for timezone name, but ``%Z`` data + without ``%z`` is discarded. + - Issue #5094: The ``datetime`` module now has a simple concrete class implementing ``datetime.tzinfo`` interface. Instances of the new class, ``datetime.timezone``, return fixed name and UTC offset from diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index 71c5cf5d557..ed045205fe7 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -4362,82 +4362,23 @@ datetime_utcfromtimestamp(PyObject *cls, PyObject *args) return result; } -/* Return new datetime from time.strptime(). */ +/* Return new datetime from _strptime.strptime_datetime(). */ static PyObject * datetime_strptime(PyObject *cls, PyObject *args) { static PyObject *module = NULL; - PyObject *result = NULL, *obj, *st = NULL, *frac = NULL; const Py_UNICODE *string, *format; if (!PyArg_ParseTuple(args, "uu:strptime", &string, &format)) return NULL; - if (module == NULL && - (module = PyImport_ImportModuleNoBlock("_strptime")) == NULL) - return NULL; - - /* _strptime._strptime returns a two-element tuple. The first - element is a time.struct_time object. The second is the - microseconds (which are not defined for time.struct_time). */ - obj = PyObject_CallMethod(module, "_strptime", "uu", string, format); - if (obj != NULL) { - int i, good_timetuple = 1; - long int ia[7]; - if (PySequence_Check(obj) && PySequence_Size(obj) == 2) { - st = PySequence_GetItem(obj, 0); - frac = PySequence_GetItem(obj, 1); - if (st == NULL || frac == NULL) - good_timetuple = 0; - /* copy y/m/d/h/m/s values out of the - time.struct_time */ - if (good_timetuple && - PySequence_Check(st) && - PySequence_Size(st) >= 6) { - for (i=0; i < 6; i++) { - PyObject *p = PySequence_GetItem(st, i); - if (p == NULL) { - good_timetuple = 0; - break; - } - if (PyLong_Check(p)) - ia[i] = PyLong_AsLong(p); - else - good_timetuple = 0; - Py_DECREF(p); - } -/* if (PyLong_CheckExact(p)) { - ia[i] = PyLong_AsLongAndOverflow(p, &overflow); - if (overflow) - good_timetuple = 0; - } - else - good_timetuple = 0; - Py_DECREF(p); -*/ } - else - good_timetuple = 0; - /* follow that up with a little dose of microseconds */ - if (PyLong_Check(frac)) - ia[6] = PyLong_AsLong(frac); - else - good_timetuple = 0; - } - else - good_timetuple = 0; - if (good_timetuple) - result = PyObject_CallFunction(cls, "iiiiiii", - ia[0], ia[1], ia[2], - ia[3], ia[4], ia[5], - ia[6]); - else - PyErr_SetString(PyExc_ValueError, - "unexpected value from _strptime._strptime"); + if (module == NULL) { + module = PyImport_ImportModuleNoBlock("_strptime"); + if(module == NULL) + return NULL; } - Py_XDECREF(obj); - Py_XDECREF(st); - Py_XDECREF(frac); - return result; + return PyObject_CallMethod(module, "_strptime_datetime", "uu", + string, format); } /* Return new datetime from date/datetime and time arguments. */