Skip to content

Conversation

@sarperavci
Copy link
Contributor

This change updates the Response.elapsed attribute to use a datetime.timedelta instead of a float. The standard requests library exposes .elapsed as a timedelta, so this change improves compatibility.

For example, in requests:

>>> import requests as r
>>> type(r.get('http://example.com').elapsed)
<class 'datetime.timedelta'>

After this change, curl_cffi.requests will match this behavior.

Changes:

  • Import timedelta where needed.
  • Initialize Response.elapsed as a timedelta object.
  • Convert TOTAL_TIME from seconds to timedelta(seconds=...) when parsing responses.

@lexiforest
Copy link
Owner

lexiforest commented Nov 10, 2025

Better compatibility with requests is good, but we should also keep compatible with previous versions of curl_cffi, consider changing this option to a tuple of int and timedelta. Thanks.

@sarperavci
Copy link
Contributor Author

Using a tuple could break backward compatibility since any return value that isn’t a float might cause issues. This wrapper class behaves like a float in numeric contexts and like a timedelta in time operations, preserving compatibility while supporting seamless arithmetic and comparisons. Would you be open to accepting this approach?

# pure python
import operator as _op
from typing import Optional
from datetime import timedelta

class TimedeltaFloat(timedelta):
    """
    A timedelta subclass that behaves like a float in numeric contexts.
    """

    def __new__(cls, td: Optional[timedelta] = None):
        if isinstance(td, timedelta):
            return super().__new__(cls, td.days, td.seconds, td.microseconds)
        return super().__new__(cls)
        
    def _f(self):
        return self.total_seconds()

    # numeric conversions
    __float__ = lambda self: self._f()
    __int__   = lambda self: int(self._f())

    # comparisons
    def __eq__(self, other):
        if isinstance(other, (int, float)):
            return self._f() == other
        if isinstance(other, timedelta):
            return self._f() == other.total_seconds()
        return NotImplemented

    def __lt__(self, other):
        if isinstance(other, (int, float)):
            return self._f() < other
        if isinstance(other, timedelta):
            return self._f() < other.total_seconds()
        return NotImplemented

    def __gt__(self, other):
        if isinstance(other, (int, float)):
            return self._f() > other
        if isinstance(other, timedelta):
            return self._f() > other.total_seconds()
        return NotImplemented

    def __le__(self, other):
        if isinstance(other, (int, float)):
            return self._f() <= other
        if isinstance(other, timedelta):
            return self._f() <= other.total_seconds()
        return NotImplemented

    def __ge__(self, other):
        if isinstance(other, (int, float)):
            return self._f() >= other
        if isinstance(other, timedelta):
            return self._f() >= other.total_seconds()
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, (int, float)):
            return self._f() != other
        if isinstance(other, timedelta):
            return self._f() != other.total_seconds()
        return NotImplemented

    # arithmetic with float returns float; with timedelta returns timedelta
    def _arith(self, other, op):
        if isinstance(other, (int, float)):
            return op(self._f(), other)
        if isinstance(other, timedelta):
            return TimedeltaFloat(op(timedelta(self.days, self.seconds, self.microseconds), other))
        return NotImplemented

    
    __add__      = lambda self, o: self._arith(o, _op.add)
    __radd__     = lambda self, o: self._arith(o, _op.add)
    __sub__      = lambda self, o: self._arith(o, _op.sub)
    __rsub__     = lambda self, o: self._arith(o, lambda x, y: y - x)
    __mul__      = lambda self, o: self._arith(o, _op.mul)
    __rmul__     = lambda self, o: self._arith(o, _op.mul)
    __truediv__  = lambda self, o: self._arith(o, _op.truediv)
    __rtruediv__ = lambda self, o: self._arith(o, lambda x, y: y / x)

    __neg__ = lambda self: -self._f()
    __pos__ = lambda self: +self._f()
    __abs__ = lambda self: abs(self._f())

    def __str__(self):
        return f"{self._f()}"

 if __name__ == "__main__":
    from datetime import datetime
    td = TimedeltaFloat(timedelta(seconds=5, microseconds=500000))
    print(td)                # 5.5s
    print(float(td))        # 5.5
    print(td + 2)          # 7.5
    print(2 + td)          # 7.5
    print(td * 2)          # 11.0
    print(td / 2)          # 2.75
    print(td > 3)          # True
    print(td > timedelta(seconds=1))  # True
    print(td + timedelta(seconds=2))  # TimedeltaFloat(7.5s)
    print(datetime.now() + td)  # current time + 5.5s
    

@lexiforest
Copy link
Owner

Oh, sorry. I was thinking that this is a parameter in requesting. Well, if it's an attribute of the response, I guess this minor breaking change is acceptable. Please try to fix the unit tests then.

@sarperavci
Copy link
Contributor Author

I have slightly modified the unit tests and they are now passing.

@lexiforest lexiforest merged commit b6c1ba7 into lexiforest:main Nov 11, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants