Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

time.sleep() should support objects with __float__ #79888

Open
jdemeyer opened this issue Jan 10, 2019 · 30 comments
Open

time.sleep() should support objects with __float__ #79888

jdemeyer opened this issue Jan 10, 2019 · 30 comments
Labels
3.10 only security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs)

Comments

@jdemeyer
Copy link
Contributor

BPO 35707
Nosy @ronaldoussoren, @ncoghlan, @abalkin, @vstinner, @serhiy-storchaka, @jdemeyer, @MojoVampire, @pganssle, @remilapeyre
PRs
  • bpo-26669: Reject float infinity in time functions #11507
  • bpo-26669: Reject float infinity in time functions #11507
  • gh-79888: support __index__ and __float__ in time functions #11636
  • gh-79888: support __index__ and __float__ in time functions #11636
  • gh-79888: support __index__ and __float__ in time functions #11636
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2019-01-10.15:58:39.782>
    labels = ['interpreter-core', '3.10']
    title = 'time.sleep() should support objects with __float__'
    updated_at = <Date 2020-05-23.00:14:27.187>
    user = 'https://github.com/jdemeyer'

    bugs.python.org fields:

    activity = <Date 2020-05-23.00:14:27.187>
    actor = 'cheryl.sabella'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Interpreter Core']
    creation = <Date 2019-01-10.15:58:39.782>
    creator = 'jdemeyer'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 35707
    keywords = ['patch', 'patch']
    message_count = 29.0
    messages = ['333391', '333395', '333396', '333397', '333401', '333404', '333416', '333547', '333550', '333554', '333555', '333961', '333963', '333968', '333969', '334120', '334121', '334122', '334125', '334128', '334129', '334131', '334136', '334480', '334486', '334509', '339123', '349004', '349560']
    nosy_count = 10.0
    nosy_names = ['ronaldoussoren', 'ncoghlan', 'belopolsky', 'vstinner', 'serhiy.storchaka', 'jdemeyer', 'josh.r', 'p-ganssle', 'remi.lapeyre', 'AVINASH MISHRA']
    pr_nums = ['11507', '11507', '11636', '11636', '11636']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = None
    url = 'https://bugs.python.org/issue35707'
    versions = ['Python 3.10']

    @jdemeyer
    Copy link
    Contributor Author

    This used to work correctly in Python 2:

    class Half(object):
        def __float__(self):
            return 0.5
    import time
    time.sleep(Half())

    With Python 3.6, one gets instead

    Traceback (most recent call last):
      File "test.py", line 6, in <module>
        time.sleep(Half())
    TypeError: an integer is required (got type Half)

    @jdemeyer jdemeyer added 3.7 (EOL) end of life 3.8 only security fixes labels Jan 10, 2019
    @AVINASHMISHRA
    Copy link
    Mannequin

    AVINASHMISHRA mannequin commented Jan 10, 2019

    hey i am a total newbie to open source contribution.
    can you help me understand this issue and can i help solve this issue?

    @remilapeyre
    Copy link
    Mannequin

    remilapeyre mannequin commented Jan 10, 2019

    time.sleep() is probably not the only function to have such a bug.

    Maybe __int__() should default to:

        def __int__(self):
            return int(self.__float__())

    when __float__ is defined and not __int__.

    Nick Coghlan suggested something similar for __int__ and __index__.

    @remilapeyre
    Copy link
    Mannequin

    remilapeyre mannequin commented Jan 10, 2019

    See bpo-33039 for the proposed change to __int__.

    @vstinner
    Copy link
    Member

    The problem comes from the private C function _PyTime_FromObject() of Python/pytime.c. This function must use the proper conversion to minimize the precision loss. Lib/test/test_time.py contains a lot of tests on conversions from different types and ensure that values are rounded correctly. See also my PEP-564 "Add new time functions with nanosecond resolution".

    The correct code works for float and int (and maybe decimal.Decimal, I don't recall!), but it seems like it doesn't support types with __float__(). You have to explicitly cast such objects using float(value).

    PyNumber_Float() can be used to convert arbitrary object to a float, but I'm not sure in which order the conversion should be tried to avoid/reduce precision loss during the conversion.

    Example:

    >>> x=2**53+1; x - int(float(x))
    1

    If we convert 'x' (int) to float, we introduce an error of 1.

    @vstinner
    Copy link
    Member

    PR 11507 is not directly related to this issue, see bpo-26669. But I wrote this PR when trying to fix this issue :-)

    @jdemeyer
    Copy link
    Contributor Author

    I'm not sure in which order the conversion should be tried to avoid/reduce precision loss during the conversion.

    I would suggest the order

    1. __index__ to ensure exact conversion of exact integers
    2. __float__ to ensure correct conversion of floating-point numbers
    3. __int__

    @ncoghlan
    Copy link
    Contributor

    Deriving __int__ from __float__ wouldn't be the right answer, as that can easily lead to unwanted OverflowError exceptions and other issues.

    However, Jeroen's suggested order of checking __index__ then __float__ then __int__ in _PyTime_FromObject makes sense to me, as that addresses Victor's desire to use the most precise conversion available.

    @serhiy-storchaka
    Copy link
    Member

    This can cause a loss of precision for Decimal.

    If we want to support other numerical types with loss in double rounding, the most reliable way is to represent them as fractions (x.as_integer_ratio() or (x.numerator, x.denominator)) and use precise integer arithmetic.

    @jdemeyer
    Copy link
    Contributor Author

    the most reliable way is to represent them as fractions (x.as_integer_ratio() or (x.numerator, x.denominator))

    I don't think that we can rely on non-dunder names like that. They are not reserved names, so classes can give them any semantics that they like. This is not just hypothetical: SageMath for example uses numerator() and denominator() methods, not properties.

    If you really want to go through with this, probably a special method like __as_integer_ratio__ should be defined.

    Anyway, I personally consider the double rounding for time.sleep() a non-issue. We are not trying to write a precise math library here, nobody will complain about sleeping a femtosecond too long.

    @jdemeyer
    Copy link
    Contributor Author

    The correct code works for float and int (and maybe decimal.Decimal, I don't recall!)

    Not for Decimal! In fact sleep(Decimal("0.99")) is interpreted as sleep(0) because __int__ is used to convert.

    @jdemeyer
    Copy link
    Contributor Author

    My proposal vastly improves the situation for Decimal. I will write a PR for this and I hope that it won't be rejected just because it's not perfect.

    @vstinner
    Copy link
    Member

    Not for Decimal! In fact sleep(Decimal("0.99")) is interpreted as sleep(0) because __int__ is used to convert.

    Oh oh. I didn't know that. It should be fixed.

    @jdemeyer
    Copy link
    Contributor Author

    I guess I should wait until PR 11507 is merged, to avoid merge conflicts.

    @vstinner
    Copy link
    Member

    No please don't wait for my PR 11507. I'm not sure that it's correct, and this bug is more important than NaN/inf :-)

    @jdemeyer
    Copy link
    Contributor Author

    To avoid code duplication, it's tempting to merge _PyTime_FromObject and _PyTime_ObjectToDenominator

    These two functions almost do the same, but not quite.

    @ronaldoussoren
    Copy link
    Contributor

    As a late response to msg333416 and msg333547, I don't agree with looking at __index__ in _PyTime_FromObject.

    The __index__ method is used when an object can be used as the index for a sequence, but should not silently convert to int or float. See <https://www.python.org/dev/peps/pep-0357/\>.

    @jdemeyer
    Copy link
    Contributor Author

    The motivation for PEP-357 was certainly using an object as the index for a sequence, but that's not the only use case.

    In fact PEP-357 states "For example, the slot can be used any time Python requires an integer internally"

    So despite the name __index__, I think that this is now the de facto standard for "convert the object (which is some kind of integer) to a Python int without loss of precision".

    @ronaldoussoren
    Copy link
    Contributor

    Using __index__ here doesn't feel right, although I can't explain why yet.

    @jdemeyer
    Copy link
    Contributor Author

    If __index__ doesn't "feel" right, what do you propose then to fix this issue, keeping in mind the concerns of https://bugs.python.org/issue35707#msg333401

    @jdemeyer
    Copy link
    Contributor Author

    In other words: if we can only use __float__ and __int__, how do we know which one to use?

    @jdemeyer
    Copy link
    Contributor Author

    If we want to support other numerical types with loss in double rounding

    Looking at the existing code, I can already see several double-rounding "bugs" in the code, so I wouldn't be too much concerned here...

    @ronaldoussoren
    Copy link
    Contributor

    In other words: if we can only use __float__ and __int__, how do we know which one to use?

    I guess __index__. I've read the definition of object.__index__ in the data model documentation, and using __index__ for this conversion is fine. I need to educate my sense for when it's right to use this method...

    Sorry about the noise.

    @jdemeyer jdemeyer added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Jan 25, 2019
    @MojoVampire
    Copy link
    Mannequin

    MojoVampire mannequin commented Jan 28, 2019

    You've got a reference leak in your __index__ based paths. PyNumber_Index is returning a new reference (either to the existing obj, or a new one, if the existing obj isn't already an int). You never release this reference. Simplest fix is to make intobj top level, initialized to NULL, and Py_XDECREF it along the convert_from_int code path (you can't DECREF it in the index specific path because it needs to survive past the goto, since it's replacing obj).

    I'm also mildly concerned by how duplicative the code becomes post-patch. If it's not a major performance hit (don't think it is; not even sure the API is even used anymore), perhaps just implement _PyTime_ObjectToTime_t as a wrapper for _PyTime_ObjectToDenominator (with a denominator of 2, so rounding simplifies to just 0 == round down, 1 == round up)?

    Example:

    int
    _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round)
    {
        long numerator;
        if (_PyTime_ObjectToDenominator(obj, sec, &numerator, 2, round) == 0) {
           if (numerator) {
               if (*sec == _Py_IntegralTypeMax(time_t)) {
                   error_time_t_overflow();
                   return -1;
               }
               ++*sec;
           }
           return 0;
        }
        return -1;
    }

    Sorry for not commenting on GitHub, but my work computer has a broken Firefox that GitHub no longer supports properly.

    @jdemeyer
    Copy link
    Contributor Author

    I'm also mildly concerned by how duplicative the code becomes post-patch.

    I know, that's why I added that comment on GitHub.

    perhaps just implement _PyTime_ObjectToTime_t as a wrapper for _PyTime_ObjectToDenominator

    Sure, but will that increase the chances of PR 11636 being accepted? Unless a core developer who is willing to merge that PR asks me that, I'd rather not add extra complications to that PR. (to be clear: I mean no offense, it's just that getting a CPython PR accepted is hard)

    @jdemeyer
    Copy link
    Contributor Author

    You've got a reference leak in your __index__ based paths.

    Thanks for pointing that out. I fixed that now.

    @jdemeyer
    Copy link
    Contributor Author

    Is anybody willing to review PR 11636?

    @jdemeyer
    Copy link
    Contributor Author

    jdemeyer commented Aug 4, 2019

    If we want to support other numerical types with loss in double rounding, the most reliable way is to represent them as fractions (x.as_integer_ratio() or (x.numerator, x.denominator))

    See https://discuss.python.org/t/pep-3141-ratio-instead-of-numerator-denominator/2037/24?u=jdemeyer for a proposal to define __ratio__ for this.

    @vstinner
    Copy link
    Member

    See also bpo-20861.

    @csabella csabella added 3.10 only security fixes and removed 3.7 (EOL) end of life 3.8 only security fixes labels May 23, 2020
    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @ald-extrahop
    Copy link

    If this issue is covering all the _PyTime_FromObject() uses, the change to PyLong_AsLongLong() in Python 3.10 (#80229) increased the severity. Many time functions that accept floats by design raise TypeError when passed (e.g.) a decimal.Decimal (they previously just printed a DeprecationWarning).

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.10 only security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs)
    Projects
    None yet
    Development

    No branches or pull requests

    7 participants