Skip to content

Commit c52d343

Browse files
lesnik512claude
andcommitted
feat(circuit-breaker): accept any Collection[int] for failure_status_codes
Requiring frozenset[int] made the natural call site `failure_status_codes={500, 503}` (a set literal) a ty error. Accept Collection[int] and freeze internally — membership semantics unchanged, no friction pushed onto callers. Tests now pass a plain set and a list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 73a420f commit c52d343

3 files changed

Lines changed: 12 additions & 10 deletions

File tree

src/httpware/middleware/resilience/circuit_breaker.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import threading
2828
import time
2929
import typing
30-
from collections.abc import Callable
30+
from collections.abc import Callable, Collection
3131

3232
import httpx2
3333

@@ -73,7 +73,7 @@ def __init__(
7373
failure_threshold: int,
7474
reset_timeout: float,
7575
success_threshold: int,
76-
failure_status_codes: frozenset[int] | None,
76+
failure_status_codes: Collection[int] | None,
7777
now: Callable[[], float],
7878
) -> None:
7979
if failure_threshold < 1:
@@ -85,8 +85,10 @@ def __init__(
8585
self._failure_threshold = failure_threshold
8686
self._reset_timeout = reset_timeout
8787
self._success_threshold = success_threshold
88+
# Accept any Collection (set, frozenset, list, ...) and freeze it so callers
89+
# aren't forced to construct a frozenset just to satisfy the type checker.
8890
self._failure_status_codes = (
89-
failure_status_codes if failure_status_codes is not None else _DEFAULT_FAILURE_STATUS_CODES
91+
frozenset(failure_status_codes) if failure_status_codes is not None else _DEFAULT_FAILURE_STATUS_CODES
9092
)
9193
self._now = now
9294
self._state = _CircuitState.CLOSED
@@ -198,7 +200,7 @@ def __init__(
198200
failure_threshold: int = 5,
199201
reset_timeout: float = 30.0,
200202
success_threshold: int = 1,
201-
failure_status_codes: frozenset[int] | None = None,
203+
failure_status_codes: Collection[int] | None = None,
202204
_now: Callable[[], float] = time.monotonic,
203205
) -> None:
204206
self._state = _CircuitBreakerState(
@@ -262,7 +264,7 @@ def __init__(
262264
failure_threshold: int = 5,
263265
reset_timeout: float = 30.0,
264266
success_threshold: int = 1,
265-
failure_status_codes: frozenset[int] | None = None,
267+
failure_status_codes: Collection[int] | None = None,
266268
_now: Callable[[], float] = time.monotonic,
267269
) -> None:
268270
self._state = _CircuitBreakerState(

tests/test_circuit_breaker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ def _raise(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001
168168

169169

170170
async def test_custom_failure_status_codes_trips_on_member() -> None:
171-
"""A status code in a custom failure set trips the breaker."""
171+
"""A status code in a custom failure set trips the breaker (plain set accepted)."""
172172
handler = _StatusSequence([503, 503])
173173
breaker = AsyncCircuitBreaker(
174174
failure_threshold=2,
175-
failure_status_codes=frozenset({503}),
175+
failure_status_codes={503}, # a plain set — any Collection[int] is accepted
176176
_now=_Clock(),
177177
)
178178
async with _client(handler, breaker=breaker) as client:
@@ -189,7 +189,7 @@ async def test_custom_failure_status_codes_excludes_other_5xx() -> None:
189189
handler = _StatusSequence([500, 500, 500, 500])
190190
breaker = AsyncCircuitBreaker(
191191
failure_threshold=2,
192-
failure_status_codes=frozenset({503}),
192+
failure_status_codes=[503], # a list, too — frozen internally
193193
_now=_Clock(),
194194
)
195195
async with _client(handler, breaker=breaker) as client:

tests/test_circuit_breaker_sync.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def _raise(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001
140140

141141
def test_custom_failure_status_codes_trips_on_member() -> None:
142142
handler = _StatusSequence([503, 503])
143-
breaker = CircuitBreaker(failure_threshold=2, failure_status_codes=frozenset({503}), _now=_Clock())
143+
breaker = CircuitBreaker(failure_threshold=2, failure_status_codes={503}, _now=_Clock()) # plain set accepted
144144
with _client(handler, breaker=breaker) as client:
145145
for _ in range(2):
146146
with pytest.raises(ServiceUnavailableError):
@@ -152,7 +152,7 @@ def test_custom_failure_status_codes_trips_on_member() -> None:
152152

153153
def test_custom_failure_status_codes_excludes_other_5xx() -> None:
154154
handler = _StatusSequence([500, 500, 500, 500])
155-
breaker = CircuitBreaker(failure_threshold=2, failure_status_codes=frozenset({503}), _now=_Clock())
155+
breaker = CircuitBreaker(failure_threshold=2, failure_status_codes=[503], _now=_Clock()) # list accepted too
156156
with _client(handler, breaker=breaker) as client:
157157
for _ in range(4):
158158
with pytest.raises(InternalServerError):

0 commit comments

Comments
 (0)