From b77edfd836609d3d7443d1ffe9cdcdbbb35f692d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 22 Sep 2025 11:30:48 +0300 Subject: [PATCH 1/3] gh-67795: Accept any real numbers as timestamp and timeout Functions that take timestamp or timeout arguments now accept any real numbers, not only integers or floats, although this does not improve precision. --- Doc/library/datetime.rst | 10 ++ Doc/library/os.rst | 8 +- Doc/library/select.rst | 26 +++- Doc/library/signal.rst | 14 ++- Doc/library/socket.rst | 10 +- Doc/library/threading.rst | 22 +++- Doc/library/time.rst | 20 +++- Doc/whatsnew/3.15.rst | 5 + Lib/test/datetimetester.py | 113 ++++++++++++++++++ Lib/test/test_time.py | 7 +- ...5-09-22-11-30-45.gh-issue-67795.fROoZt.rst | 3 + Modules/clinic/selectmodule.c.h | 8 +- Modules/clinic/signalmodule.c.h | 4 +- Modules/posixmodule.c | 2 +- Modules/selectmodule.c | 37 +++--- Modules/signalmodule.c | 4 +- Modules/socketmodule.c | 2 +- Python/pytime.c | 94 ++++++++------- 18 files changed, 295 insertions(+), 94 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst 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 `. .. 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..6895e0878fa26b 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, 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_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..2787e25e12abfc --- /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, 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; } From fa9847b1b2e2cb5ace40d792938fa8ab3c4ad97f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 22 Sep 2025 15:35:11 +0300 Subject: [PATCH 2/3] Add more tests. --- Lib/test/test_os.py | 32 ++++++++++++++++++++++++++++++++ Lib/test/test_socket.py | 18 +++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) 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) From fff31e6fa342c37851970d397985cd48e99b1a34 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 22 Sep 2025 15:48:44 +0300 Subject: [PATCH 3/3] Explicitly mention Decimal and Fraction. --- Doc/whatsnew/3.15.rst | 6 +++--- .../Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6895e0878fa26b..7b146621dddcfa 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -279,9 +279,9 @@ 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, not only integers or floats, although this does not - improve precision. +* 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`.) 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 index 2787e25e12abfc..6c11c93bc1170f 100644 --- 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 @@ -1,3 +1,3 @@ Functions that take timestamp or timeout arguments now accept any real -numbers, not only integers or floats, although this does not improve -precision. +numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`), +not only integers or floats, although this does not improve precision.