From fc070d27316ebb0ed877fade811130ac566bbc14 Mon Sep 17 00:00:00 2001 From: Skip Montanaro Date: Sat, 15 Mar 2008 16:04:45 +0000 Subject: [PATCH] add %f format to datetime - issue 1158 --- Doc/library/datetime.rst | 53 ++++++++++++++++---- Lib/_strptime.py | 21 +++++--- Lib/test/test_datetime.py | 17 ++++--- Lib/test/test_strptime.py | 66 +++++++++++++----------- Modules/datetimemodule.c | 102 +++++++++++++++++++++++++++++--------- Modules/timemodule.c | 2 +- 6 files changed, 181 insertions(+), 80 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 34772503003..32813b98b70 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1489,9 +1489,31 @@ 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 ``0`` for the month and day. -For :class:`date` objects, the format codes for hours, minutes, and seconds -should not be used, as :class:`date` objects have no such values. If they're -used anyway, ``0`` is substituted for them. +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. + +:class:`time` and :class:`datetime` objects support a ``%f`` format code +which expands to the number of microseconds in the object, zero-padded on +the left to six places. + +.. versionadded:: 2.6 + +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 + -HHMM, where HH is a 2-digit string giving the number of UTC offset hours, and + MM is a 2-digit string giving the number of UTC offset minutes. For example, if + :meth:`utcoffset` returns ``timedelta(hours=-3, minutes=-30)``, ``%z`` is + replaced with the string ``'-0330'``. + +``%Z`` + If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty string. + Otherwise ``%Z`` is replaced by the returned value, which must be a string. The full set of format codes supported varies across platforms, because Python calls the platform C library's :func:`strftime` function, and platform @@ -1524,6 +1546,10 @@ platforms. Regardless of platform, years before 1900 cannot be used. | ``%d`` | Day of the month as a decimal | | | | number [01,31]. | | +-----------+--------------------------------+-------+ +| ``%f`` | Microsecond as a decimal | \(1) | +| | number [0,999999], zero-padded | | +| | on the left | | ++-----------+--------------------------------+-------+ | ``%H`` | Hour (24-hour clock) as a | | | | decimal number [00,23]. | | +-----------+--------------------------------+-------+ @@ -1539,13 +1565,13 @@ platforms. Regardless of platform, years before 1900 cannot be used. | ``%M`` | Minute as a decimal number | | | | [00,59]. | | +-----------+--------------------------------+-------+ -| ``%p`` | Locale's equivalent of either | \(1) | +| ``%p`` | Locale's equivalent of either | \(2) | | | AM or PM. | | +-----------+--------------------------------+-------+ -| ``%S`` | Second as a decimal number | \(2) | +| ``%S`` | Second as a decimal number | \(3) | | | [00,61]. | | +-----------+--------------------------------+-------+ -| ``%U`` | Week number of the year | \(3) | +| ``%U`` | Week number of the year | \(4) | | | (Sunday as the first day of | | | | the week) as a decimal number | | | | [00,53]. All days in a new | | @@ -1556,7 +1582,7 @@ platforms. Regardless of platform, years before 1900 cannot be used. | ``%w`` | Weekday as a decimal number | | | | [0(Sunday),6]. | | +-----------+--------------------------------+-------+ -| ``%W`` | Week number of the year | \(3) | +| ``%W`` | Week number of the year | \(4) | | | (Monday as the first day of | | | | the week) as a decimal number | | | | [00,53]. All days in a new | | @@ -1576,7 +1602,7 @@ platforms. Regardless of platform, years before 1900 cannot be used. | ``%Y`` | Year with century as a decimal | | | | number. | | +-----------+--------------------------------+-------+ -| ``%z`` | UTC offset in the form +HHMM | \(4) | +| ``%z`` | UTC offset in the form +HHMM | \(5) | | | or -HHMM (empty string if the | | | | the object is naive). | | +-----------+--------------------------------+-------+ @@ -1589,17 +1615,22 @@ platforms. Regardless of platform, years before 1900 cannot be used. Notes: (1) + When used with the :func:`strptime` function, the ``%f`` directive + accepts from one to six digits and zero pads on the right. ``%f`` is + an extension to the set of format characters in the C standard. + +(2) When used with the :func:`strptime` function, the ``%p`` directive only affects the output hour field if the ``%I`` directive is used to parse the hour. -(2) +(3) The range really is ``0`` to ``61``; this accounts for leap seconds and the (very rare) double leap seconds. -(3) +(4) When used with the :func:`strptime` function, ``%U`` and ``%W`` are only used in calculations when the day of the week and the year are specified. -(4) +(5) For example, if :meth:`utcoffset` returns ``timedelta(hours=-3, minutes=-30)``, ``%z`` is replaced with the string ``'-0330'``. diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 166cf822036..d9563b9e2b8 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -22,7 +22,7 @@ try: except: from dummy_thread import allocate_lock as _thread_allocate_lock -__all__ = ['strptime'] +__all__ = [] def _getlang(): # Figure out what the current language is set to. @@ -190,6 +190,7 @@ class TimeRE(dict): base.__init__({ # The " \d" part of the regex is to make %c from ANSI C work 'd': r"(?P3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])", + 'f': r"(?P[0-9]{1,6})", 'H': r"(?P2[0-3]|[0-1]\d|\d)", 'I': r"(?P1[0-2]|0[1-9]|[1-9])", 'j': r"(?P36[0-6]|3[0-5]\d|[1-2]\d\d|0[1-9]\d|00[1-9]|[1-9]\d|0[1-9]|[1-9])", @@ -291,7 +292,7 @@ def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon): return 1 + days_to_week + day_of_week -def strptime(data_string, format="%a %b %d %H:%M:%S %Y"): +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.""" global _TimeRE_cache, _regex_cache with _cache_lock: @@ -327,7 +328,7 @@ def strptime(data_string, format="%a %b %d %H:%M:%S %Y"): data_string[found.end():]) year = 1900 month = day = 1 - hour = minute = second = 0 + hour = minute = second = fraction = 0 tz = -1 # Default to -1 to signify that values not known; not critical to have, # though @@ -384,6 +385,11 @@ def strptime(data_string, format="%a %b %d %H:%M:%S %Y"): minute = int(found_dict['M']) elif group_key == 'S': second = int(found_dict['S']) + elif group_key == 'f': + s = found_dict['f'] + # Pad to always return microseconds. + s += "0" * (6 - len(s)) + fraction = int(s) elif group_key == 'A': weekday = locale_time.f_weekday.index(found_dict['A'].lower()) elif group_key == 'a': @@ -440,6 +446,9 @@ 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)) + return (time.struct_time((year, month, day, + hour, minute, second, + weekday, julian, tz)), fraction) + +def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): + return _strptime(data_string, format)[0] diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index ee104e45ed5..84f45b46824 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -1507,11 +1507,12 @@ class TestDateTime(TestDate): self.failUnless(abs(from_timestamp - from_now) <= tolerance) def test_strptime(self): - import time + import _strptime - string = '2004-12-01 13:02:47' - format = '%Y-%m-%d %H:%M:%S' - expected = self.theclass(*(time.strptime(string, format)[0:6])) + 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,))) got = self.theclass.strptime(string, format) self.assertEqual(expected, got) @@ -1539,9 +1540,9 @@ class TestDateTime(TestDate): def test_more_strftime(self): # This tests fields beyond those tested by the TestDate.test_strftime. - t = self.theclass(2004, 12, 31, 6, 22, 33) - self.assertEqual(t.strftime("%m %d %y %S %M %H %j"), - "12 31 04 33 22 06 366") + 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") def test_extract(self): dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) @@ -1814,7 +1815,7 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): def test_strftime(self): t = self.theclass(1, 2, 3, 4) - self.assertEqual(t.strftime('%H %M %S'), "01 02 03") + 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'"), "'' ''") diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 92c722ab155..2aa4bc90246 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -208,11 +208,11 @@ class StrptimeTests(unittest.TestCase): def test_ValueError(self): # Make sure ValueError is raised when match fails or format is bad - self.assertRaises(ValueError, _strptime.strptime, data_string="%d", + self.assertRaises(ValueError, _strptime._strptime_time, data_string="%d", format="%A") for bad_format in ("%", "% ", "%e"): try: - _strptime.strptime("2005", bad_format) + _strptime._strptime_time("2005", bad_format) except ValueError: continue except Exception, err: @@ -223,12 +223,12 @@ class StrptimeTests(unittest.TestCase): def test_unconverteddata(self): # Check ValueError is raised when there is unconverted data - self.assertRaises(ValueError, _strptime.strptime, "10 12", "%m") + self.assertRaises(ValueError, _strptime._strptime_time, "10 12", "%m") def helper(self, directive, position): """Helper fxn in testing.""" strf_output = time.strftime("%" + directive, self.time_tuple) - strp_output = _strptime.strptime(strf_output, "%" + directive) + strp_output = _strptime._strptime_time(strf_output, "%" + directive) self.failUnless(strp_output[position] == self.time_tuple[position], "testing of '%s' directive failed; '%s' -> %s != %s" % (directive, strf_output, strp_output[position], @@ -241,7 +241,7 @@ class StrptimeTests(unittest.TestCase): # Must also make sure %y values are correct for bounds set by Open Group for century, bounds in ((1900, ('69', '99')), (2000, ('00', '68'))): for bound in bounds: - strp_output = _strptime.strptime(bound, '%y') + strp_output = _strptime._strptime_time(bound, '%y') expected_result = century + int(bound) self.failUnless(strp_output[0] == expected_result, "'y' test failed; passed in '%s' " @@ -260,7 +260,7 @@ class StrptimeTests(unittest.TestCase): # Test hour directives self.helper('H', 3) strf_output = time.strftime("%I %p", self.time_tuple) - strp_output = _strptime.strptime(strf_output, "%I %p") + strp_output = _strptime._strptime_time(strf_output, "%I %p") self.failUnless(strp_output[3] == self.time_tuple[3], "testing of '%%I %%p' directive failed; '%s' -> %s != %s" % (strf_output, strp_output[3], self.time_tuple[3])) @@ -273,6 +273,12 @@ class StrptimeTests(unittest.TestCase): # Test second directives self.helper('S', 5) + def test_fraction(self): + import datetime + now = datetime.datetime.now() + tup, frac = _strptime._strptime(str(now), format="%Y-%m-%d %H:%M:%S.%f") + self.assertEqual(frac, now.microsecond) + def test_weekday(self): # Test weekday directives for directive in ('A', 'a', 'w'): @@ -287,16 +293,16 @@ class StrptimeTests(unittest.TestCase): # When gmtime() is used with %Z, entire result of strftime() is empty. # Check for equal timezone names deals with bad locale info when this # occurs; first found in FreeBSD 4.4. - strp_output = _strptime.strptime("UTC", "%Z") + strp_output = _strptime._strptime_time("UTC", "%Z") self.failUnlessEqual(strp_output.tm_isdst, 0) - strp_output = _strptime.strptime("GMT", "%Z") + strp_output = _strptime._strptime_time("GMT", "%Z") self.failUnlessEqual(strp_output.tm_isdst, 0) if sys.platform == "mac": # Timezones don't really work on MacOS9 return time_tuple = time.localtime() strf_output = time.strftime("%Z") #UTC does not have a timezone - strp_output = _strptime.strptime(strf_output, "%Z") + strp_output = _strptime._strptime_time(strf_output, "%Z") locale_time = _strptime.LocaleTime() if time.tzname[0] != time.tzname[1] or not time.daylight: self.failUnless(strp_output[8] == time_tuple[8], @@ -320,7 +326,7 @@ class StrptimeTests(unittest.TestCase): original_daylight = time.daylight time.tzname = (tz_name, tz_name) time.daylight = 1 - tz_value = _strptime.strptime(tz_name, "%Z")[8] + tz_value = _strptime._strptime_time(tz_name, "%Z")[8] self.failUnlessEqual(tz_value, -1, "%s lead to a timezone value of %s instead of -1 when " "time.daylight set to %s and passing in %s" % @@ -347,7 +353,7 @@ class StrptimeTests(unittest.TestCase): def test_percent(self): # Make sure % signs are handled properly strf_output = time.strftime("%m %% %Y", self.time_tuple) - strp_output = _strptime.strptime(strf_output, "%m %% %Y") + strp_output = _strptime._strptime_time(strf_output, "%m %% %Y") self.failUnless(strp_output[0] == self.time_tuple[0] and strp_output[1] == self.time_tuple[1], "handling of percent sign failed") @@ -355,17 +361,17 @@ class StrptimeTests(unittest.TestCase): def test_caseinsensitive(self): # Should handle names case-insensitively. strf_output = time.strftime("%B", self.time_tuple) - self.failUnless(_strptime.strptime(strf_output.upper(), "%B"), + self.failUnless(_strptime._strptime_time(strf_output.upper(), "%B"), "strptime does not handle ALL-CAPS names properly") - self.failUnless(_strptime.strptime(strf_output.lower(), "%B"), + self.failUnless(_strptime._strptime_time(strf_output.lower(), "%B"), "strptime does not handle lowercase names properly") - self.failUnless(_strptime.strptime(strf_output.capitalize(), "%B"), + self.failUnless(_strptime._strptime_time(strf_output.capitalize(), "%B"), "strptime does not handle capword names properly") def test_defaults(self): # Default return value should be (1900, 1, 1, 0, 0, 0, 0, 1, 0) defaults = (1900, 1, 1, 0, 0, 0, 0, 1, -1) - strp_output = _strptime.strptime('1', '%m') + strp_output = _strptime._strptime_time('1', '%m') self.failUnless(strp_output == defaults, "Default values for strptime() are incorrect;" " %s != %s" % (strp_output, defaults)) @@ -377,7 +383,7 @@ class StrptimeTests(unittest.TestCase): # escaped. # Test instigated by bug #796149 . need_escaping = ".^$*+?{}\[]|)(" - self.failUnless(_strptime.strptime(need_escaping, need_escaping)) + self.failUnless(_strptime._strptime_time(need_escaping, need_escaping)) class Strptime12AMPMTests(unittest.TestCase): """Test a _strptime regression in '%I %p' at 12 noon (12 PM)""" @@ -386,8 +392,8 @@ class Strptime12AMPMTests(unittest.TestCase): eq = self.assertEqual eq(time.strptime('12 PM', '%I %p')[3], 12) eq(time.strptime('12 AM', '%I %p')[3], 0) - eq(_strptime.strptime('12 PM', '%I %p')[3], 12) - eq(_strptime.strptime('12 AM', '%I %p')[3], 0) + eq(_strptime._strptime_time('12 PM', '%I %p')[3], 12) + eq(_strptime._strptime_time('12 AM', '%I %p')[3], 0) class JulianTests(unittest.TestCase): @@ -397,7 +403,7 @@ class JulianTests(unittest.TestCase): eq = self.assertEqual for i in range(1, 367): # use 2004, since it is a leap year, we have 366 days - eq(_strptime.strptime('%d 2004' % i, '%j %Y')[7], i) + eq(_strptime._strptime_time('%d 2004' % i, '%j %Y')[7], i) class CalculationTests(unittest.TestCase): """Test that strptime() fills in missing info correctly""" @@ -408,7 +414,7 @@ class CalculationTests(unittest.TestCase): def test_julian_calculation(self): # Make sure that when Julian is missing that it is calculated format_string = "%Y %m %d %H %M %S %w %Z" - result = _strptime.strptime(time.strftime(format_string, self.time_tuple), + result = _strptime._strptime_time(time.strftime(format_string, self.time_tuple), format_string) self.failUnless(result.tm_yday == self.time_tuple.tm_yday, "Calculation of tm_yday failed; %s != %s" % @@ -417,7 +423,7 @@ class CalculationTests(unittest.TestCase): def test_gregorian_calculation(self): # Test that Gregorian date can be calculated from Julian day format_string = "%Y %H %M %S %w %j %Z" - result = _strptime.strptime(time.strftime(format_string, self.time_tuple), + result = _strptime._strptime_time(time.strftime(format_string, self.time_tuple), format_string) self.failUnless(result.tm_year == self.time_tuple.tm_year and result.tm_mon == self.time_tuple.tm_mon and @@ -431,7 +437,7 @@ class CalculationTests(unittest.TestCase): def test_day_of_week_calculation(self): # Test that the day of the week is calculated as needed format_string = "%Y %m %d %H %S %j %Z" - result = _strptime.strptime(time.strftime(format_string, self.time_tuple), + result = _strptime._strptime_time(time.strftime(format_string, self.time_tuple), format_string) self.failUnless(result.tm_wday == self.time_tuple.tm_wday, "Calculation of day of the week failed;" @@ -445,7 +451,7 @@ class CalculationTests(unittest.TestCase): format_string = "%%Y %%%s %%w" % directive dt_date = datetime_date(*ymd_tuple) strp_input = dt_date.strftime(format_string) - strp_output = _strptime.strptime(strp_input, format_string) + strp_output = _strptime._strptime_time(strp_input, format_string) self.failUnless(strp_output[:3] == ymd_tuple, "%s(%s) test failed w/ '%s': %s != %s (%s != %s)" % (test_reason, directive, strp_input, @@ -484,11 +490,11 @@ class CacheTests(unittest.TestCase): def test_time_re_recreation(self): # Make sure cache is recreated when current locale does not match what # cached object was created with. - _strptime.strptime("10", "%d") - _strptime.strptime("2005", "%Y") + _strptime._strptime_time("10", "%d") + _strptime._strptime_time("2005", "%Y") _strptime._TimeRE_cache.locale_time.lang = "Ni" original_time_re = id(_strptime._TimeRE_cache) - _strptime.strptime("10", "%d") + _strptime._strptime_time("10", "%d") self.failIfEqual(original_time_re, id(_strptime._TimeRE_cache)) self.failUnlessEqual(len(_strptime._regex_cache), 1) @@ -502,7 +508,7 @@ class CacheTests(unittest.TestCase): while len(_strptime._regex_cache) <= _strptime._CACHE_MAX_SIZE: _strptime._regex_cache[bogus_key] = None bogus_key += 1 - _strptime.strptime("10", "%d") + _strptime._strptime_time("10", "%d") self.failUnlessEqual(len(_strptime._regex_cache), 1) def test_new_localetime(self): @@ -510,7 +516,7 @@ class CacheTests(unittest.TestCase): # is created. locale_time_id = id(_strptime._TimeRE_cache.locale_time) _strptime._TimeRE_cache.locale_time.lang = "Ni" - _strptime.strptime("10", "%d") + _strptime._strptime_time("10", "%d") self.failIfEqual(locale_time_id, id(_strptime._TimeRE_cache.locale_time)) @@ -522,13 +528,13 @@ class CacheTests(unittest.TestCase): except locale.Error: return try: - _strptime.strptime('10', '%d') + _strptime._strptime_time('10', '%d') # Get id of current cache object. first_time_re_id = id(_strptime._TimeRE_cache) try: # Change the locale and force a recreation of the cache. locale.setlocale(locale.LC_TIME, ('de_DE', 'UTF8')) - _strptime.strptime('10', '%d') + _strptime._strptime_time('10', '%d') # Get the new cache object's id. second_time_re_id = id(_strptime._TimeRE_cache) # They should not be equal. diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index fb11e35cb6c..89423abc626 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -1130,10 +1130,24 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, return 0; } +static PyObject * +make_freplacement(PyObject *object) +{ + char freplacement[7]; + if (PyTime_Check(object)) + sprintf(freplacement, "%06d", TIME_GET_MICROSECOND(object)); + else if (PyDateTime_Check(object)) + sprintf(freplacement, "%06d", DATE_GET_MICROSECOND(object)); + else + sprintf(freplacement, "%06d", 0); + + return PyString_FromStringAndSize(freplacement, strlen(freplacement)); +} + /* I sure don't want to reproduce the strftime code from the time module, * so this imports the module and calls it. All the hair is due to - * giving special meanings to the %z and %Z format codes via a preprocessing - * step on the format string. + * giving special meanings to the %z, %Z and %f format codes via a + * preprocessing step on the format string. * tzinfoarg is the argument to pass to the object's tzinfo method, if * needed. */ @@ -1145,6 +1159,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *zreplacement = NULL; /* py string, replacement for %z */ PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ + PyObject *freplacement = NULL; /* py string, replacement for %f */ char *pin; /* pointer to next char in input format */ char ch; /* next char in input format */ @@ -1186,11 +1201,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } } - /* Scan the input format, looking for %z and %Z escapes, building + /* Scan the input format, looking for %z/%Z/%f escapes, building * a new format. Since computing the replacements for those codes * is expensive, don't unless they're actually used. */ - totalnew = PyString_Size(format) + 1; /* realistic if no %z/%Z */ + totalnew = PyString_Size(format) + 1; /* realistic if no %z/%Z/%f */ newfmt = PyString_FromStringAndSize(NULL, totalnew); if (newfmt == NULL) goto Done; pnew = PyString_AsString(newfmt); @@ -1272,6 +1287,18 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, ptoappend = PyString_AS_STRING(Zreplacement); ntoappend = PyString_GET_SIZE(Zreplacement); } + else if (ch == 'f') { + /* format microseconds */ + if (freplacement == NULL) { + freplacement = make_freplacement(object); + if (freplacement == NULL) + goto Done; + } + assert(freplacement != NULL); + assert(PyString_Check(freplacement)); + ptoappend = PyString_AS_STRING(freplacement); + ntoappend = PyString_GET_SIZE(freplacement); + } else { /* percent followed by neither z nor Z */ ptoappend = pin - 2; @@ -1313,6 +1340,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_DECREF(time); } Done: + Py_XDECREF(freplacement); Py_XDECREF(zreplacement); Py_XDECREF(Zreplacement); Py_XDECREF(newfmt); @@ -3853,43 +3881,69 @@ datetime_utcfromtimestamp(PyObject *cls, PyObject *args) static PyObject * datetime_strptime(PyObject *cls, PyObject *args) { - PyObject *result = NULL, *obj, *module; + static PyObject *module = NULL; + PyObject *result = NULL, *obj, *st = NULL, *frac = NULL; const char *string, *format; if (!PyArg_ParseTuple(args, "ss:strptime", &string, &format)) return NULL; - if ((module = PyImport_ImportModuleNoBlock("time")) == NULL) + if (module == NULL && + (module = PyImport_ImportModuleNoBlock("_strptime")) == NULL) return NULL; - obj = PyObject_CallMethod(module, "strptime", "ss", string, format); - Py_DECREF(module); + /* _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", "ss", string, format); if (obj != NULL) { int i, good_timetuple = 1; - long int ia[6]; - if (PySequence_Check(obj) && PySequence_Size(obj) >= 6) - for (i=0; i < 6; i++) { - PyObject *p = PySequence_GetItem(obj, i); - if (p == NULL) { - Py_DECREF(obj); - return NULL; + 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 (PyInt_Check(p)) + ia[i] = PyInt_AsLong(p); + else + good_timetuple = 0; + Py_DECREF(p); } - if (PyInt_Check(p)) - ia[i] = PyInt_AsLong(p); - else - good_timetuple = 0; - Py_DECREF(p); } + else + good_timetuple = 0; + /* follow that up with a little dose of microseconds */ + if (PyInt_Check(frac)) + ia[6] = PyInt_AsLong(frac); + else + good_timetuple = 0; + } else good_timetuple = 0; if (good_timetuple) - result = PyObject_CallFunction(cls, "iiiiii", - ia[0], ia[1], ia[2], ia[3], ia[4], ia[5]); + 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 time.strptime"); - Py_DECREF(obj); + "unexpected value from _strptime._strptime"); } + Py_XDECREF(obj); + Py_XDECREF(st); + Py_XDECREF(frac); return result; } diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 5b4d21031fc..bd1ad06f49f 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -520,7 +520,7 @@ time_strptime(PyObject *self, PyObject *args) if (!strptime_module) return NULL; - strptime_result = PyObject_CallMethod(strptime_module, "strptime", "O", args); + strptime_result = PyObject_CallMethod(strptime_module, "_strptime_time", "O", args); Py_DECREF(strptime_module); return strptime_result; }