Skip to content

Commit 3d4d2dd

Browse files
committed
gh-56959: Add cross-platform support for %s strftime-format code
1 parent 7fb315b commit 3d4d2dd

5 files changed

Lines changed: 77 additions & 0 deletions

File tree

Doc/library/datetime.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2704,6 +2704,9 @@ convenience.
27042704
| | (empty string if the object is | +06:34:15, | |
27052705
| | naive). | -03:07:12.345216 | |
27062706
+-----------+--------------------------------+------------------------+-------+
2707+
| ``%s`` | UNIX timestamp (the number of | 1404212722 | \(11) |
2708+
| | seconds since the Epoch). | | |
2709+
+-----------+--------------------------------+------------------------+-------+
27072710

27082711
The full set of format codes supported varies across platforms, because Python
27092712
calls the platform C library's :c:func:`strftime` function, and platform
@@ -2924,6 +2927,12 @@ Notes:
29242927
:meth:`~.datetime.strptime` calls using a format string containing
29252928
``%e`` without a year now emit a :exc:`DeprecationWarning`.
29262929

2930+
(11)
2931+
.. versionadded:: 3.15
2932+
``%s`` format code support is added for only :meth:`~.datetime.strftime`
2933+
and :meth:`~.date.strftime`. In previous versions, ``%s`` format code
2934+
behavior was undefined and varied across platforms.
2935+
29272936
.. rubric:: Footnotes
29282937

29292938
.. [#] If, that is, we ignore the effects of relativity.

Lib/_pydatetime.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ def _wrap_strftime(object, format, timetuple):
220220
zreplace = None # the string to use for %z
221221
colonzreplace = None # the string to use for %:z
222222
Zreplace = None # the string to use for %Z
223+
sreplace = None # the string to use for %s
223224

224225
# Scan format for %z, %:z and %Z escapes, replacing as needed.
225226
newformat = []
@@ -270,6 +271,10 @@ def _wrap_strftime(object, format, timetuple):
270271
# strftime is going to have at this: escape %
271272
Zreplace = s.replace('%', '%%')
272273
newformat.append(Zreplace)
274+
elif ch == 's' and hasattr(object, "timestamp"):
275+
if sreplace is None:
276+
sreplace = str(_math.floor(object.timestamp()))
277+
newformat.append(sreplace)
273278
# Note that datetime(1000, 1, 1).strftime('%G') == '1000' so
274279
# year 1000 for %G can go on the fast path.
275280
elif (ch in 'YGFC' and timetuple[0] < 1000 and

Lib/test/datetimetester.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3214,6 +3214,31 @@ def test_strftime_special(self):
32143214
self.assertEqual(t.strftime('\0%c\0%B'), f'\0{s1}\0{s2}')
32153215
self.assertEqual(t.strftime('%c\0%B\0'), f'{s1}\0{s2}\0')
32163216

3217+
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
3218+
def test_strftime_naive_s(self):
3219+
t = self.theclass(1970, 1, 1)
3220+
self.assertEqual(t.strftime('%s'), '18000')
3221+
t = self.theclass(1970, 1, 1, 1, 2, 3, 4)
3222+
self.assertEqual(t.strftime('%s'),
3223+
str(18000 + 3600 + 2*60 + 3))
3224+
3225+
def test_strftime_respect_timezone_s(self):
3226+
t = self.theclass(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
3227+
self.assertEqual(t.strftime('%s'), '0')
3228+
3229+
t = self.theclass(1970, 1, 1, 1, 2, 3, 600000, tzinfo=timezone.utc)
3230+
self.assertEqual(t.strftime('%s'),
3231+
str(3600 + 2*60 + 3))
3232+
3233+
t = self.theclass(1970, 1, 1, 1, 2, 3, 400000,
3234+
tzinfo=timezone(timedelta(hours=-5), 'EST'))
3235+
self.assertEqual(t.strftime('%s'),
3236+
str(18000 + 3600 + 2*60 + 3))
3237+
3238+
t = self.theclass(1969, 1, 1, 0, 0, 0, 700000,
3239+
tzinfo=timezone.utc)
3240+
self.assertEqual(t.strftime('%s'), '-31536000')
3241+
32173242
def test_extract(self):
32183243
dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
32193244
self.assertEqual(dt.date(), date(2002, 3, 4))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add cross-platform support for the ``%s`` strftime format code to the :mod:`datetime` module.

Modules/_datetimemodule.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,32 @@ make_freplacement(PyObject *object)
18711871
return PyUnicode_FromString(freplacement);
18721872
}
18731873

1874+
static PyObject *
1875+
make_sreplacement(PyObject *object)
1876+
{
1877+
PyObject *timestamp = PyObject_CallMethodNoArgs(object, &_Py_ID(timestamp));
1878+
if (timestamp == NULL) {
1879+
return NULL;
1880+
}
1881+
1882+
PyObject *math = PyImport_ImportModule("math");
1883+
if (math == NULL) {
1884+
Py_DECREF(timestamp);
1885+
return NULL;
1886+
}
1887+
1888+
PyObject *temp = PyObject_CallMethodObjArgs(math, &_Py_ID(floor), timestamp, NULL);
1889+
Py_DECREF(math);
1890+
Py_DECREF(timestamp);
1891+
if (temp == NULL) {
1892+
return NULL;
1893+
}
1894+
1895+
PyObject *sreplacement = PyObject_Str(temp);
1896+
Py_DECREF(temp);
1897+
return sreplacement;
1898+
}
1899+
18741900
/* I sure don't want to reproduce the strftime code from the time module,
18751901
* so this imports the module and calls it. All the hair is due to
18761902
* giving special meanings to the %z, %:z, %Z and %f format codes via a
@@ -1888,6 +1914,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
18881914
PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */
18891915
PyObject *Zreplacement = NULL; /* py string, replacement for %Z */
18901916
PyObject *freplacement = NULL; /* py string, replacement for %f */
1917+
PyObject *sreplacement = NULL; /* py string, replacement for %s */
18911918

18921919
assert(object && format && timetuple);
18931920
assert(PyUnicode_Check(format));
@@ -1964,6 +1991,15 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
19641991
}
19651992
replacement = freplacement;
19661993
}
1994+
else if (ch == 's' && PyDateTime_Check(object)) {
1995+
/* format timestamp */
1996+
if (sreplacement == NULL) {
1997+
sreplacement = make_sreplacement(object);
1998+
if (sreplacement == NULL)
1999+
goto Error;
2000+
}
2001+
replacement = sreplacement;
2002+
}
19672003
else if (normalize_century()
19682004
&& (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C'))
19692005
{
@@ -2055,6 +2091,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
20552091
Py_XDECREF(zreplacement);
20562092
Py_XDECREF(colonzreplacement);
20572093
Py_XDECREF(Zreplacement);
2094+
Py_XDECREF(sreplacement);
20582095
Py_XDECREF(strftime);
20592096
return result;
20602097

0 commit comments

Comments
 (0)