]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-67795: Accept any real numbers as timestamp and timeout (GH-139224)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 23 Sep 2025 18:31:42 +0000 (21:31 +0300)
committerGitHub <noreply@github.com>
Tue, 23 Sep 2025 18:31:42 +0000 (21:31 +0300)
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.

20 files changed:
Doc/library/datetime.rst
Doc/library/os.rst
Doc/library/select.rst
Doc/library/signal.rst
Doc/library/socket.rst
Doc/library/threading.rst
Doc/library/time.rst
Doc/whatsnew/3.15.rst
Lib/test/datetimetester.py
Lib/test/test_os.py
Lib/test/test_socket.py
Lib/test/test_time.py
Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst [new file with mode: 0644]
Modules/clinic/selectmodule.c.h
Modules/clinic/signalmodule.c.h
Modules/posixmodule.c
Modules/selectmodule.c
Modules/signalmodule.c
Modules/socketmodule.c
Python/pytime.c

index c0ae4d66b76a7b169e0d93325c4f15529839af5c..3892367ff06efd45dbba4836d38c82972127b7db 100644 (file)
@@ -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)
 
index dab960629b21d0c45516514e6d3dcdedce543d6b..8c81e1dcd070ab597c338263e400441485b59dcd 100644 (file)
@@ -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.
index d2094283d5473615952a68d97a614d8307030d08..5b14428574c0a7f308d917b699ee43f927711edd 100644 (file)
@@ -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:
 
index b0307d3dea117096833cb8a31861ff87947ed4e7..66f31b28da2a95027b3f5e77477ba78b77703fa2 100644 (file)
@@ -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:
 
index bc89a3228f0ed9e56c01e84d7832b5972bdedd86..134d0962db8503201c5ae4fe2b89c31c995f4501 100644 (file)
@@ -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)
index 9a0aeb7c1287eee09bd1571e80b4f5375234b18b..c1705939fb644d5a6ecc63d57b8bffc0f17b7eb6 100644 (file)
@@ -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
index b05c0a312dbe34d99cbda89557d63847ede1e534..3e6f5a97b49be90361e11b4fa8817071707ec38a 100644 (file)
@@ -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
 
index 295dc201ec0ae4bfeec26d2c6b1515cbdbfdca28..7b146621dddcfaa7aa3c23be273fc32b586324a4 100644 (file)
@@ -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
 ===========
index 43cea44bc3d6c044b8de0a23316ae13ac3a493b5..7df2720620626853095c848de3915a1c3d3592e9 100644 (file)
@@ -3,6 +3,7 @@ import bisect
 import contextlib
 import copy
 import decimal
+import fractions
 import io
 import itertools
 import os
@@ -2626,6 +2627,10 @@ class TestDateTime(TestDate):
         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 @@ class TestDateTime(TestDate):
         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 @@ class TestDateTime(TestDate):
             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)
index cd15aa10f16de8fe8e06ee1953cf0f4102125691..1180e27a7a53106db9bedff300efc45d38931d83 100644 (file)
@@ -948,6 +948,20 @@ class UtimeTests(unittest.TestCase):
         # 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 @@ class UtimeTests(unittest.TestCase):
             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.")
index 0b93c36c70b70546f1c40f8f836c30406d159237..e4e7fa20f8aab94b2df3f4db539e6d22c7f2220f 100644 (file)
@@ -8,7 +8,9 @@ from test.support.import_helper import ensure_lazy_imports
 import _thread as thread
 import array
 import contextlib
+import decimal
 import errno
+import fractions
 import gc
 import io
 import itertools
@@ -1310,10 +1312,20 @@ class GeneralModuleTests(unittest.TestCase):
             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)
index 5312faa50774ecfa71892fb306e98082e07d2b69..ebc25a589876a02b5350e3412102ea495ce2a75d 100644 (file)
@@ -2,6 +2,7 @@ from test import support
 from test.support import warnings_helper
 import decimal
 import enum
+import fractions
 import math
 import platform
 import sys
@@ -170,10 +171,12 @@ class TimeTestCase(unittest.TestCase):
         # 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 (file)
index 0000000..6c11c93
--- /dev/null
@@ -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.
index c0a5f678ad038d278b892664bf8014400bd1e28c..26ddc6ffba75bfdcbbaccd52e9164d0eeb38375b 100644 (file)
@@ -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 @@ exit:
 #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]*/
index b0cd9e2e561640a16e1328fb15854177ca0a01b4..9fd24d15bf25004e322b083efeecaced92e03c3c 100644 (file)
@@ -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 @@ exit:
 #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]*/
index 6da90dc95addce9043db665e79b068065cc7fe6a..b7a0110226590ea3947d42f011b874111265dab9 100644 (file)
@@ -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;
index 107e674907cf732ceee41ce95485e64583915459..19fe509ec5e32ae648636c7de76020ec8e6bfbeb 100644 (file)
@@ -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;
         }
 
index 3c79ef1429087a76c57620936c3dca7fa3969491..4d0e224ff757e7114f6696f560806ce51fb27e10 100644 (file)
@@ -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,
index 92e6be68192dcc801ef07177623e0e5d284b267d..ec8b53273bc0830004571b1b28e8648f5f8131cd 100644 (file)
@@ -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.");
 
index 67cf643726449096c54de3e451f8044f5e9c64fc..0206467364f8940ec83a1c53b281f8c3ea04150b 100644 (file)
@@ -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;
 }