From ef54ef336e145802aa47272118d908a8c497932a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 3 Mar 2026 00:27:42 -0500 Subject: [PATCH 1/4] Disallow absolute deadlines for relative cancel scopes --- newsfragments/3403.deprecated.rst | 3 +++ src/trio/_core/_run.py | 20 ++++++-------------- src/trio/_tests/test_timeouts.py | 22 +++++++++------------- 3 files changed, 18 insertions(+), 27 deletions(-) create mode 100644 newsfragments/3403.deprecated.rst diff --git a/newsfragments/3403.deprecated.rst b/newsfragments/3403.deprecated.rst new file mode 100644 index 0000000000..300736582b --- /dev/null +++ b/newsfragments/3403.deprecated.rst @@ -0,0 +1,3 @@ +Trying to use absolute deadlines on a `trio.CancelScope` constructed +with a relative deadline now raises, after being deprecated since Trio +0.27.0. diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 2d649d2917..c731fe59eb 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -808,13 +808,9 @@ def deadline(self) -> float: """ if self._relative_deadline != inf: assert self._deadline == inf - warnings.warn( - DeprecationWarning( - "unentered relative cancel scope does not have an absolute deadline. Use `.relative_deadline`", - ), - stacklevel=2, + raise RuntimeError( + "Unentered relative cancel scope does not have an absolute deadline." ) - return current_time() + self._relative_deadline return self._deadline @deadline.setter @@ -823,13 +819,9 @@ def deadline(self, new_deadline: float) -> None: raise ValueError("deadline must not be NaN") if self._relative_deadline != inf: assert self._deadline == inf - warnings.warn( - DeprecationWarning( - "unentered relative cancel scope does not have an absolute deadline. Transforming into an absolute cancel scope. First set `.relative_deadline = math.inf` if you do want an absolute cancel scope.", - ), - stacklevel=2, + raise RuntimeError( + "Unentered relative cancel scope does not have an absolute deadline." ) - self._relative_deadline = inf with self._might_change_registered_deadline(): self._deadline = float(new_deadline) @@ -852,7 +844,7 @@ def relative_deadline(self) -> float: elif self._deadline != inf: assert self._relative_deadline == inf raise RuntimeError( - "unentered non-relative cancel scope does not have a relative deadline", + "Unentered non-relative cancel scope does not have a relative deadline", ) return self._relative_deadline @@ -868,7 +860,7 @@ def relative_deadline(self, new_relative_deadline: float) -> None: elif self._deadline != inf: assert self._relative_deadline == inf raise RuntimeError( - "unentered non-relative cancel scope does not have a relative deadline", + "Unentered non-relative cancel scope does not have a relative deadline", ) else: self._relative_deadline = new_relative_deadline diff --git a/src/trio/_tests/test_timeouts.py b/src/trio/_tests/test_timeouts.py index ff62a0708f..39c619f0de 100644 --- a/src/trio/_tests/test_timeouts.py +++ b/src/trio/_tests/test_timeouts.py @@ -246,26 +246,22 @@ async def test_timeout_deadline_on_entry(mock_clock: _core.MockClock) -> None: async def test_invalid_access_unentered(mock_clock: _core.MockClock) -> None: cs = move_on_after(5) mock_clock.jump(3) - start = _core.current_time() - match_str = "^unentered relative cancel scope does not have an absolute deadline" - with pytest.warns(DeprecationWarning, match=match_str): - assert cs.deadline == start + 5 - mock_clock.jump(1) - # this is hella sketchy, but they *have* been warned - with pytest.warns(DeprecationWarning, match=match_str): - assert cs.deadline == start + 6 + match_str = "^Unentered relative cancel scope does not have an absolute deadline" + with pytest.raises(RuntimeError, match=match_str): + print("SHOULD NOT PRINT! deadline:", cs.deadline) - with pytest.warns(DeprecationWarning, match=match_str): + with pytest.raises(RuntimeError, match=match_str): cs.deadline = 7 - # now transformed into absolute - assert cs.deadline == 7 - assert not cs.is_relative + + # nothing happened! + assert cs.relative_deadline == 5 + assert cs.is_relative cs = move_on_at(5) match_str = ( - "^unentered non-relative cancel scope does not have a relative deadline$" + "^Unentered non-relative cancel scope does not have a relative deadline$" ) with pytest.raises(RuntimeError, match=match_str): assert cs.relative_deadline From 980db2b79d41f6fae86d924a67521f110a15487d Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 12 Mar 2026 06:04:42 +0900 Subject: [PATCH 2/4] PR feedback --- newsfragments/3403.deprecated.rst | 3 +++ src/trio/_tests/test_timeouts.py | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/newsfragments/3403.deprecated.rst b/newsfragments/3403.deprecated.rst index 300736582b..d13abb1eaf 100644 --- a/newsfragments/3403.deprecated.rst +++ b/newsfragments/3403.deprecated.rst @@ -1,3 +1,6 @@ Trying to use absolute deadlines on a `trio.CancelScope` constructed with a relative deadline now raises, after being deprecated since Trio 0.27.0. + +To move over, replace the `trio.CancelScope` object instead of mutating +it. diff --git a/src/trio/_tests/test_timeouts.py b/src/trio/_tests/test_timeouts.py index 39c619f0de..13388022c3 100644 --- a/src/trio/_tests/test_timeouts.py +++ b/src/trio/_tests/test_timeouts.py @@ -243,13 +243,12 @@ async def test_timeout_deadline_on_entry(mock_clock: _core.MockClock) -> None: assert rcs is cs -async def test_invalid_access_unentered(mock_clock: _core.MockClock) -> None: +async def test_invalid_access_unentered() -> None: cs = move_on_after(5) - mock_clock.jump(3) match_str = "^Unentered relative cancel scope does not have an absolute deadline" with pytest.raises(RuntimeError, match=match_str): - print("SHOULD NOT PRINT! deadline:", cs.deadline) + assert cs.deadline with pytest.raises(RuntimeError, match=match_str): cs.deadline = 7 From 0e4d3d1b9598c2a9cb22448042f71999c27a4866 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 12 Mar 2026 06:06:49 +0900 Subject: [PATCH 3/4] Allow switching from relative to absolute --- newsfragments/3403.deprecated.rst | 4 ++-- src/trio/_core/_run.py | 15 ++++----------- src/trio/_tests/test_timeouts.py | 20 +++++++++----------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/newsfragments/3403.deprecated.rst b/newsfragments/3403.deprecated.rst index d13abb1eaf..4efb61bc77 100644 --- a/newsfragments/3403.deprecated.rst +++ b/newsfragments/3403.deprecated.rst @@ -2,5 +2,5 @@ Trying to use absolute deadlines on a `trio.CancelScope` constructed with a relative deadline now raises, after being deprecated since Trio 0.27.0. -To move over, replace the `trio.CancelScope` object instead of mutating -it. +You can still set absolute deadlines or relative deadlines to change +the kind of `trio.CancelScope` you have. diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index c731fe59eb..ffd839406b 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -817,12 +817,8 @@ def deadline(self) -> float: def deadline(self, new_deadline: float) -> None: if isnan(new_deadline): raise ValueError("deadline must not be NaN") - if self._relative_deadline != inf: - assert self._deadline == inf - raise RuntimeError( - "Unentered relative cancel scope does not have an absolute deadline." - ) with self._might_change_registered_deadline(): + self._relative_deadline = inf self._deadline = float(new_deadline) @property @@ -857,13 +853,10 @@ def relative_deadline(self, new_relative_deadline: float) -> None: if self._has_been_entered: with self._might_change_registered_deadline(): self._deadline = current_time() + float(new_relative_deadline) - elif self._deadline != inf: - assert self._relative_deadline == inf - raise RuntimeError( - "Unentered non-relative cancel scope does not have a relative deadline", - ) else: - self._relative_deadline = new_relative_deadline + with self._might_change_registered_deadline(): + self._deadline = inf + self._relative_deadline = new_relative_deadline @property def is_relative(self) -> bool | None: diff --git a/src/trio/_tests/test_timeouts.py b/src/trio/_tests/test_timeouts.py index 13388022c3..3ecbd1a0dd 100644 --- a/src/trio/_tests/test_timeouts.py +++ b/src/trio/_tests/test_timeouts.py @@ -243,29 +243,27 @@ async def test_timeout_deadline_on_entry(mock_clock: _core.MockClock) -> None: assert rcs is cs -async def test_invalid_access_unentered() -> None: +def test_invalid_access_unentered() -> None: cs = move_on_after(5) match_str = "^Unentered relative cancel scope does not have an absolute deadline" with pytest.raises(RuntimeError, match=match_str): assert cs.deadline - with pytest.raises(RuntimeError, match=match_str): - cs.deadline = 7 - - # nothing happened! - assert cs.relative_deadline == 5 - assert cs.is_relative - - cs = move_on_at(5) + # switch from relative to absolute + cs.deadline = 7 + assert cs.deadline == 7 + assert not cs.is_relative match_str = ( "^Unentered non-relative cancel scope does not have a relative deadline$" ) with pytest.raises(RuntimeError, match=match_str): assert cs.relative_deadline - with pytest.raises(RuntimeError, match=match_str): - cs.relative_deadline = 7 + + cs.relative_deadline = 9 + assert cs.relative_deadline == 9 + assert cs.is_relative @pytest.mark.xfail(reason="not implemented") From 0a66eab158d50e55d3cb61244176a47dbcf34763 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 12 Mar 2026 06:15:01 +0900 Subject: [PATCH 4/4] Make sure += follows accessing, rather than setting --- src/trio/_tests/test_timeouts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/trio/_tests/test_timeouts.py b/src/trio/_tests/test_timeouts.py index 3ecbd1a0dd..3eb5450021 100644 --- a/src/trio/_tests/test_timeouts.py +++ b/src/trio/_tests/test_timeouts.py @@ -249,6 +249,8 @@ def test_invalid_access_unentered() -> None: match_str = "^Unentered relative cancel scope does not have an absolute deadline" with pytest.raises(RuntimeError, match=match_str): assert cs.deadline + with pytest.raises(RuntimeError, match=match_str): + cs.deadline += 5 # switch from relative to absolute cs.deadline = 7 @@ -260,6 +262,8 @@ def test_invalid_access_unentered() -> None: ) with pytest.raises(RuntimeError, match=match_str): assert cs.relative_deadline + with pytest.raises(RuntimeError, match=match_str): + cs.relative_deadline += 5 cs.relative_deadline = 9 assert cs.relative_deadline == 9