https://github.com/python/cpython/commit/1a2e00c97acfe9f797228b836e2345f630d07b8e
commit: 1a2e00c97acfe9f797228b836e2345f630d07b8e
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2025-09-23T21:31:42+03:00
summary:
gh-67795: Accept any real numbers as timestamp and timeout (GH-139224)
Functions that take timestamp or timeout arguments now accept any
real numbers (such as Decimal and Fraction), not only integers or floats,
although this does not improve precision.
files:
A Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
M Doc/library/datetime.rst
M Doc/library/os.rst
M Doc/library/select.rst
M Doc/library/signal.rst
M Doc/library/socket.rst
M Doc/library/threading.rst
M Doc/library/time.rst
M Doc/whatsnew/3.15.rst
M Lib/test/datetimetester.py
M Lib/test/test_os.py
M Lib/test/test_socket.py
M Lib/test/test_time.py
M Modules/clinic/selectmodule.c.h
M Modules/clinic/signalmodule.c.h
M Modules/posixmodule.c
M Modules/selectmodule.c
M Modules/signalmodule.c
M Modules/socketmodule.c
M Python/pytime.c
diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index c0ae4d66b76a7b..3892367ff06efd 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -535,6 +535,9 @@ Other constructors, all class methods:
:c:func:`localtime` function. Raise :exc:`OSError` instead of
:exc:`ValueError` on :c:func:`localtime` failure.
+ .. versionchanged:: next
+ Accepts any real number as *timestamp*, not only integer or float.
+
.. classmethod:: date.fromordinal(ordinal)
@@ -1020,6 +1023,10 @@ Other constructors, all class methods:
.. versionchanged:: 3.6
:meth:`fromtimestamp` may return instances with :attr:`.fold` set to 1.
+ .. versionchanged:: next
+ Accepts any real number as *timestamp*, not only integer or float.
+
+
.. classmethod:: datetime.utcfromtimestamp(timestamp)
Return the UTC :class:`.datetime` corresponding to the POSIX timestamp, with
@@ -1060,6 +1067,9 @@ Other constructors, all class methods:
Use :meth:`datetime.fromtimestamp` with :const:`UTC` instead.
+ .. versionchanged:: next
+ Accepts any real number as *timestamp*, not only integer or float.
+
.. classmethod:: datetime.fromordinal(ordinal)
diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index dab960629b21d0..8c81e1dcd070ab 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -3618,7 +3618,8 @@ features:
where each member is an int expressing nanoseconds.
- If *times* is not ``None``,
it must be a 2-tuple of the form ``(atime, mtime)``
- where each member is an int or float expressing seconds.
+ where each member is a real number expressing seconds,
+ rounded down to nanoseconds.
- If *times* is ``None`` and *ns* is unspecified,
this is equivalent to specifying ``ns=(atime_ns, mtime_ns)``
where both times are the current time.
@@ -3645,6 +3646,9 @@ features:
.. versionchanged:: 3.6
Accepts a :term:`path-like object`.
+ .. versionchanged:: next
+ Accepts any real numbers as *times*, not only integers or floats.
+
.. function:: walk(top, topdown=True, onerror=None, followlinks=False)
@@ -4050,7 +4054,7 @@ Naturally, they are all only available on Linux.
the timer will fire when the timer's clock
(set by *clockid* in :func:`timerfd_create`) reaches *initial* seconds.
- The timer's interval is set by the *interval* :py:class:`float`.
+ The timer's interval is set by the *interval* real number.
If *interval* is zero, the timer only fires once, on the initial expiration.
If *interval* is greater than zero, the timer fires every time *interval*
seconds have elapsed since the previous expiration.
diff --git a/Doc/library/select.rst b/Doc/library/select.rst
index d2094283d54736..5b14428574c0a7 100644
--- a/Doc/library/select.rst
+++ b/Doc/library/select.rst
@@ -129,8 +129,9 @@ The module defines the following:
Empty iterables are allowed, but acceptance of three empty iterables is
platform-dependent. (It is known to work on Unix but not on Windows.) The
- optional *timeout* argument specifies a time-out as a floating-point number
- in seconds. When the *timeout* argument is omitted the function blocks
until
+ optional *timeout* argument specifies a time-out in seconds; it may be
+ a non-integer to specify fractions of seconds.
+ When the *timeout* argument is omitted the function blocks until
at least one file descriptor is ready. A time-out value of zero specifies a
poll and never blocks.
@@ -164,6 +165,9 @@ The module defines the following:
:pep:`475` for the rationale), instead of raising
:exc:`InterruptedError`.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. data:: PIPE_BUF
@@ -270,6 +274,9 @@ object.
:pep:`475` for the rationale), instead of raising
:exc:`InterruptedError`.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. _epoll-objects:
@@ -368,7 +375,9 @@ Edge and Level Trigger Polling (epoll) Objects
.. method:: epoll.poll(timeout=None, maxevents=-1)
- Wait for events. timeout in seconds (float)
+ Wait for events.
+ If *timeout* is given, it specifies the length of time in seconds
+ (may be non-integer) which the system will wait for events before returning.
.. versionchanged:: 3.5
The function is now retried with a recomputed timeout when interrupted by
@@ -376,6 +385,9 @@ Edge and Level Trigger Polling (epoll) Objects
:pep:`475` for the rationale), instead of raising
:exc:`InterruptedError`.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. _poll-objects:
@@ -464,6 +476,9 @@ linearly scanned again. :c:func:`!select` is *O*\ (*highest
file descriptor*), w
:pep:`475` for the rationale), instead of raising
:exc:`InterruptedError`.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. _kqueue-objects:
@@ -496,7 +511,7 @@ Kqueue Objects
- changelist must be an iterable of kevent objects or ``None``
- max_events must be 0 or a positive integer
- - timeout in seconds (floats possible); the default is ``None``,
+ - timeout in seconds (non-integers are possible); the default is ``None``,
to wait forever
.. versionchanged:: 3.5
@@ -505,6 +520,9 @@ Kqueue Objects
:pep:`475` for the rationale), instead of raising
:exc:`InterruptedError`.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. _kevent-objects:
diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst
index b0307d3dea1170..66f31b28da2a95 100644
--- a/Doc/library/signal.rst
+++ b/Doc/library/signal.rst
@@ -478,11 +478,11 @@ The :mod:`signal` module defines the following functions:
.. versionadded:: 3.3
-.. function:: setitimer(which, seconds, interval=0.0)
+.. function:: setitimer(which, seconds, interval=0)
Sets given interval timer (one of :const:`signal.ITIMER_REAL`,
:const:`signal.ITIMER_VIRTUAL` or :const:`signal.ITIMER_PROF`) specified
- by *which* to fire after *seconds* (float is accepted, different from
+ by *which* to fire after *seconds* (rounded up to microseconds, different
from
:func:`alarm`) and after that every *interval* seconds (if *interval*
is non-zero). The interval timer specified by *which* can be cleared by
setting *seconds* to zero.
@@ -493,13 +493,18 @@ The :mod:`signal` module defines the following functions:
:const:`signal.ITIMER_VIRTUAL` sends :const:`SIGVTALRM`,
and :const:`signal.ITIMER_PROF` will deliver :const:`SIGPROF`.
- The old values are returned as a tuple: (delay, interval).
+ The old values are returned as a two-tuple of floats:
+ (``delay``, ``interval``).
Attempting to pass an invalid interval timer will cause an
:exc:`ItimerError`.
.. availability:: Unix.
+ .. versionchanged:: next
+ Accepts any real numbers as *seconds* and *interval*, not only integers
+ or floats.
+
.. function:: getitimer(which)
@@ -676,6 +681,9 @@ The :mod:`signal` module defines the following functions:
by a signal not in *sigset* and the signal handler does not raise an
exception (see :pep:`475` for the rationale).
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. _signal-example:
diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst
index bc89a3228f0ed9..134d0962db8503 100644
--- a/Doc/library/socket.rst
+++ b/Doc/library/socket.rst
@@ -1407,11 +1407,14 @@ The :mod:`socket` module also offers various
network-related services:
.. function:: setdefaulttimeout(timeout)
- Set the default timeout in seconds (float) for new socket objects. When
+ Set the default timeout in seconds (real number) for new socket objects.
When
the socket module is first imported, the default is ``None``. See
:meth:`~socket.settimeout` for possible values and their respective
meanings.
+ .. versionchanged:: next
+ Accepts any real number, not only integer or float.
+
.. function:: sethostname(name)
@@ -2073,7 +2076,7 @@ to sockets.
.. method:: socket.settimeout(value)
Set a timeout on blocking socket operations. The *value* argument can be a
- nonnegative floating-point number expressing seconds, or ``None``.
+ nonnegative real number expressing seconds, or ``None``.
If a non-zero value is given, subsequent socket operations will raise a
:exc:`timeout` exception if the timeout period *value* has elapsed before
the operation has completed. If zero is given, the socket is put in
@@ -2085,6 +2088,9 @@ to sockets.
The method no longer toggles :const:`SOCK_NONBLOCK` flag on
:attr:`socket.type`.
+ .. versionchanged:: next
+ Accepts any real number, not only integer or float.
+
.. method:: socket.setsockopt(level, optname, value: int)
.. method:: socket.setsockopt(level, optname, value: buffer)
diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst
index 9a0aeb7c1287ee..c1705939fb644d 100644
--- a/Doc/library/threading.rst
+++ b/Doc/library/threading.rst
@@ -608,7 +608,7 @@ since it is impossible to detect the termination of alien
threads.
timeout occurs.
When the *timeout* argument is present and not ``None``, it should be a
- floating-point number specifying a timeout for the operation in seconds
+ real number specifying a timeout for the operation in seconds
(or fractions thereof). As :meth:`~Thread.join` always returns ``None``,
you must call :meth:`~Thread.is_alive` after :meth:`~Thread.join` to
decide whether a timeout happened -- if the thread is still alive, the
@@ -632,6 +632,9 @@ since it is impossible to detect the termination of alien
threads.
May raise :exc:`PythonFinalizationError`.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. attribute:: name
A string used for identification purposes only. It has no semantics.
@@ -764,7 +767,7 @@ All methods are executed atomically.
If a call with *blocking* set to ``True`` would block, return ``False``
immediately; otherwise, set the lock to locked and return ``True``.
- When invoked with the floating-point *timeout* argument set to a positive
+ When invoked with the *timeout* argument set to a positive
value, block for at most the number of seconds specified by *timeout*
and as long as the lock cannot be acquired. A *timeout* argument of
``-1``
specifies an unbounded wait. It is forbidden to specify a *timeout*
@@ -783,6 +786,9 @@ All methods are executed atomically.
.. versionchanged:: 3.14
Lock acquisition can now be interrupted by signals on Windows.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. method:: release()
@@ -863,7 +869,7 @@ call release as many times the lock has been acquired can
lead to deadlock.
* If no thread owns the lock, acquire the lock and return immediately.
* If another thread owns the lock, block until we are able to acquire
- lock, or *timeout*, if set to a positive float value.
+ lock, or *timeout*, if set to a positive value.
* If the same thread owns the lock, acquire the lock again, and
return immediately. This is the difference between :class:`Lock` and
@@ -890,6 +896,9 @@ call release as many times the lock has been acquired can
lead to deadlock.
.. versionchanged:: 3.2
The *timeout* parameter is new.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. method:: release()
@@ -1023,7 +1032,7 @@ item to the buffer only needs to wake up one consumer
thread.
occurs. Once awakened or timed out, it re-acquires the lock and returns.
When the *timeout* argument is present and not ``None``, it should be a
- floating-point number specifying a timeout for the operation in seconds
+ real number specifying a timeout for the operation in seconds
(or fractions thereof).
When the underlying lock is an :class:`RLock`, it is not released using
@@ -1150,6 +1159,9 @@ Semaphores also support the :ref:`context management
protocol <with-locks>`.
.. versionchanged:: 3.2
The *timeout* parameter is new.
+ .. versionchanged:: next
+ Accepts any real number as *timeout*, not only integer or float.
+
.. method:: release(n=1)
Release a semaphore, incrementing the internal counter by *n*. When it
@@ -1250,7 +1262,7 @@ method. The :meth:`~Event.wait` method blocks until the
flag is true.
the internal flag did not become true within the given wait time.
When the timeout argument is present and not ``None``, it should be a
- floating-point number specifying a timeout for the operation in seconds,
+ real number specifying a timeout for the operation in seconds,
or fractions thereof.
.. versionchanged:: 3.1
diff --git a/Doc/library/time.rst b/Doc/library/time.rst
index b05c0a312dbe34..3e6f5a97b49be9 100644
--- a/Doc/library/time.rst
+++ b/Doc/library/time.rst
@@ -189,7 +189,7 @@ Functions
.. versionadded:: 3.7
-.. function:: clock_settime(clk_id, time: float)
+.. function:: clock_settime(clk_id, time)
Set the time of the specified clock *clk_id*. Currently,
:data:`CLOCK_REALTIME` is the only accepted value for *clk_id*.
@@ -201,6 +201,9 @@ Functions
.. versionadded:: 3.3
+ .. versionchanged:: next
+ Accepts any real number as *time*, not only integer or float.
+
.. function:: clock_settime_ns(clk_id, time: int)
@@ -223,6 +226,9 @@ Functions
``asctime(localtime(secs))``. Locale information is not used by
:func:`ctime`.
+ .. versionchanged:: next
+ Accepts any real number, not only integer or float.
+
.. function:: get_clock_info(name)
@@ -258,6 +264,9 @@ Functions
:class:`struct_time` object. See :func:`calendar.timegm` for the inverse of
this
function.
+ .. versionchanged:: next
+ Accepts any real number, not only integer or float.
+
.. function:: localtime([secs])
@@ -271,6 +280,9 @@ Functions
:c:func:`gmtime` failure. It's common for this to be restricted to years
between 1970 and 2038.
+ .. versionchanged:: next
+ Accepts any real number, not only integer or float.
+
.. function:: mktime(t)
@@ -382,8 +394,7 @@ Functions
.. function:: sleep(secs)
Suspend execution of the calling thread for the given number of seconds.
- The argument may be a floating-point number to indicate a more precise sleep
- time.
+ The argument may be a non-integer to indicate a more precise sleep time.
If the sleep is interrupted by a signal and no exception is raised by the
signal handler, the sleep is restarted with a recomputed timeout.
@@ -428,6 +439,9 @@ Functions
.. versionchanged:: 3.13
Raises an auditing event.
+ .. versionchanged:: next
+ Accepts any real number, not only integer or float.
+
.. index::
single: % (percent); datetime format
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 295dc201ec0ae4..7b146621dddcfa 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -279,6 +279,11 @@ Other language changes
and ends with a forward slash (``/``).
(Contributed by Serhiy Storchaka in :gh:`134716`.)
+* Functions that take timestamp or timeout arguments now accept any real
+ numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`),
+ not only integers or floats, although this does not improve precision.
+ (Contributed by Serhiy Storchaka in :gh:`67795`.)
+
New modules
===========
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 43cea44bc3d6c0..7df27206206268 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -3,6 +3,7 @@
import contextlib
import copy
import decimal
+import fractions
import io
import itertools
import os
@@ -2626,6 +2627,10 @@ def test_fromtimestamp(self):
expected = time.localtime(ts)
got = self.theclass.fromtimestamp(ts)
self.verify_field_equality(expected, got)
+ got = self.theclass.fromtimestamp(decimal.Decimal(ts))
+ self.verify_field_equality(expected, got)
+ got = self.theclass.fromtimestamp(fractions.Fraction(ts))
+ self.verify_field_equality(expected, got)
def test_fromtimestamp_keyword_arg(self):
import time
@@ -2641,6 +2646,12 @@ def test_utcfromtimestamp(self):
with self.assertWarns(DeprecationWarning):
got = self.theclass.utcfromtimestamp(ts)
self.verify_field_equality(expected, got)
+ with self.assertWarns(DeprecationWarning):
+ got = self.theclass.utcfromtimestamp(decimal.Decimal(ts))
+ self.verify_field_equality(expected, got)
+ with self.assertWarns(DeprecationWarning):
+ got = self.theclass.utcfromtimestamp(fractions.Fraction(ts))
+ self.verify_field_equality(expected, got)
# Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
# March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
@@ -2728,6 +2739,108 @@ def utcfromtimestamp(*args, **kwargs):
self.assertEqual(t.second, 0)
self.assertEqual(t.microsecond, 7812)
+ @support.run_with_tz('MSK-03') # Something east of Greenwich
+ def test_microsecond_rounding_decimal(self):
+ D = decimal.Decimal
+ def utcfromtimestamp(*args, **kwargs):
+ with self.assertWarns(DeprecationWarning):
+ return self.theclass.utcfromtimestamp(*args, **kwargs)
+
+ for fts in [self.theclass.fromtimestamp,
+ utcfromtimestamp]:
+ zero = fts(D(0))
+ self.assertEqual(zero.second, 0)
+ self.assertEqual(zero.microsecond, 0)
+ one = fts(D('0.000_001'))
+ try:
+ minus_one = fts(D('-0.000_001'))
+ except OSError:
+ # localtime(-1) and gmtime(-1) is not supported on Windows
+ pass
+ else:
+ self.assertEqual(minus_one.second, 59)
+ self.assertEqual(minus_one.microsecond, 999_999)
+
+ t = fts(D('-0.000_000_1'))
+ self.assertEqual(t, zero)
+ t = fts(D('-0.000_000_9'))
+ self.assertEqual(t, minus_one)
+ t = fts(D(-1)/2**7)
+ self.assertEqual(t.second, 59)
+ self.assertEqual(t.microsecond, 992188)
+
+ t = fts(D('0.000_000_1'))
+ self.assertEqual(t, zero)
+ t = fts(D('0.000_000_5'))
+ self.assertEqual(t, zero)
+ t = fts(D('0.000_000_500_000_000_000_000_1'))
+ self.assertEqual(t, one)
+ t = fts(D('0.000_000_9'))
+ self.assertEqual(t, one)
+ t = fts(D('0.999_999_499_999_999_9'))
+ self.assertEqual(t.second, 0)
+ self.assertEqual(t.microsecond, 999_999)
+ t = fts(D('0.999_999_5'))
+ self.assertEqual(t.second, 1)
+ self.assertEqual(t.microsecond, 0)
+ t = fts(D('0.999_999_9'))
+ self.assertEqual(t.second, 1)
+ self.assertEqual(t.microsecond, 0)
+ t = fts(D(1)/2**7)
+ self.assertEqual(t.second, 0)
+ self.assertEqual(t.microsecond, 7812)
+
+ @support.run_with_tz('MSK-03') # Something east of Greenwich
+ def test_microsecond_rounding_fraction(self):
+ F = fractions.Fraction
+ def utcfromtimestamp(*args, **kwargs):
+ with self.assertWarns(DeprecationWarning):
+ return self.theclass.utcfromtimestamp(*args, **kwargs)
+
+ for fts in [self.theclass.fromtimestamp,
+ utcfromtimestamp]:
+ zero = fts(F(0))
+ self.assertEqual(zero.second, 0)
+ self.assertEqual(zero.microsecond, 0)
+ one = fts(F(1, 1_000_000))
+ try:
+ minus_one = fts(F(-1, 1_000_000))
+ except OSError:
+ # localtime(-1) and gmtime(-1) is not supported on Windows
+ pass
+ else:
+ self.assertEqual(minus_one.second, 59)
+ self.assertEqual(minus_one.microsecond, 999_999)
+
+ t = fts(F(-1, 10_000_000))
+ self.assertEqual(t, zero)
+ t = fts(F(-9, 10_000_000))
+ self.assertEqual(t, minus_one)
+ t = fts(F(-1, 2**7))
+ self.assertEqual(t.second, 59)
+ self.assertEqual(t.microsecond, 992188)
+
+ t = fts(F(1, 10_000_000))
+ self.assertEqual(t, zero)
+ t = fts(F(5, 10_000_000))
+ self.assertEqual(t, zero)
+ t = fts(F(5_000_000_000, 9_999_999_999_999_999))
+ self.assertEqual(t, one)
+ t = fts(F(9, 10_000_000))
+ self.assertEqual(t, one)
+ t = fts(F(9_999_995_000_000_000, 10_000_000_000_000_001))
+ self.assertEqual(t.second, 0)
+ self.assertEqual(t.microsecond, 999_999)
+ t = fts(F(9_999_995, 10_000_000))
+ self.assertEqual(t.second, 1)
+ self.assertEqual(t.microsecond, 0)
+ t = fts(F(9_999_999, 10_000_000))
+ self.assertEqual(t.second, 1)
+ self.assertEqual(t.microsecond, 0)
+ t = fts(F(1, 2**7))
+ self.assertEqual(t.second, 0)
+ self.assertEqual(t.microsecond, 7812)
+
def test_timestamp_limits(self):
with self.subTest("minimum UTC"):
min_dt = self.theclass.min.replace(tzinfo=timezone.utc)
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index cd15aa10f16de8..1180e27a7a5310 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -948,6 +948,20 @@ def ns_to_sec(ns):
# issue, os.utime() rounds towards minus infinity.
return (ns * 1e-9) + 0.5e-9
+ @staticmethod
+ def ns_to_sec_decimal(ns):
+ # Convert a number of nanosecond (int) to a number of seconds
(Decimal).
+ # Round towards infinity by adding 0.5 nanosecond to avoid rounding
+ # issue, os.utime() rounds towards minus infinity.
+ return decimal.Decimal('1e-9') * ns + decimal.Decimal('0.5e-9')
+
+ @staticmethod
+ def ns_to_sec_fraction(ns):
+ # Convert a number of nanosecond (int) to a number of seconds
(Fraction).
+ # Round towards infinity by adding 0.5 nanosecond to avoid rounding
+ # issue, os.utime() rounds towards minus infinity.
+ return fractions.Fraction(ns, 10**9) + fractions.Fraction(1, 2*10**9)
+
def test_utime_by_indexed(self):
# pass times as floating-point seconds as the second indexed parameter
def set_time(filename, ns):
@@ -968,6 +982,24 @@ def set_time(filename, ns):
os.utime(filename, times=(atime, mtime))
self._test_utime(set_time)
+ def test_utime_decimal(self):
+ # pass times as Decimal seconds
+ def set_time(filename, ns):
+ atime_ns, mtime_ns = ns
+ atime = self.ns_to_sec_decimal(atime_ns)
+ mtime = self.ns_to_sec_decimal(mtime_ns)
+ os.utime(filename, (atime, mtime))
+ self._test_utime(set_time)
+
+ def test_utime_fraction(self):
+ # pass times as Fraction seconds
+ def set_time(filename, ns):
+ atime_ns, mtime_ns = ns
+ atime = self.ns_to_sec_fraction(atime_ns)
+ mtime = self.ns_to_sec_fraction(mtime_ns)
+ os.utime(filename, (atime, mtime))
+ self._test_utime(set_time)
+
@unittest.skipUnless(os.utime in os.supports_follow_symlinks,
"follow_symlinks support for utime required "
"for this test.")
diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py
index 0b93c36c70b705..e4e7fa20f8aab9 100644
--- a/Lib/test/test_socket.py
+++ b/Lib/test/test_socket.py
@@ -8,7 +8,9 @@
import _thread as thread
import array
import contextlib
+import decimal
import errno
+import fractions
import gc
import io
import itertools
@@ -1310,10 +1312,20 @@ def testDefaultTimeout(self):
self.assertEqual(s.gettimeout(), None)
# Set the default timeout to 10, and see if it propagates
- with socket_setdefaulttimeout(10):
- self.assertEqual(socket.getdefaulttimeout(), 10)
+ with socket_setdefaulttimeout(10.125):
+ self.assertEqual(socket.getdefaulttimeout(), 10.125)
with socket.socket() as sock:
- self.assertEqual(sock.gettimeout(), 10)
+ self.assertEqual(sock.gettimeout(), 10.125)
+
+ socket.setdefaulttimeout(decimal.Decimal('11.125'))
+ self.assertEqual(socket.getdefaulttimeout(), 11.125)
+ with socket.socket() as sock:
+ self.assertEqual(sock.gettimeout(), 11.125)
+
+ socket.setdefaulttimeout(fractions.Fraction(97, 8))
+ self.assertEqual(socket.getdefaulttimeout(), 12.125)
+ with socket.socket() as sock:
+ self.assertEqual(sock.gettimeout(), 12.125)
# Reset the default timeout to None, and see if it propagates
socket.setdefaulttimeout(None)
diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py
index 5312faa50774ec..ebc25a589876a0 100644
--- a/Lib/test/test_time.py
+++ b/Lib/test/test_time.py
@@ -2,6 +2,7 @@
from test.support import warnings_helper
import decimal
import enum
+import fractions
import math
import platform
import sys
@@ -170,10 +171,12 @@ def test_sleep_exceptions(self):
# Improved exception #81267
with self.assertRaises(TypeError) as errmsg:
time.sleep([])
- self.assertIn("integer or float", str(errmsg.exception))
+ self.assertIn("real number", str(errmsg.exception))
def test_sleep(self):
- for value in [-0.0, 0, 0.0, 1e-100, 1e-9, 1e-6, 1, 1.2]:
+ for value in [-0.0, 0, 0.0, 1e-100, 1e-9, 1e-6, 1, 1.2,
+ decimal.Decimal('0.02'),
+ fractions.Fraction(1, 50)]:
with self.subTest(value=value):
time.sleep(value)
diff --git
a/Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
b/Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
new file mode 100644
index 00000000000000..6c11c93bc1170f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
@@ -0,0 +1,3 @@
+Functions that take timestamp or timeout arguments now accept any real
+numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`),
+not only integers or floats, although this does not improve precision.
diff --git a/Modules/clinic/selectmodule.c.h b/Modules/clinic/selectmodule.c.h
index c0a5f678ad038d..26ddc6ffba75bf 100644
--- a/Modules/clinic/selectmodule.c.h
+++ b/Modules/clinic/selectmodule.c.h
@@ -26,7 +26,7 @@ PyDoc_STRVAR(select_select__doc__,
"gotten from a fileno() method call on one of those.\n"
"\n"
"The optional 4th argument specifies a timeout in seconds; it may be\n"
-"a floating-point number to specify fractions of seconds. If it is absent\n"
+"a non-integer to specify fractions of seconds. If it is absent\n"
"or None, the call will never time out.\n"
"\n"
"The return value is a tuple of three lists corresponding to the first three\n"
@@ -973,7 +973,7 @@ PyDoc_STRVAR(select_epoll_poll__doc__,
"Wait for events on the epoll file descriptor.\n"
"\n"
" timeout\n"
-" the maximum time to wait in seconds (as float);\n"
+" the maximum time to wait in seconds (with fractions);\n"
" a timeout of None or -1 makes poll wait indefinitely\n"
" maxevents\n"
" the maximum number of events returned; -1 means no limit\n"
@@ -1262,7 +1262,7 @@ PyDoc_STRVAR(select_kqueue_control__doc__,
" The maximum number of events that the kernel will return.\n"
" timeout\n"
" The maximum time to wait in seconds, or else None to wait forever.\n"
-" This accepts floats for smaller timeouts, too.");
+" This accepts non-integers for smaller timeouts, too.");
#define SELECT_KQUEUE_CONTROL_METHODDEF \
{"control", _PyCFunction_CAST(select_kqueue_control), METH_FASTCALL,
select_kqueue_control__doc__},
@@ -1399,4 +1399,4 @@ select_kqueue_control(PyObject *self, PyObject *const
*args, Py_ssize_t nargs)
#ifndef SELECT_KQUEUE_CONTROL_METHODDEF
#define SELECT_KQUEUE_CONTROL_METHODDEF
#endif /* !defined(SELECT_KQUEUE_CONTROL_METHODDEF) */
-/*[clinic end generated code: output=2a66dd831f22c696 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=ae54d65938513132 input=a9049054013a1b77]*/
diff --git a/Modules/clinic/signalmodule.c.h b/Modules/clinic/signalmodule.c.h
index b0cd9e2e561640..9fd24d15bf2500 100644
--- a/Modules/clinic/signalmodule.c.h
+++ b/Modules/clinic/signalmodule.c.h
@@ -600,7 +600,7 @@ PyDoc_STRVAR(signal_sigtimedwait__doc__,
"\n"
"Like sigwaitinfo(), but with a timeout.\n"
"\n"
-"The timeout is specified in seconds, with floating-point numbers allowed.");
+"The timeout is specified in seconds, rounded up to nanoseconds.");
#define SIGNAL_SIGTIMEDWAIT_METHODDEF \
{"sigtimedwait", _PyCFunction_CAST(signal_sigtimedwait), METH_FASTCALL,
signal_sigtimedwait__doc__},
@@ -794,4 +794,4 @@ signal_pidfd_send_signal(PyObject *module, PyObject *const
*args, Py_ssize_t nar
#ifndef SIGNAL_PIDFD_SEND_SIGNAL_METHODDEF
#define SIGNAL_PIDFD_SEND_SIGNAL_METHODDEF
#endif /* !defined(SIGNAL_PIDFD_SEND_SIGNAL_METHODDEF) */
-/*[clinic end generated code: output=37ae8ebeae4178fa input=a9049054013a1b77]*/
+/*[clinic end generated code: output=42e20d118435d7fa input=a9049054013a1b77]*/
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 6da90dc95addce..b7a0110226590e 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -6678,7 +6678,7 @@ os_utime_impl(PyObject *module, path_t *path, PyObject
*times, PyObject *ns,
if (!PyTuple_CheckExact(times) || (PyTuple_Size(times) != 2)) {
PyErr_SetString(PyExc_TypeError,
"utime: 'times' must be either"
- " a tuple of two ints or None");
+ " a tuple of two numbers or None");
return NULL;
}
utime.now = 0;
diff --git a/Modules/selectmodule.c b/Modules/selectmodule.c
index 107e674907cf73..19fe509ec5e32a 100644
--- a/Modules/selectmodule.c
+++ b/Modules/selectmodule.c
@@ -262,7 +262,7 @@ A file descriptor is either a socket or file object, or a
small integer
gotten from a fileno() method call on one of those.
The optional 4th argument specifies a timeout in seconds; it may be
-a floating-point number to specify fractions of seconds. If it is absent
+a non-integer to specify fractions of seconds. If it is absent
or None, the call will never time out.
The return value is a tuple of three lists corresponding to the first three
@@ -277,7 +277,7 @@ descriptors can be used.
static PyObject *
select_select_impl(PyObject *module, PyObject *rlist, PyObject *wlist,
PyObject *xlist, PyObject *timeout_obj)
-/*[clinic end generated code: output=2b3cfa824f7ae4cf input=df20779a9c2f5c1e]*/
+/*[clinic end generated code: output=2b3cfa824f7ae4cf input=b0403de75cd11cc1]*/
{
#ifdef SELECT_USES_HEAP
pylist *rfd2obj, *wfd2obj, *efd2obj;
@@ -305,8 +305,9 @@ select_select_impl(PyObject *module, PyObject *rlist,
PyObject *wlist,
if (_PyTime_FromSecondsObject(&timeout, timeout_obj,
_PyTime_ROUND_TIMEOUT) < 0) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
- PyErr_SetString(PyExc_TypeError,
- "timeout must be a float or None");
+ PyErr_Format(PyExc_TypeError,
+ "timeout must be a real number or None, not %T",
+ timeout_obj);
}
return NULL;
}
@@ -632,8 +633,9 @@ select_poll_poll_impl(pollObject *self, PyObject
*timeout_obj)
if (_PyTime_FromMillisecondsObject(&timeout, timeout_obj,
_PyTime_ROUND_TIMEOUT) < 0) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
- PyErr_SetString(PyExc_TypeError,
- "timeout must be an integer or None");
+ PyErr_Format(PyExc_TypeError,
+ "timeout must be a real number or None, not %T",
+ timeout_obj);
}
return NULL;
}
@@ -974,8 +976,9 @@ select_devpoll_poll_impl(devpollObject *self, PyObject
*timeout_obj)
if (_PyTime_FromMillisecondsObject(&timeout, timeout_obj,
_PyTime_ROUND_TIMEOUT) < 0) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
- PyErr_SetString(PyExc_TypeError,
- "timeout must be an integer or None");
+ PyErr_Format(PyExc_TypeError,
+ "timeout must be a real number or None, not %T",
+ timeout_obj);
}
return NULL;
}
@@ -1565,7 +1568,7 @@ select_epoll_unregister_impl(pyEpoll_Object *self, int fd)
select.epoll.poll
timeout as timeout_obj: object = None
- the maximum time to wait in seconds (as float);
+ the maximum time to wait in seconds (with fractions);
a timeout of None or -1 makes poll wait indefinitely
maxevents: int = -1
the maximum number of events returned; -1 means no limit
@@ -1579,7 +1582,7 @@ as a list of (fd, events) 2-tuples.
static PyObject *
select_epoll_poll_impl(pyEpoll_Object *self, PyObject *timeout_obj,
int maxevents)
-/*[clinic end generated code: output=e02d121a20246c6c input=33d34a5ea430fd5b]*/
+/*[clinic end generated code: output=e02d121a20246c6c input=deafa7f04a60ebe0]*/
{
int nfds, i;
PyObject *elist = NULL, *etuple = NULL;
@@ -1595,8 +1598,9 @@ select_epoll_poll_impl(pyEpoll_Object *self, PyObject
*timeout_obj,
if (_PyTime_FromSecondsObject(&timeout, timeout_obj,
_PyTime_ROUND_TIMEOUT) < 0) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
- PyErr_SetString(PyExc_TypeError,
- "timeout must be an integer or None");
+ PyErr_Format(PyExc_TypeError,
+ "timeout must be a real number or None, not %T",
+ timeout_obj);
}
return NULL;
}
@@ -2291,7 +2295,7 @@ select.kqueue.control
The maximum number of events that the kernel will return.
timeout as otimeout: object = None
The maximum time to wait in seconds, or else None to wait forever.
- This accepts floats for smaller timeouts, too.
+ This accepts non-integers for smaller timeouts, too.
/
Calls the kernel kevent function.
@@ -2300,7 +2304,7 @@ Calls the kernel kevent function.
static PyObject *
select_kqueue_control_impl(kqueue_queue_Object *self, PyObject *changelist,
int maxevents, PyObject *otimeout)
-/*[clinic end generated code: output=81324ff5130db7ae input=59c4e30811209c47]*/
+/*[clinic end generated code: output=81324ff5130db7ae input=be969d2bc6f84205]*/
{
int gotevents = 0;
int nchanges = 0;
@@ -2331,9 +2335,8 @@ select_kqueue_control_impl(kqueue_queue_Object *self,
PyObject *changelist,
if (_PyTime_FromSecondsObject(&timeout,
otimeout, _PyTime_ROUND_TIMEOUT) < 0) {
PyErr_Format(PyExc_TypeError,
- "timeout argument must be a number "
- "or None, got %.200s",
- _PyType_Name(Py_TYPE(otimeout)));
+ "timeout must be a real number or None, not %T",
+ otimeout);
return NULL;
}
diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c
index 3c79ef1429087a..4d0e224ff757e7 100644
--- a/Modules/signalmodule.c
+++ b/Modules/signalmodule.c
@@ -1210,13 +1210,13 @@ signal.sigtimedwait
Like sigwaitinfo(), but with a timeout.
-The timeout is specified in seconds, with floating-point numbers allowed.
+The timeout is specified in seconds, rounded up to nanoseconds.
[clinic start generated code]*/
static PyObject *
signal_sigtimedwait_impl(PyObject *module, sigset_t sigset,
PyObject *timeout_obj)
-/*[clinic end generated code: output=59c8971e8ae18a64 input=955773219c1596cd]*/
+/*[clinic end generated code: output=59c8971e8ae18a64 input=f89af57d645e48e0]*/
{
PyTime_t timeout;
if (_PyTime_FromSecondsObject(&timeout,
diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c
index 92e6be68192dcc..ec8b53273bc083 100644
--- a/Modules/socketmodule.c
+++ b/Modules/socketmodule.c
@@ -7182,7 +7182,7 @@ socket_setdefaulttimeout(PyObject *self, PyObject *arg)
PyDoc_STRVAR(setdefaulttimeout_doc,
"setdefaulttimeout(timeout)\n\
\n\
-Set the default timeout in seconds (float) for new socket objects.\n\
+Set the default timeout in seconds (real number) for new socket objects.\n\
A value of None indicates that new socket objects have no timeout.\n\
When the socket module is first imported, the default is None.");
diff --git a/Python/pytime.c b/Python/pytime.c
index 67cf6437264490..0206467364f894 100644
--- a/Python/pytime.c
+++ b/Python/pytime.c
@@ -368,8 +368,20 @@ pytime_object_to_denominator(PyObject *obj, time_t *sec,
long *numerator,
{
assert(denominator >= 1);
- if (PyFloat_Check(obj)) {
+ if (PyIndex_Check(obj)) {
+ *sec = _PyLong_AsTime_t(obj);
+ *numerator = 0;
+ if (*sec == (time_t)-1 && PyErr_Occurred()) {
+ return -1;
+ }
+ return 0;
+ }
+ else {
double d = PyFloat_AsDouble(obj);
+ if (d == -1 && PyErr_Occurred()) {
+ *numerator = 0;
+ return -1;
+ }
if (isnan(d)) {
*numerator = 0;
PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a
number)");
@@ -378,30 +390,28 @@ pytime_object_to_denominator(PyObject *obj, time_t *sec,
long *numerator,
return pytime_double_to_denominator(d, sec, numerator,
denominator, round);
}
- else {
- *sec = _PyLong_AsTime_t(obj);
- *numerator = 0;
- if (*sec == (time_t)-1 && PyErr_Occurred()) {
- if (PyErr_ExceptionMatches(PyExc_TypeError)) {
- PyErr_Format(PyExc_TypeError,
- "argument must be int or float, not %T", obj);
- }
- return -1;
- }
- return 0;
- }
}
int
_PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round)
{
- if (PyFloat_Check(obj)) {
+ if (PyIndex_Check(obj)) {
+ *sec = _PyLong_AsTime_t(obj);
+ if (*sec == (time_t)-1 && PyErr_Occurred()) {
+ return -1;
+ }
+ return 0;
+ }
+ else {
double intpart;
/* volatile avoids optimization changing how numbers are rounded */
volatile double d;
d = PyFloat_AsDouble(obj);
+ if (d == -1 && PyErr_Occurred()) {
+ return -1;
+ }
if (isnan(d)) {
PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a
number)");
return -1;
@@ -418,13 +428,6 @@ _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec,
_PyTime_round_t round)
*sec = (time_t)intpart;
return 0;
}
- else {
- *sec = _PyLong_AsTime_t(obj);
- if (*sec == (time_t)-1 && PyErr_Occurred()) {
- return -1;
- }
- return 0;
- }
}
@@ -586,39 +589,38 @@ static int
pytime_from_object(PyTime_t *tp, PyObject *obj, _PyTime_round_t round,
long unit_to_ns)
{
- if (PyFloat_Check(obj)) {
+ if (PyIndex_Check(obj)) {
+ long long sec = PyLong_AsLongLong(obj);
+ if (sec == -1 && PyErr_Occurred()) {
+ if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
+ pytime_overflow();
+ }
+ return -1;
+ }
+
+ static_assert(sizeof(long long) <= sizeof(PyTime_t),
+ "PyTime_t is smaller than long long");
+ PyTime_t ns = (PyTime_t)sec;
+ if (pytime_mul(&ns, unit_to_ns) < 0) {
+ pytime_overflow();
+ return -1;
+ }
+
+ *tp = ns;
+ return 0;
+ }
+ else {
double d;
d = PyFloat_AsDouble(obj);
+ if (d == -1 && PyErr_Occurred()) {
+ return -1;
+ }
if (isnan(d)) {
PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a
number)");
return -1;
}
return pytime_from_double(tp, d, round, unit_to_ns);
}
-
- long long sec = PyLong_AsLongLong(obj);
- if (sec == -1 && PyErr_Occurred()) {
- if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
- pytime_overflow();
- }
- else if (PyErr_ExceptionMatches(PyExc_TypeError)) {
- PyErr_Format(PyExc_TypeError,
- "'%T' object cannot be interpreted as an integer or
float",
- obj);
- }
- return -1;
- }
-
- static_assert(sizeof(long long) <= sizeof(PyTime_t),
- "PyTime_t is smaller than long long");
- PyTime_t ns = (PyTime_t)sec;
- if (pytime_mul(&ns, unit_to_ns) < 0) {
- pytime_overflow();
- return -1;
- }
-
- *tp = ns;
- return 0;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]