Skip to content

Commit 7a1ce5b

Browse files
committed
raise brokenresourceerror if registering an already exited task. fix docstring. fix runtimewarning transforming into triointernalerror. add a bunch of tests
1 parent c742a52 commit 7a1ce5b

File tree

3 files changed

+96
-21
lines changed

3 files changed

+96
-21
lines changed

src/trio/_core/_parking_lot.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
# See: https://github.com/python-trio/trio/issues/53
7272
from __future__ import annotations
7373

74+
import inspect
7475
import math
7576
import warnings
7677
from collections import OrderedDict
@@ -92,7 +93,15 @@
9293

9394

9495
def add_parking_lot_breaker(task: Task, lot: ParkingLot) -> None:
95-
"""Register a task as a breaker for a lot. See :meth:`ParkingLot.break_lot`"""
96+
"""Register a task as a breaker for a lot. See :meth:`ParkingLot.break_lot`.
97+
98+
raises:
99+
trio.BrokenResourceError: if the task has already exited.
100+
"""
101+
if inspect.getcoroutinestate(task.coro) == inspect.CORO_CLOSED:
102+
raise _core._exceptions.BrokenResourceError(
103+
"Attempted to add already exited task as lot breaker.",
104+
)
96105
if task not in GLOBAL_PARKING_LOT_BREAKER:
97106
GLOBAL_PARKING_LOT_BREAKER[task] = [lot]
98107
else:
@@ -275,8 +284,8 @@ def break_lot(self, task: Task | None = None) -> None:
275284
future tasks attempting to park to error. Unpark & repark become no-ops as the
276285
parking lot is empty.
277286
278-
The error raised contains a reference to the task sent as a parameter. It is also
279-
saved in the ``broken_by`` attribute.
287+
The error raised contains a reference to the task sent as a parameter. The task
288+
is also saved in the parking lot in the ``broken_by`` attribute.
280289
"""
281290
if task is None:
282291
task = _core.current_task()

src/trio/_core/_run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,8 @@ def unrolled_run(
28002800
),
28012801
stacklevel=1,
28022802
)
2803+
except RuntimeWarning:
2804+
raise
28032805
except TrioInternalError:
28042806
raise
28052807
except BaseException as exc:

src/trio/_core/_tests/test_parking_lot.py

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
import pytest
66

7-
import trio.lowlevel
7+
import trio
8+
from trio.lowlevel import (
9+
add_parking_lot_breaker,
10+
current_task,
11+
remove_parking_lot_breaker,
12+
)
813
from trio.testing import Matcher, RaisesGroup
914

1015
from ... import _core
@@ -220,27 +225,34 @@ async def test_parking_lot_repark_with_count() -> None:
220225
lot1.unpark_all()
221226

222227

228+
async def dummy_task(
229+
task_status: _core.TaskStatus[_core.Task] = trio.TASK_STATUS_IGNORED,
230+
) -> None:
231+
task_status.started(_core.current_task())
232+
await trio.sleep_forever()
233+
234+
223235
async def test_parking_lot_breaker_basic() -> None:
224236
lot = ParkingLot()
225-
task = trio.lowlevel.current_task()
237+
task = current_task()
226238

227239
with pytest.raises(
228240
RuntimeError,
229241
match="Attempted to remove task as breaker for a lot it is not registered for",
230242
):
231-
trio.lowlevel.remove_parking_lot_breaker(task, lot)
243+
remove_parking_lot_breaker(task, lot)
232244

233245
# check that a task can be registered as breaker for the same lot multiple times
234-
trio.lowlevel.add_parking_lot_breaker(task, lot)
235-
trio.lowlevel.add_parking_lot_breaker(task, lot)
236-
trio.lowlevel.remove_parking_lot_breaker(task, lot)
237-
trio.lowlevel.remove_parking_lot_breaker(task, lot)
246+
add_parking_lot_breaker(task, lot)
247+
add_parking_lot_breaker(task, lot)
248+
remove_parking_lot_breaker(task, lot)
249+
remove_parking_lot_breaker(task, lot)
238250

239251
with pytest.raises(
240252
RuntimeError,
241253
match="Attempted to remove task as breaker for a lot it is not registered for",
242254
):
243-
trio.lowlevel.remove_parking_lot_breaker(task, lot)
255+
remove_parking_lot_breaker(task, lot)
244256

245257
# defaults to current task
246258
lot.break_lot()
@@ -249,30 +261,81 @@ async def test_parking_lot_breaker_basic() -> None:
249261
# breaking the lot again with the same task is a no-op
250262
lot.break_lot()
251263

252-
# but with a different task it gives a warning
253-
async def dummy_task(
254-
task_status: _core.TaskStatus[_core.Task] = trio.TASK_STATUS_IGNORED,
255-
) -> None:
256-
task_status.started(_core.current_task())
264+
child_task = None
265+
with pytest.warns(RuntimeWarning):
266+
async with trio.open_nursery() as nursery:
267+
child_task = await nursery.start(dummy_task)
268+
# registering a task as breaker on an already broken lot is fine... though it
269+
# maybe shouldn't be as it will always cause a RuntimeWarning???
270+
# Or is this a sign that we shouldn't raise a warning?
271+
add_parking_lot_breaker(child_task, lot)
272+
nursery.cancel_scope.cancel()
273+
274+
# manually breaking a lot with an already exited task is fine
275+
lot = ParkingLot()
276+
lot.break_lot(child_task)
277+
278+
279+
async def test_parking_lot_breaker_warnings() -> None:
280+
lot = ParkingLot()
281+
task = current_task()
282+
lot.break_lot()
257283

284+
warn_str = "attempted to break parking .* already broken by .*"
285+
# breaking an already broken lot with a different task gives a warning
258286
# The nursery is only to create a task we can pass to lot.break_lot
259-
# and has no effect on the test otherwise.
260287
async with trio.open_nursery() as nursery:
261288
child_task = await nursery.start(dummy_task)
262289
with pytest.warns(
263290
RuntimeWarning,
264-
match="attempted to break parking .* already broken by .*",
291+
match=warn_str,
265292
):
266293
lot.break_lot(child_task)
267294
nursery.cancel_scope.cancel()
268295

296+
# note that this get put into an exceptiongroup if inside a nursery, making any
297+
# stacklevel arguments irrelevant
298+
with RaisesGroup(Matcher(RuntimeWarning, match=warn_str)):
299+
async with trio.open_nursery() as nursery:
300+
child_task = await nursery.start(dummy_task)
301+
lot.break_lot(child_task)
302+
nursery.cancel_scope.cancel()
303+
269304
# and doesn't change broken_by
270305
assert lot.broken_by == task
271306

307+
# register multiple tasks as lot breakers, then have them all exit
308+
lot = ParkingLot()
309+
child_task = None
310+
# This does not give an exception group, as the warning is raised by the nursery
311+
# exiting, and not any of the tasks inside the nursery.
312+
# And we only get a single warning because... of the default warning filter? and the
313+
# location being the same? (because I haven't figured out why stacklevel makes no
314+
# difference)
315+
with pytest.warns(
316+
RuntimeWarning,
317+
match=warn_str,
318+
):
319+
async with trio.open_nursery() as nursery:
320+
child_task = await nursery.start(dummy_task)
321+
child_task2 = await nursery.start(dummy_task)
322+
child_task3 = await nursery.start(dummy_task)
323+
add_parking_lot_breaker(child_task, lot)
324+
add_parking_lot_breaker(child_task2, lot)
325+
add_parking_lot_breaker(child_task3, lot)
326+
nursery.cancel_scope.cancel()
327+
328+
# trying to register an exited task as lot breaker errors
329+
with pytest.raises(
330+
trio.BrokenResourceError,
331+
match="^Attempted to add already exited task as lot breaker.$",
332+
):
333+
add_parking_lot_breaker(child_task, lot)
334+
272335

273336
async def test_parking_lot_breaker() -> None:
274337
async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None:
275-
trio.lowlevel.add_parking_lot_breaker(trio.lowlevel.current_task(), lot)
338+
add_parking_lot_breaker(current_task(), lot)
276339
with scope:
277340
await trio.sleep_forever()
278341

@@ -298,8 +361,9 @@ async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None:
298361

299362

300363
async def test_parking_lot_weird() -> None:
301-
"""break a parking lot, where the breakee is parked. Doing this is weird, but should probably be supported??
302-
Although the message makes less sense"""
364+
"""Break a parking lot, where the breakee is parked.
365+
Doing this is weird, but should probably be supported.
366+
"""
303367

304368
async def return_me_and_park(
305369
lot: ParkingLot,

0 commit comments

Comments
 (0)