Skip to content

Commit d7ecf40

Browse files
andystaplesCopilot
andcommitted
Preserve behavior while narrowing Ruff exceptions
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 2e49481 commit d7ecf40

5 files changed

Lines changed: 142 additions & 8 deletions

File tree

‎durabletask/internal/orchestration_entity_context.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ def is_inside_critical_section(self) -> bool:
2828

2929
def get_available_entities(self) -> Generator[EntityInstanceId, None, None]:
3030
if self.is_inside_critical_section:
31-
for available_lock in self.available_locks:
31+
# Do not forward send, throw, or close to the lock iterator.
32+
for available_lock in self.available_locks: # noqa: UP028
3233
yield available_lock
3334

3435
def validate_suborchestration_transition(self) -> tuple[bool, str]:

‎durabletask/scheduled/schedule_status.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from enum import Enum
55

66

7-
class ScheduleStatus(str, Enum):
7+
# StrEnum would change the public string and format representations.
8+
class ScheduleStatus(str, Enum): # noqa: UP042
89
"""Represents the current status of a schedule."""
910

1011
UNINITIALIZED = "Uninitialized"

‎pyproject.toml‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,3 @@ ignore = [
134134
# https://github.com/microsoft/durabletask-python/issues/323
135135
"UP017",
136136
]
137-
138-
[tool.ruff.lint.per-file-ignores]
139-
# These suggestions can change generator protocol or public enum behavior:
140-
# https://github.com/microsoft/durabletask-python/issues/324
141-
"durabletask/internal/orchestration_entity_context.py" = ["UP028"]
142-
"durabletask/scheduled/schedule_status.py" = ["UP042"]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
import json
5+
6+
import pytest
7+
8+
from durabletask.scheduled import ScheduleInvalidTransitionError, ScheduleStatus
9+
10+
11+
@pytest.mark.parametrize(("status", "name", "value", "ordinal"), [
12+
(ScheduleStatus.UNINITIALIZED, "UNINITIALIZED", "Uninitialized", 0),
13+
(ScheduleStatus.ACTIVE, "ACTIVE", "Active", 1),
14+
(ScheduleStatus.PAUSED, "PAUSED", "Paused", 2),
15+
])
16+
def test_status_preserves_string_formatting_and_conversions(
17+
status: ScheduleStatus, name: str, value: str, ordinal: int
18+
) -> None:
19+
formatted = f"ScheduleStatus.{name}"
20+
assert isinstance(status, str)
21+
assert status == value
22+
assert str(status) == formatted
23+
assert f"{status}" == formatted
24+
assert format(status, ">30") == formatted.rjust(30)
25+
assert json.dumps(status) == json.dumps(value)
26+
assert ScheduleStatus(value) is status
27+
assert status.to_dotnet_ordinal() == ordinal
28+
assert ScheduleStatus.from_dotnet(ordinal) is status
29+
assert ScheduleStatus.from_dotnet(value) is status
30+
31+
32+
def test_transition_error_preserves_status_formatting() -> None:
33+
error = ScheduleInvalidTransitionError(
34+
"schedule", ScheduleStatus.PAUSED, ScheduleStatus.PAUSED, "pause_schedule")
35+
36+
assert str(error) == (
37+
"Invalid state transition for schedule 'schedule': operation "
38+
"'pause_schedule' cannot transition from 'ScheduleStatus.PAUSED' "
39+
"to 'ScheduleStatus.PAUSED'."
40+
)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
from collections.abc import Generator
5+
from typing import cast
6+
7+
import pytest
8+
9+
from durabletask.entities import EntityInstanceId
10+
from durabletask.internal.orchestration_entity_context import OrchestrationEntityContext
11+
12+
13+
class _ProtocolAwareLocks(list[EntityInstanceId]):
14+
def __init__(self, locks: list[EntityInstanceId]) -> None:
15+
super().__init__(locks)
16+
self.received_exception = False
17+
self.closed = False
18+
self.iterator = self._iterate()
19+
20+
def _iterate(self) -> Generator[EntityInstanceId, None, None]:
21+
try:
22+
yield from super().__iter__()
23+
except ValueError:
24+
self.received_exception = True
25+
raise
26+
finally:
27+
self.closed = True
28+
29+
def __iter__(self) -> Generator[EntityInstanceId, None, None]:
30+
return self.iterator
31+
32+
33+
def _context_with_locks(locks: list[EntityInstanceId]) -> OrchestrationEntityContext:
34+
context = OrchestrationEntityContext("owner")
35+
context.critical_section_id = "section"
36+
context.available_locks = locks
37+
return context
38+
39+
40+
def test_available_entities_follow_critical_section_lock_order() -> None:
41+
first = EntityInstanceId("counter", "a")
42+
second = EntityInstanceId("counter", "b")
43+
context = OrchestrationEntityContext("owner")
44+
45+
assert list(context.get_available_entities()) == []
46+
context.emit_acquire_message("section", [second, first])
47+
assert list(context.get_available_entities()) == []
48+
context.complete_acquire("section")
49+
assert list(context.get_available_entities()) == [first, second]
50+
51+
assert context.validate_operation_transition(first, one_way=False) == (True, "")
52+
assert list(context.get_available_entities()) == [second]
53+
context.recover_lock_after_call(first)
54+
assert list(context.get_available_entities()) == [second, first]
55+
56+
list(context.emit_lock_release_messages())
57+
assert list(context.get_available_entities()) == []
58+
59+
60+
def test_available_entities_ignores_sent_values() -> None:
61+
locks = [EntityInstanceId("counter", key) for key in ("a", "b", "c")]
62+
context = _context_with_locks(locks)
63+
# The runtime accepts non-None sends even though the annotation requires None.
64+
available = cast(Generator[EntityInstanceId, object, None], context.get_available_entities())
65+
66+
assert next(available) == locks[0]
67+
assert available.send(None) == locks[1]
68+
assert available.send("ignored") == locks[2]
69+
with pytest.raises(StopIteration):
70+
next(available)
71+
72+
73+
def test_available_entities_does_not_forward_throw() -> None:
74+
first = EntityInstanceId("counter", "a")
75+
second = EntityInstanceId("counter", "b")
76+
locks = _ProtocolAwareLocks([first, second])
77+
available = _context_with_locks(locks).get_available_entities()
78+
79+
assert next(available) == first
80+
with pytest.raises(ValueError, match="outer generator"):
81+
available.throw(ValueError("outer generator"))
82+
assert not locks.received_exception
83+
assert not locks.closed
84+
assert next(locks.iterator) == second
85+
locks.iterator.close()
86+
87+
88+
def test_available_entities_does_not_forward_close() -> None:
89+
first = EntityInstanceId("counter", "a")
90+
second = EntityInstanceId("counter", "b")
91+
locks = _ProtocolAwareLocks([first, second])
92+
available = _context_with_locks(locks).get_available_entities()
93+
94+
assert next(available) == first
95+
available.close()
96+
assert not locks.closed
97+
assert next(locks.iterator) == second
98+
locks.iterator.close()

0 commit comments

Comments
 (0)