Skip to content

Commit cd19652

Browse files
move_on_ and fail_ functions accepts shield kwarg (#3051)
* move_on_ and fail_ context managers accepts shield arg * make it a kwarg * news rst * news rst * better docstring and parametrize test * undo * black * new line * update news rst to issue number * no need to explicitly link to docs --------- Co-authored-by: EXPLOSION <[email protected]>
1 parent 7c08af7 commit cd19652

File tree

4 files changed

+64
-11
lines changed

4 files changed

+64
-11
lines changed

docs/source/reference-core.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,7 @@ attribute to :data:`True`:
449449
try:
450450
await conn.send_hello_msg()
451451
finally:
452-
with trio.move_on_after(CLEANUP_TIMEOUT) as cleanup_scope:
453-
cleanup_scope.shield = True
452+
with trio.move_on_after(CLEANUP_TIMEOUT, shield=True) as cleanup_scope:
454453
await conn.send_goodbye_msg()
455454
456455
So long as you're inside a scope with ``shield = True`` set, then

newsfragments/3052.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`trio.move_on_at`, `trio.move_on_after`, `trio.fail_at` and `trio.fail_after` now accept *shield* as a keyword argument. If specified, it provides an initial value for the `~trio.CancelScope.shield` attribute of the `trio.CancelScope` object created by the context manager.

src/trio/_tests/test_timeouts.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import time
2-
from typing import Awaitable, Callable, TypeVar
2+
from typing import Awaitable, Callable, Protocol, TypeVar
33

44
import outcome
55
import pytest
@@ -75,6 +75,49 @@ async def sleep_3() -> None:
7575
await check_takes_about(sleep_3, TARGET)
7676

7777

78+
class TimeoutScope(Protocol):
79+
def __call__(self, seconds: float, *, shield: bool) -> trio.CancelScope: ...
80+
81+
82+
@pytest.mark.parametrize("scope", [move_on_after, fail_after])
83+
async def test_context_shields_from_outer(scope: TimeoutScope) -> None:
84+
with _core.CancelScope() as outer, scope(TARGET, shield=True) as inner:
85+
outer.cancel()
86+
try:
87+
await trio.lowlevel.checkpoint()
88+
except trio.Cancelled:
89+
pytest.fail("shield didn't work")
90+
inner.shield = False
91+
with pytest.raises(trio.Cancelled):
92+
await trio.lowlevel.checkpoint()
93+
94+
95+
@slow
96+
async def test_move_on_after_moves_on_even_if_shielded() -> None:
97+
async def task() -> None:
98+
with _core.CancelScope() as outer, move_on_after(TARGET, shield=True):
99+
outer.cancel()
100+
# The outer scope is cancelled, but this task is protected by the
101+
# shield, so it manages to get to sleep until deadline is met
102+
await sleep_forever()
103+
104+
await check_takes_about(task, TARGET)
105+
106+
107+
@slow
108+
async def test_fail_after_fails_even_if_shielded() -> None:
109+
async def task() -> None:
110+
with pytest.raises(TooSlowError), _core.CancelScope() as outer, fail_after(
111+
TARGET, shield=True
112+
):
113+
outer.cancel()
114+
# The outer scope is cancelled, but this task is protected by the
115+
# shield, so it manages to get to sleep until deadline is met
116+
await sleep_forever()
117+
118+
await check_takes_about(task, TARGET)
119+
120+
78121
@slow
79122
async def test_fail() -> None:
80123
async def sleep_4() -> None:

src/trio/_timeouts.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,40 @@
77
import trio
88

99

10-
def move_on_at(deadline: float) -> trio.CancelScope:
10+
def move_on_at(deadline: float, *, shield: bool = False) -> trio.CancelScope:
1111
"""Use as a context manager to create a cancel scope with the given
1212
absolute deadline.
1313
1414
Args:
1515
deadline (float): The deadline.
16+
shield (bool): Initial value for the `~trio.CancelScope.shield` attribute
17+
of the newly created cancel scope.
1618
1719
Raises:
1820
ValueError: if deadline is NaN.
1921
2022
"""
2123
if math.isnan(deadline):
2224
raise ValueError("deadline must not be NaN")
23-
return trio.CancelScope(deadline=deadline)
25+
return trio.CancelScope(deadline=deadline, shield=shield)
2426

2527

26-
def move_on_after(seconds: float) -> trio.CancelScope:
28+
def move_on_after(seconds: float, *, shield: bool = False) -> trio.CancelScope:
2729
"""Use as a context manager to create a cancel scope whose deadline is
2830
set to now + *seconds*.
2931
3032
Args:
3133
seconds (float): The timeout.
34+
shield (bool): Initial value for the `~trio.CancelScope.shield` attribute
35+
of the newly created cancel scope.
3236
3337
Raises:
3438
ValueError: if timeout is less than zero or NaN.
3539
3640
"""
3741
if seconds < 0:
3842
raise ValueError("timeout must be non-negative")
39-
return move_on_at(trio.current_time() + seconds)
43+
return move_on_at(trio.current_time() + seconds, shield=shield)
4044

4145

4246
async def sleep_forever() -> None:
@@ -96,7 +100,7 @@ class TooSlowError(Exception):
96100

97101
# workaround for PyCharm not being able to infer return type from @contextmanager
98102
# see https://youtrack.jetbrains.com/issue/PY-36444/PyCharm-doesnt-infer-types-when-using-contextlib.contextmanager-decorator
99-
def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # type: ignore[misc]
103+
def fail_at(deadline: float, *, shield: bool = False) -> AbstractContextManager[trio.CancelScope]: # type: ignore[misc]
100104
"""Creates a cancel scope with the given deadline, and raises an error if it
101105
is actually cancelled.
102106
@@ -110,14 +114,16 @@ def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # typ
110114
111115
Args:
112116
deadline (float): The deadline.
117+
shield (bool): Initial value for the `~trio.CancelScope.shield` attribute
118+
of the newly created cancel scope.
113119
114120
Raises:
115121
TooSlowError: if a :exc:`Cancelled` exception is raised in this scope
116122
and caught by the context manager.
117123
ValueError: if deadline is NaN.
118124
119125
"""
120-
with move_on_at(deadline) as scope:
126+
with move_on_at(deadline, shield=shield) as scope:
121127
yield scope
122128
if scope.cancelled_caught:
123129
raise TooSlowError
@@ -127,7 +133,9 @@ def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # typ
127133
fail_at = contextmanager(fail_at)
128134

129135

130-
def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]:
136+
def fail_after(
137+
seconds: float, *, shield: bool = False
138+
) -> AbstractContextManager[trio.CancelScope]:
131139
"""Creates a cancel scope with the given timeout, and raises an error if
132140
it is actually cancelled.
133141
@@ -140,6 +148,8 @@ def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]:
140148
141149
Args:
142150
seconds (float): The timeout.
151+
shield (bool): Initial value for the `~trio.CancelScope.shield` attribute
152+
of the newly created cancel scope.
143153
144154
Raises:
145155
TooSlowError: if a :exc:`Cancelled` exception is raised in this scope
@@ -149,4 +159,4 @@ def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]:
149159
"""
150160
if seconds < 0:
151161
raise ValueError("timeout must be non-negative")
152-
return fail_at(trio.current_time() + seconds)
162+
return fail_at(trio.current_time() + seconds, shield=shield)

0 commit comments

Comments
 (0)