Skip to content
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