Skip to content

WIP: bpo-1100942: Add datetime.time.strptime and datetime.date.strptime #5578

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

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
38 changes: 31 additions & 7 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,15 @@ Other constructors, all class methods:
date.max.toordinal()``. For any date *d*, ``date.fromordinal(d.toordinal()) ==
d``.

.. classmethod:: date.strptime(date_string, format)

Return a :class:`date` corresponding to *date_string*, parsed according to
*format*. :exc:`ValueError` is raised if the date string and format can't be
parsed by :meth:`datetime.time.strptime`, or if time components are present
in the format string. For a complete list of formatting directives, see
:ref:`strftime-strptime-behavior`.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add something like "For a complete list of formatting directives, see strftime() and strptime() Behavior." with a link? (copy the sentence from datetime.datetime.strptime) Just from this doc, I have no idea of what is the expected format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

.. versionadded:: 3.8

.. classmethod:: date.fromisoformat(date_string)

Expand Down Expand Up @@ -1426,6 +1435,19 @@ day, and subject to adjustment via a :class:`tzinfo` object.
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 class methods:

.. classmethod:: time.strptime(date_string, format)

Return a :class:`datetime.time` corresponding to *date_string*, parsed
according to *format*. :exc:`ValueError` is raised if the date string and
format can't be parsed by :meth:`time.strptime`. For a complete list of
formatting directives, see :ref:`strftime-strptime-behavior`.

.. versionadded:: 3.8


Class attributes:


Expand Down Expand Up @@ -2021,15 +2043,17 @@ Conversely, the :meth:`datetime.strptime` class method creates a
corresponding format string. ``datetime.strptime(date_string, format)`` is
equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``, except
when the format includes sub-second components or timezone offset information,
which are supported in ``datetime.strptime`` but are discarded by ``time.strptime``.
which are supported in ``datetime.strptime`` but are discarded by
:meth:`time.strptime`.

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.
The :meth:`date.strptime` class method creates a :class:`date` object from a
string representing a date and a corresponding format string. :exc:`ValueError`
is raised if the format codes for hours, minutes, seconds, or microseconds are
used.

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:`.time.strptime` class 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.

The full set of format codes supported varies across platforms, because Python
calls the platform C library's :func:`strftime` function, and platform
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ Added :func:`~gettext.pgettext` and its variants.
(Contributed by Franz Glasner, Éric Araujo, and Cheryl Sabella in :issue:`2504`.)


datetime
--------

Added :func:`~datetime.date.strptime` and :func:`~datetime.time.strptime`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect you lose the reference to date and time, so all you are saying is you added two functions with the same name repeated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vadmium yep, here, we just add the class methods datetime.date.strptime and datetime.time.strptime

(Patch by Alexander Belopolsky, Amaury Forgeot d'Arc, Berker Peksag, Josh-sf,
Juraez Bochi, Maciej Szulik, Matheus Vieira Portela. Contributed by Stéphane
Wirtel)


gc
--

Expand Down
25 changes: 23 additions & 2 deletions Lib/_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from re import IGNORECASE
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)
from _thread import allocate_lock as _thread_allocate_lock
Expand Down Expand Up @@ -307,9 +308,9 @@ def _calc_julian_from_V(iso_year, iso_week, iso_weekday):


def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
"""Return a 2-tuple consisting of a time struct and an int containing
"""Return a 3-tuple consisting of a time struct and an int containing
the number of microseconds based on the input string and the
format string."""
format string, and the UTC offset."""

for index, arg in enumerate([data_string, format]):
if not isinstance(arg, str):
Expand Down Expand Up @@ -556,6 +557,10 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
hour, minute, second,
weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction

date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', '%G',
'%u', '%V', '%w', '%W', '%x', '%y', '%Y', '%G', '%u', '%V',)
time_specs = ('%H', '%I', '%M', '%S', '%f',)

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."""
Expand All @@ -577,3 +582,19 @@ def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
args += (tz,)

return cls(*args)

def _strptime_datetime_date(data_string, format):
"""Return a date based on the input string and the format string."""
msg = "'{!s}' {} not valid in date format specification."
from _datetime import _check_invalid_datetime_specs
if _check_invalid_datetime_specs(format, time_specs, msg):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not move the msg string into the check function, and just pass the bit that varies ('date' or 'time') as an argument? It would make the code easier to read.

Looks like the check function either raises an exception or returns True. It would be clearer to not use an if statement here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the beginning, this PR was a submitted patch by other contributors, I just wanted to convert it to a PR. and now, I try to fix all the issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but for this point, the first step was the conversion of _check_invalid_datetime_specs to C, and after start to improve the code with the other recommendations. (from @vstinner and @pganssle)

_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."""
msg = "'{!s}' {} not valid in time format specification."
from _datetime import _check_invalid_datetime_specs
if _check_invalid_datetime_specs(format, date_specs, msg):
_time = _strptime_datetime(datetime_datetime, data_string, format)
return _time.time()
28 changes: 27 additions & 1 deletion Lib/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ class date:
fromtimestamp()
today()
fromordinal()
strptime()

Operators:

Expand Down Expand Up @@ -885,6 +886,16 @@ def fromisoformat(cls, date_string):
raise ValueError(f'Invalid isoformat string: {date_string!r}')


@classmethod
def strptime(cls, date_string, format):
"""string, format -> new date instance parsed from a string.

>>> datetime.date.strptime('2012/07/20', '%Y/%m/%d')
datetime.date(2012, 7, 20)
"""
import _strptime
return _strptime._strptime_datetime_date(date_string, format)

# Conversions to string

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

__new__()
strptime()

Operators:

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

@staticmethod
def strptime(time_string, format):
"""string, format -> new time instance parsed from a string.

>>> datetime.time.strptime('10:40am', '%H:%M%p')
datetime.time(10, 40)
"""
import _strptime
return _strptime._strptime_datetime_time(time_string, format)

# Read-only field accessors
@property
def hour(self):
Expand Down Expand Up @@ -1906,7 +1928,11 @@ def __str__(self):

@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.

>>> datetime.datetime.strptime('2012/07/20 10:40am', '%Y/%m/%d %H:%M%p')
datetime.datetime(2012, 7, 20, 10, 40)
"""
import _strptime
return _strptime._strptime_datetime(cls, date_string, format)

Expand Down
43 changes: 43 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,30 @@ def test_delta_non_days_ignored(self):
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:
with self.subTest(date_string=date_string,
date_format=date_format,
expected=expected):
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'),
('2018-01-01', ''),
('01', '%M'),
('02', '%H'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs at least two more test cases:

    ('2018-01-01 00:00', '%Y-%m-%d %H:%M'),
    ('2018-01-01', ''),

Both should fail.

]
for hour, format in tests:
with self.subTest(hour=hour, format=format):
with self.assertRaises(ValueError):
date.strptime(hour, format)


class SubclassDate(date):
sub_var = 1

Expand Down Expand Up @@ -3105,6 +3129,25 @@ def test_strftime(self):
except UnicodeEncodeError:
pass

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'),
('12:30:15', ''),
]
for date_string, date_format in tests:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to use with self.subTest for these parametrized tests.

Also, per the other comment I guess you need to add something like ('12:30:15', '')` to get full coverage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need two more test cases:

    ('1900-01-01 12:30', '%Y-%m-%d %H:%M'),
    ('12:30:15', ''),

date.strptime has similarly missing tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test with ('1900-01-01 12:30', '%Y-%m-%d %H:%M') does not raise an exception but returns datetime.time(12, 30)

For the other test, yep, there is an issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that it doesn't raise an exception is an issue. I'm a bit surprised that it doesn't raise an exception on pure Python, that's a bug, because I'm pretty sure that:

datetime.time.strptime("1901-01-01 12:30", "%Y-%m-%d %H:%M") does raise an exception.

with self.subTest(date_string=date_string, date_format=date_format):
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, gmtoff = _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))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :func:`datetime.date.strptime` and :func:`datetime.time.strptime` class methods.
Loading