Skip to content

gh-41431: Add datetime.time.strptime() and datetime.date.strptime() #120752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ struct _Py_global_strings {
STRUCT_FOR_ID(_shutdown)
STRUCT_FOR_ID(_slotnames)
STRUCT_FOR_ID(_strptime)
STRUCT_FOR_ID(_strptime_datetime)
STRUCT_FOR_ID(_strptime_datetime_date)
STRUCT_FOR_ID(_strptime_datetime_datetime)
STRUCT_FOR_ID(_strptime_datetime_time)
STRUCT_FOR_ID(_swappedbytes_)
STRUCT_FOR_ID(_type_)
STRUCT_FOR_ID(_uninitialized_submodules)
Expand Down
4 changes: 3 additions & 1 deletion Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,7 @@ class date:
fromtimestamp()
today()
fromordinal()
strptime()

Operators:

Expand Down Expand Up @@ -1008,6 +1009,12 @@ def fromisocalendar(cls, year, week, day):
This is the inverse of the date.isocalendar() function"""
return cls(*_isoweek_to_gregorian(year, week, day))

@classmethod
def strptime(cls, date_string, format):
'string, format -> new date parsed from a string (like time.strptime()).'
import _strptime
return _strptime._strptime_datetime_date(cls, date_string, format)

# Conversions to string

def __repr__(self):
Expand Down Expand Up @@ -1328,6 +1335,7 @@ class time:
Constructors:

__new__()
strptime()

Operators:

Expand Down Expand Up @@ -1386,6 +1394,12 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold
self._fold = fold
return self

@classmethod
def strptime(cls, date_string, format):
'string, format -> new time parsed from a string (like time.strptime()).'
import _strptime
return _strptime._strptime_datetime_time(cls, date_string, format)

# Read-only field accessors
@property
def hour(self):
Expand Down Expand Up @@ -2092,7 +2106,7 @@ def __str__(self):
def strptime(cls, date_string, format):
'string, format -> new datetime parsed from a string (like time.strptime()).'
import _strptime
return _strptime._strptime_datetime(cls, date_string, format)
return _strptime._strptime_datetime_datetime(cls, date_string, format)

def utcoffset(self):
"""Return the timezone offset as timedelta positive east of UTC (negative west of
Expand Down
27 changes: 25 additions & 2 deletions Lib/_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,31 @@ def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
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
def _strptime_datetime_date(cls, data_string, format="%a %b %d %Y"):
"""Return a date instance based on the input string and the
format string."""
tt, _, _ = _strptime(data_string, format)
args = tt[:3]
return cls(*args)

def _strptime_datetime_time(cls, data_string, format="%H:%M:%S"):
"""Return a time instance based on the input string and the
format string."""
tt, fraction, gmtoff_fraction = _strptime(data_string, format)
tzname, gmtoff = tt[-2:]
args = tt[3:6] + (fraction,)
if gmtoff is not None:
tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction)
if tzname:
tz = datetime_timezone(tzdelta, tzname)
else:
tz = datetime_timezone(tzdelta)
args += (tz,)

return cls(*args)

def _strptime_datetime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
"""Return a datetime instance based on the input string and the
format string."""
tt, fraction, gmtoff_fraction = _strptime(data_string, format)
tzname, gmtoff = tt[-2:]
Expand Down
143 changes: 140 additions & 3 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,65 @@ def test_delta_non_days_ignored(self):
dt2 = dt - delta
self.assertEqual(dt2, dt - days)

def test_strptime(self):
string = '2004-12-01'
format = '%Y-%m-%d'
expected = _strptime._strptime_datetime_date(date, string, format)
got = date.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(expected), date)
self.assertIs(type(got), date)

# bpo-34482: Check that surrogates are handled properly.
inputs = [
('2004-12\ud80001', '%Y-%m\ud800%d'),
('2004\ud80012-01', '%Y\ud800%m-%d'),
]
for string, format in inputs:
with self.subTest(string=string, format=format):
expected = _strptime._strptime_datetime_date(date, string,
format)
got = date.strptime(string, format)
self.assertEqual(expected, got)

def test_strptime_single_digit(self):
# bpo-34903: Check that single digit dates are allowed.
strptime = date.strptime
with self.assertRaises(ValueError):
# %y does require two digits.
newdate = strptime('01/02/3', '%d/%m/%y')

d1 = date(2003, 2, 1)
d2 = date(2003, 1, 2)
d3 = date(2003, 1, 25)
inputs = [
('%d', '1/02/03', '%d/%m/%y', d1),
('%m', '01/2/03', '%d/%m/%y', d1),
('%j', '2/03', '%j/%y', d2),
('%w', '6/04/03', '%w/%U/%y', d1),
# %u requires a single digit.
('%W', '6/4/2003', '%u/%W/%Y', d1),
('%V', '6/4/2003', '%u/%V/%G', d3),
]
for reason, string, format, target in inputs:
reason = 'test single digit ' + reason
with self.subTest(reason=reason,
string=string,
format=format,
target=target):
newdate = strptime(string, format)
self.assertEqual(newdate, target, msg=reason)

@warnings_helper.ignore_warnings(category=DeprecationWarning)
def test_strptime_leap_year(self):
# GH-70647: warns if parsing a format with a day and no year.
with self.assertRaises(ValueError):
# The existing behavior that GH-70647 seeks to change.
date.strptime('02-29', '%m-%d')
with self._assertNotWarns(DeprecationWarning):
date.strptime('20-03-14', '%y-%m-%d')
date.strptime('02-29,2024', '%m-%d,%Y')

class SubclassDate(date):
sub_var = 1

Expand Down Expand Up @@ -2718,7 +2777,8 @@ def test_utcnow(self):
def test_strptime(self):
string = '2004-12-01 13:02:47.197'
format = '%Y-%m-%d %H:%M:%S.%f'
expected = _strptime._strptime_datetime(self.theclass, string, format)
expected = _strptime._strptime_datetime_datetime(self.theclass, string,
format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(expected), self.theclass)
Expand All @@ -2732,8 +2792,8 @@ def test_strptime(self):
]
for string, format in inputs:
with self.subTest(string=string, format=format):
expected = _strptime._strptime_datetime(self.theclass, string,
format)
expected = _strptime._strptime_datetime_datetime(self.theclass,
string, format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)

Expand Down Expand Up @@ -3726,6 +3786,83 @@ def test_compat_unpickle(self):
derived = loads(data, encoding='latin1')
self.assertEqual(derived, expected)

def test_strptime(self):
string = '13:02:47.197'
format = '%H:%M:%S.%f'
expected = _strptime._strptime_datetime_time(self.theclass, string,
format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(expected), self.theclass)
self.assertIs(type(got), self.theclass)

# bpo-34482: Check that surrogates are handled properly.
inputs = [
('13:02\ud80047.197', '%H:%M\ud800%S.%f'),
('13\ud80002:47.197', '%H\ud800%M:%S.%f'),
]
for string, format in inputs:
with self.subTest(string=string, format=format):
expected = _strptime._strptime_datetime_time(self.theclass,
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)
self.assertEqual(
strptime("-00:02:01.000003", "%z").utcoffset(),
-timedelta(minutes=2, seconds=1, microseconds=3)
)
# 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)
tstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname)
t = strptime(tstr, "%z %Z")
self.assertEqual(t.utcoffset(), timedelta(seconds=tzseconds))
self.assertEqual(t.tzname(), tzname)

# Can produce inconsistent time
tstr, fmt = "+1234 UTC", "%z %Z"
t = strptime(tstr, fmt)
self.assertEqual(t.utcoffset(), 12 * HOUR + 34 * MINUTE)
self.assertEqual(t.tzname(), 'UTC')
# yet will roundtrip
self.assertEqual(t.strftime(fmt), tstr)

# Produce naive time 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")
with self.assertRaises(ValueError): strptime("z", "%z")

def test_strptime_single_digit(self):
# bpo-34903: Check that single digit times are allowed.
t = self.theclass(4, 5, 6)
inputs = [
('%H', '4:05:06', '%H:%M:%S', t),
('%M', '04:5:06', '%H:%M:%S', t),
('%S', '04:05:6', '%H:%M:%S', t),
('%I', '4am:05:06', '%I%p:%M:%S', t),
]
for reason, string, format, target in inputs:
reason = 'test single digit ' + reason
with self.subTest(reason=reason,
string=string,
format=format,
target=target):
newdate = self.theclass.strptime(string, format)
self.assertEqual(newdate, target, msg=reason)

def test_bool(self):
# time is always True.
cls = self.theclass
Expand Down
Loading
Loading