Skip to content
Open
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
9 changes: 9 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2704,6 +2704,9 @@ convenience.
| | (empty string if the object is | +06:34:15, | |
| | naive). | -03:07:12.345216 | |
+-----------+--------------------------------+------------------------+-------+
| ``%s`` | UNIX timestamp (the number of | 1404212722 | \(11) |
| | seconds since the Epoch). | | |
+-----------+--------------------------------+------------------------+-------+

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

(11)
.. versionadded:: 3.15
``%s`` format code support is added for only :meth:`~.datetime.strftime`
and :meth:`~.date.strftime`. In previous versions, ``%s`` format code
behavior was undefined and varied across platforms.

.. rubric:: Footnotes

.. [#] If, that is, we ignore the effects of relativity.
Expand Down
5 changes: 5 additions & 0 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def _wrap_strftime(object, format, timetuple):
zreplace = None # the string to use for %z
colonzreplace = None # the string to use for %:z
Zreplace = None # the string to use for %Z
sreplace = None # the string to use for %s

# Scan format for %z, %:z and %Z escapes, replacing as needed.
newformat = []
Expand Down Expand Up @@ -270,6 +271,10 @@ def _wrap_strftime(object, format, timetuple):
# strftime is going to have at this: escape %
Zreplace = s.replace('%', '%%')
newformat.append(Zreplace)
elif ch == 's' and hasattr(object, "timestamp"):
if sreplace is None:
sreplace = str(_math.floor(object.timestamp()))
newformat.append(sreplace)
# Note that datetime(1000, 1, 1).strftime('%G') == '1000' so
# year 1000 for %G can go on the fast path.
elif (ch in 'YGFC' and timetuple[0] < 1000 and
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -3214,6 +3214,31 @@ def test_strftime_special(self):
self.assertEqual(t.strftime('\0%c\0%B'), f'\0{s1}\0{s2}')
self.assertEqual(t.strftime('%c\0%B\0'), f'{s1}\0{s2}\0')

@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
def test_strftime_naive_s(self):
t = self.theclass(1970, 1, 1)
self.assertEqual(t.strftime('%s'), '18000')
t = self.theclass(1970, 1, 1, 1, 2, 3, 4)
self.assertEqual(t.strftime('%s'),
str(18000 + 3600 + 2*60 + 3))

def test_strftime_respect_timezone_s(self):
t = self.theclass(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
self.assertEqual(t.strftime('%s'), '0')

t = self.theclass(1970, 1, 1, 1, 2, 3, 600000, tzinfo=timezone.utc)
self.assertEqual(t.strftime('%s'),
str(3600 + 2*60 + 3))

t = self.theclass(1970, 1, 1, 1, 2, 3, 400000,
tzinfo=timezone(timedelta(hours=-5), 'EST'))
self.assertEqual(t.strftime('%s'),
str(18000 + 3600 + 2*60 + 3))

t = self.theclass(1969, 1, 1, 0, 0, 0, 700000,
tzinfo=timezone.utc)
self.assertEqual(t.strftime('%s'), '-31536000')

def test_extract(self):
dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
self.assertEqual(dt.date(), date(2002, 3, 4))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add cross-platform support for the ``%s`` strftime format code to the :mod:`datetime` module.
37 changes: 37 additions & 0 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,32 @@ make_freplacement(PyObject *object)
return PyUnicode_FromString(freplacement);
}

static PyObject *
make_sreplacement(PyObject *object)
{
PyObject *timestamp = PyObject_CallMethodNoArgs(object, &_Py_ID(timestamp));
if (timestamp == NULL) {
return NULL;
}

PyObject *math = PyImport_ImportModule("math");
if (math == NULL) {
Py_DECREF(timestamp);
return NULL;
}

PyObject *temp = PyObject_CallMethod(math, "floor", "O", timestamp);
Py_DECREF(math);
Py_DECREF(timestamp);
if (temp == NULL) {
return NULL;
}

PyObject *sreplacement = PyObject_Str(temp);
Py_DECREF(temp);
return sreplacement;
}

/* 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, %:z, %Z and %f format codes via a
Expand All @@ -1888,6 +1914,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */
PyObject *Zreplacement = NULL; /* py string, replacement for %Z */
PyObject *freplacement = NULL; /* py string, replacement for %f */
PyObject *sreplacement = NULL; /* py string, replacement for %s */

assert(object && format && timetuple);
assert(PyUnicode_Check(format));
Expand Down Expand Up @@ -1964,6 +1991,15 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
}
replacement = freplacement;
}
else if (ch == 's' && PyDateTime_Check(object)) {
/* format timestamp */
if (sreplacement == NULL) {
sreplacement = make_sreplacement(object);
if (sreplacement == NULL)
goto Error;
}
replacement = sreplacement;
}
else if (normalize_century()
&& (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C'))
{
Expand Down Expand Up @@ -2055,6 +2091,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
Py_XDECREF(zreplacement);
Py_XDECREF(colonzreplacement);
Py_XDECREF(Zreplacement);
Py_XDECREF(sreplacement);
Py_XDECREF(strftime);
return result;

Expand Down
Loading