Skip to content

Commit 80482c3

Browse files
committed
fix(sorts): handle trivial inputs and unpickleable worker errors
1 parent e2d8ea8 commit 80482c3

2 files changed

Lines changed: 59 additions & 13 deletions

File tree

‎sorts/odd_even_transposition_parallel.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import multiprocessing as mp
1515
from multiprocessing.connection import Connection, wait
16+
from multiprocessing.reduction import ForkingPickler
1617
from typing import Any, Protocol
1718

1819
"""
@@ -66,7 +67,12 @@ def oe_process[T: Comparable](
6667
# after all swaps are performed, send the values back to main
6768
result_pipe[1].send((value, None))
6869
except Exception as error: # noqa: BLE001 -- propagate worker errors to the caller
69-
result_pipe[1].send((None, error))
70+
try:
71+
payload = ForkingPickler.dumps((None, error))
72+
except Exception: # noqa: BLE001 -- user exceptions can fail to pickle
73+
fallback = RuntimeError(f"{type(error).__name__}: {error}")
74+
payload = ForkingPickler.dumps((None, fallback))
75+
result_pipe[1].send_bytes(payload)
7076
finally:
7177
for pipe in (l_send, r_send, lr_cv, rr_cv, result_pipe):
7278
if pipe is not None:
@@ -83,6 +89,14 @@ def oe_process[T: Comparable](
8389

8490
def odd_even_transposition[T: Comparable](arr: list[T]) -> list[T]:
8591
"""
92+
Sort in place, propagating worker errors to the caller. Unpickleable
93+
exceptions become RuntimeError with the original type name and message.
94+
95+
>>> odd_even_transposition([])
96+
[]
97+
>>> values = [42]
98+
>>> odd_even_transposition(values) is values
99+
True
86100
>>> odd_even_transposition(list(range(10)[::-1])) == sorted(list(range(10)[::-1]))
87101
True
88102
>>> odd_even_transposition(["a", "x", "c"]) == sorted(["x", "a", "c"])
@@ -113,6 +127,9 @@ def odd_even_transposition[T: Comparable](arr: list[T]) -> list[T]:
113127
...
114128
TypeError: '<' not supported between instances of 'str' and 'int'
115129
"""
130+
if len(arr) < 2:
131+
return arr
132+
116133
# spawn method is considered safer than fork
117134
multiprocessing_context = mp.get_context("spawn")
118135

‎tests/test_sorts.py‎

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import signal
2323
from dataclasses import dataclass
2424
from typing import NamedTuple
25+
from unittest.mock import patch
2526

2627
import pytest
2728

@@ -252,14 +253,36 @@ def test_bitonic_sort_comparable_items() -> None:
252253
bitonic_sort([1, "two", 3, "four"], 0, 4, 1)
253254

254255

255-
def _check_parallel_odd_even_transposition(case: list[object], rejects: bool) -> None:
256+
@dataclass
257+
class UnpicklableComparison:
258+
value: int
259+
260+
def __lt__(self, _other: object, /) -> bool:
261+
class LocalComparisonError(TypeError):
262+
pass
263+
264+
raise LocalComparisonError("comparison failed")
265+
266+
267+
def _check_parallel_odd_even_transposition(
268+
case: list[object], error: type[Exception] | None
269+
) -> None:
256270
# Give this probe and its workers a process group that the test alone owns.
257271
os.setsid()
258272
collection = list(case)
259-
if rejects:
260-
with pytest.raises(TypeError):
273+
if error is not None:
274+
with pytest.raises(error) as caught:
261275
parallel_odd_even_transposition(collection)
276+
if isinstance(case[0], UnpicklableComparison):
277+
assert "LocalComparisonError: comparison failed" in str(caught.value)
262278
assert collection == case
279+
elif len(collection) < 2:
280+
with patch(
281+
"multiprocessing.process.BaseProcess.start",
282+
side_effect=AssertionError("trivial input must not start a worker"),
283+
):
284+
assert parallel_odd_even_transposition(collection) is collection
285+
assert all(item is original for item, original in zip(collection, case))
263286
else:
264287
assert parallel_odd_even_transposition(collection) is collection
265288
assert collection == sorted(case)
@@ -270,19 +293,25 @@ def _check_parallel_odd_even_transposition(case: list[object], rejects: bool) ->
270293
os.name != "posix", reason="timeout cleanup requires process groups"
271294
)
272295
@pytest.mark.parametrize(
273-
("case", "rejects"),
296+
("case", "error"),
274297
[
275-
(["c", "a", "b"], False),
276-
([2.5, -1, 0.0], False),
277-
([Person(cost=100.0), Person(cost=-100.0), Person(name="Al")], False),
278-
([Dog(weight=15.5), Dog(weight=15.1), Dog(name="Buddy")], False),
279-
([1, "a"], True),
280-
([3, 2, "a", 1], True),
298+
([], None),
299+
([1], None),
300+
([Person()], None),
301+
(["c", "a", "b"], None),
302+
([2.5, -1, 0.0], None),
303+
([Person(cost=100.0), Person(cost=-100.0), Person(name="Al")], None),
304+
([Dog(weight=15.5), Dog(weight=15.1), Dog(name="Buddy")], None),
305+
([1, "a"], TypeError),
306+
([3, 2, "a", 1], TypeError),
307+
([UnpicklableComparison(3), UnpicklableComparison(2)], RuntimeError),
281308
],
282309
)
283-
def test_parallel_odd_even_transposition(case: list[object], rejects: bool) -> None:
310+
def test_parallel_odd_even_transposition(
311+
case: list[object], error: type[Exception] | None
312+
) -> None:
284313
process = mp.get_context("spawn").Process(
285-
target=_check_parallel_odd_even_transposition, args=(case, rejects)
314+
target=_check_parallel_odd_even_transposition, args=(case, error)
286315
)
287316
process.start()
288317
try:

0 commit comments

Comments
 (0)