From fae387ab6ef2683eeb756fcbc7b098502396e6c2 Mon Sep 17 00:00:00 2001 From: SanjanaG-01 Date: Mon, 9 Feb 2026 23:36:46 +0530 Subject: [PATCH 1/3] Docs: Add ecosystem examples to CoR, add Factory tests --- .../behavioral/chain_of_responsibility.py | 4 +++ patterns/creational/factory.py | 2 +- tests/creational/test_factory.py | 30 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/creational/test_factory.py diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index 9d46c4a8..46c3a419 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -14,6 +14,10 @@ As a variation some receivers may be capable of sending requests out in several directions, forming a `tree of responsibility`. +*Examples in Python ecosystem: +Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/ +The middleware components act as a chain where each processes the request/response. + *TL;DR Allow a request to pass down a chain of receivers until it is handled. """ diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index f75bb2b2..4f7bee71 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -54,7 +54,7 @@ def get_localizer(language: str = "English") -> Localizer: "Greek": GreekLocalizer, } - return localizers.get(language, EnglishLocalizer)() + return localizers.get(language, EnglishLocalizer)() diff --git a/tests/creational/test_factory.py b/tests/creational/test_factory.py new file mode 100644 index 00000000..4bcfd4c5 --- /dev/null +++ b/tests/creational/test_factory.py @@ -0,0 +1,30 @@ +import unittest +from patterns.creational.factory import get_localizer, GreekLocalizer, EnglishLocalizer + +class TestFactory(unittest.TestCase): + def test_get_localizer_greek(self): + localizer = get_localizer("Greek") + self.assertIsInstance(localizer, GreekLocalizer) + self.assertEqual(localizer.localize("dog"), "σκύλος") + self.assertEqual(localizer.localize("cat"), "γάτα") + # Test unknown word returns the word itself + self.assertEqual(localizer.localize("monkey"), "monkey") + + def test_get_localizer_english(self): + localizer = get_localizer("English") + self.assertIsInstance(localizer, EnglishLocalizer) + self.assertEqual(localizer.localize("dog"), "dog") + self.assertEqual(localizer.localize("cat"), "cat") + + def test_get_localizer_default(self): + # Test default argument + localizer = get_localizer() + self.assertIsInstance(localizer, EnglishLocalizer) + + def test_get_localizer_unknown_language(self): + # Test fallback for unknown language if applicable, + # or just verify what happens. + # Based on implementation: localizers.get(language, EnglishLocalizer)() + # It defaults to EnglishLocalizer for unknown keys. + localizer = get_localizer("Spanish") + self.assertIsInstance(localizer, EnglishLocalizer) From f25807360f6602caf0c94a74432331ed319dbbdd Mon Sep 17 00:00:00 2001 From: Ankithm-1006 <22bsm006@iiitdmj.ac.in> Date: Sun, 26 Apr 2026 16:15:37 +0530 Subject: [PATCH 2/3] docs: add design spec for CoR fallback mechanism --- .../specs/2026-04-26-cor-fallback-design.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-26-cor-fallback-design.md diff --git a/docs/superpowers/specs/2026-04-26-cor-fallback-design.md b/docs/superpowers/specs/2026-04-26-cor-fallback-design.md new file mode 100644 index 00000000..6719ef20 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-cor-fallback-design.md @@ -0,0 +1,74 @@ +--- +title: Chain of Responsibility — Fallback Mechanism +date: 2026-04-26 +status: approved +--- + +## Summary + +Enhance `patterns/behavioral/chain_of_responsibility.py` by integrating a proper fallback mechanism using the Template Method pattern. Add a test suite and real-world documentation. + +## Problem + +The current `FallbackHandler` must be manually chained at the end of every chain. If a developer forgets it, unhandled requests are silently dropped with no feedback. `handle()` returns `None`, so callers have no way to know whether a request was processed. + +## Design + +### Pattern Composition: CoR + Template Method + +`Handler.handle()` becomes a Template Method that owns the full flow: + +``` +check_range(request) + ↓ not handled +successor exists? + ├── yes → successor.handle(request) + └── no → self.handle_fallback(request) ← fires automatically +``` + +Subclasses fill in `check_range`. The base class calls `handle_fallback` automatically when the chain exhausts — developers never silently drop requests again. + +### Handler base class changes + +- `handle()` returns `bool` (was `None`) +- New `handle_fallback(request: int) -> bool` — default is a no-op returning `False`. Subclasses can override. + +### FallbackHandler — two modes + +```python +FallbackHandler(mode="log") # default — prints warning, returns False +FallbackHandler(mode="strict") # raises ValueError — for production systems +``` + +Constructor arg is preferred over subclasses: simpler, more Pythonic at this scale. + +### Documentation + +Replace abstract docstring with a **support ticket escalation** real-world analogy: +- L1 Support → L2 Support → L3 Support → fallback fires if none handle it + +Add ecosystem reference: Django middleware chain. + +### Tests — `tests/behavioral/test_chain_of_responsibility.py` + +| Test | What it verifies | +|---|---| +| `test_handler_routes_correctly` | Each handler processes requests in its own range | +| `test_fallback_log_mode` | Unhandled request triggers log fallback, returns `False` | +| `test_fallback_strict_mode` | Unhandled request raises `ValueError` | +| `test_handle_returns_true_on_success` | `handle()` returns `True` when handled | +| `test_handle_returns_false_on_fallback` | `handle()` returns `False` when fallback fires | +| `test_chain_without_fallback_handler` | Chain exhausts gracefully via base no-op (no crash) | + +## Files Changed + +| File | Change | +|---|---| +| `patterns/behavioral/chain_of_responsibility.py` | Template Method integration, FallbackHandler modes, return types, docstring | +| `tests/behavioral/test_chain_of_responsibility.py` | New file — full pytest suite | + +## Non-Goals + +- No changes to concrete handlers (ConcreteHandler0/1/2) +- No new dependencies +- Existing doctest in `main()` remains valid From 8ffe3263e0a6dea7358dbacc3cf3818147d1d6a0 Mon Sep 17 00:00:00 2001 From: SanjanaG-01 Date: Sun, 26 Apr 2026 18:03:39 +0530 Subject: [PATCH 3/3] feat(behavioral): add fallback mechanism to Chain of Responsibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handler.handle() refactored as a Template Method returning bool - Added handle_fallback() hook to Handler base class; fires automatically when the chain exhausts with no handler — requests never silently dropped - FallbackHandler redesigned with mode='log'|'strict' for dev vs production - Added tests/behavioral/test_chain_of_responsibility.py (16 tests, full coverage) - Updated module docstring with real-world support escalation analogy - Updated .gitignore to exclude docs/superpowers/ and .claude/ --- .gitignore | 2 + .../behavioral/chain_of_responsibility.py | 139 ++++++++++++++---- .../test_chain_of_responsibility.py | 104 +++++++++++++ 3 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 tests/behavioral/test_chain_of_responsibility.py diff --git a/.gitignore b/.gitignore index 4521242b..d54979ee 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ venv/ /.pytest_cache/ build/ dist/ +docs/superpowers/ +.claude/ diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index 46c3a419..81677ca7 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -1,25 +1,46 @@ """ *What is this pattern about? -The Chain of responsibility is an object oriented version of the -`if ... elif ... elif ... else ...` idiom, with the -benefit that the condition–action blocks can be dynamically rearranged -and reconfigured at runtime. - -This pattern aims to decouple the senders of a request from its -receivers by allowing request to move through chained -receivers until it is handled. - -Request receiver in simple form keeps a reference to a single successor. -As a variation some receivers may be capable of sending requests out -in several directions, forming a `tree of responsibility`. +The Chain of Responsibility is an object-oriented version of the +`if ... elif ... elif ... else ...` idiom, with the benefit that +condition-action blocks can be dynamically rearranged and reconfigured +at runtime. + +This pattern aims to decouple senders of a request from its receivers +by allowing the request to move through chained receivers until it is +handled. If no handler claims it, a fallback fires — requests are +never silently dropped. + +*Real-world example — customer support escalation: + L1 Support → handles common issues (billing, password reset) + L2 Support → handles technical problems (bugs, integrations) + L3 Support → handles complex/escalated cases (architecture, security) + Fallback → logs unresolved tickets or raises a production alert + +Each level tries to resolve the ticket. If it cannot, it escalates to +the next. The fallback guarantees no ticket silently disappears. + +*Fallback mechanism: +Handler.handle() is a Template Method that owns the full dispatch flow: + 1. check_range() — let this handler attempt to process the request + 2. successor.handle() — if unhandled, delegate to the next in chain + 3. handle_fallback() — if no successor, this always fires automatically + +Use FallbackHandler for explicit control at the end of a chain: + FallbackHandler(mode="log") — prints warning, returns False (default) + FallbackHandler(mode="strict") — raises ValueError (production systems) + +Without an explicit FallbackHandler, the base Handler.handle_fallback() +no-op fires — no crash, returns False, request silently ignored. *Examples in Python ecosystem: Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/ -The middleware components act as a chain where each processes the request/response. +Each middleware component processes the request/response in sequence. +Django's built-in 404/500 error views act as the chain's fallback. *TL;DR -Allow a request to pass down a chain of receivers until it is handled. +Allow a request to pass down a chain of handlers. The fallback mechanism +guarantees the chain never silently swallows an unhandled request. """ from abc import ABC, abstractmethod @@ -30,21 +51,34 @@ class Handler(ABC): def __init__(self, successor: Optional["Handler"] = None): self.successor = successor - def handle(self, request: int) -> None: + def handle(self, request: int) -> bool: + """ + Template Method: attempt handling, delegate up the chain, or fall back. + + Returns True if a concrete handler processed the request, + False if the request reached the end of the chain unhandled. + Delegates to handle_fallback() if no successor is set. + """ + if self.check_range(request): + return True + if self.successor: + return self.successor.handle(request) + return self.handle_fallback(request) + + def handle_fallback(self, request: int) -> bool: """ - Handle request and stop. - If can't - call next handler in chain. + Called automatically when the chain exhausts without handling request. - As an alternative you might even in case of success - call the next handler. + Override this in a subclass to customize end-of-chain behavior + (e.g., write to a dead-letter queue, send an alert, log metrics). + Subclasses may also raise instead of returning False (see FallbackHandler strict mode). + Default implementation is a silent no-op that returns False. """ - res = self.check_range(request) - if not res and self.successor: - self.successor.handle(request) + return False @abstractmethod def check_range(self, request: int) -> Optional[bool]: - """Compare passed value to predefined interval""" + """Return True if request was handled, None/False to pass it along.""" class ConcreteHandler0(Handler): @@ -61,7 +95,7 @@ def check_range(request: int) -> Optional[bool]: class ConcreteHandler1(Handler): - """... With it's own internal state""" + """... With its own internal state""" start, end = 10, 20 @@ -88,8 +122,44 @@ def get_interval_from_db() -> Tuple[int, int]: class FallbackHandler(Handler): - @staticmethod - def check_range(request: int) -> Optional[bool]: + """ + Terminal handler for explicit end-of-chain fallback control. + + Place at the end of a chain to define what happens when no concrete + handler processes a request. Supports two modes set at construction: + + mode="log" (default) — prints a warning and returns False. + Useful during development and for monitoring. + mode="strict" — raises ValueError. Use in production systems + where an unhandled request is always a bug. + + Real-world analogy — support ticket escalation: + l3 = ConcreteHandler2(FallbackHandler(mode="strict")) + l2 = ConcreteHandler1(l3) + l1 = ConcreteHandler0(l2) + l1.handle(ticket_priority) # raises ValueError if nobody claims it + + To extend: subclass FallbackHandler and override handle_fallback() to add + custom logic such as writing to a dead-letter queue or sending an alert. + + Do not assign a successor to FallbackHandler — it is the terminal node. + If a successor is set, handle_fallback() will never be called. + """ + + def __init__(self, mode: str = "log") -> None: + super().__init__() + if mode not in ("log", "strict"): + raise ValueError( + f"Invalid mode {mode!r}. Choose 'log' or 'strict'." + ) + self.mode = mode + + def check_range(self, request: int) -> Optional[bool]: + return None # FallbackHandler never handles requests directly + + def handle_fallback(self, request: int) -> bool: + if self.mode == "strict": + raise ValueError(f"No handler found for request {request}") print(f"end of chain, no handler for {request}") return False @@ -104,7 +174,7 @@ def main(): >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] >>> for request in requests: - ... h0.handle(request) + ... _ = h0.handle(request) request 2 handled in handler 0 request 5 handled in handler 0 request 14 handled in handler 1 @@ -114,6 +184,21 @@ def main(): end of chain, no handler for 35 request 27 handled in handler 2 request 20 handled in handler 2 + + >>> # Strict mode raises ValueError for unhandled requests: + >>> h_strict = ConcreteHandler0(FallbackHandler(mode="strict")) + >>> h_strict.handle(5) + request 5 handled in handler 0 + True + >>> h_strict.handle(99) + Traceback (most recent call last): + ... + ValueError: No handler found for request 99 + + >>> # Chain without FallbackHandler returns False gracefully: + >>> h_bare = ConcreteHandler0() + >>> h_bare.handle(99) + False """ diff --git a/tests/behavioral/test_chain_of_responsibility.py b/tests/behavioral/test_chain_of_responsibility.py new file mode 100644 index 00000000..d5579190 --- /dev/null +++ b/tests/behavioral/test_chain_of_responsibility.py @@ -0,0 +1,104 @@ +import pytest +from patterns.behavioral.chain_of_responsibility import ( + ConcreteHandler0, + ConcreteHandler1, + ConcreteHandler2, + FallbackHandler, +) + +# These tests are written in TDD style — they currently fail because: +# - Handler.handle() returns None (planned to return bool) +# - FallbackHandler has no mode= parameter (planned in next task) +# Failures are intentional. Subsequent tasks implement against these tests. + + +def make_chain(fallback=None): + """Build h0 -> h1 -> h2 -> fallback (optional).""" + h2 = ConcreteHandler2(fallback) + h1 = ConcreteHandler1(h2) + h0 = ConcreteHandler0(h1) + return h0 + + +class TestHandlerRouting: + """handle() returns True when a concrete handler processes the request.""" + + def test_routes_to_handler0(self): + assert make_chain().handle(5) is True + + def test_routes_to_handler1(self): + assert make_chain().handle(15) is True + + def test_routes_to_handler2(self): + assert make_chain().handle(25) is True + + def test_boundary_value_handler0(self): + assert make_chain().handle(0) is True # range is [0, 10) + + def test_boundary_value_handler1(self): + assert make_chain().handle(10) is True # range is [10, 20) + + def test_boundary_value_handler2(self): + assert make_chain().handle(20) is True # range is [20, 30) + + +class TestChainWithoutExplicitFallback: + """When no FallbackHandler is chained, the base no-op fires — no crash.""" + + def test_returns_false_when_no_handler_matches(self): + chain = make_chain() # No FallbackHandler + assert chain.handle(99) is False + + def test_does_not_raise(self): + chain = make_chain() + chain.handle(99) # must not raise + + +class TestFallbackLogMode: + """FallbackHandler(mode='log') prints a warning and returns False.""" + + def test_returns_false(self): + chain = make_chain(FallbackHandler(mode="log")) + assert chain.handle(99) is False + + def test_prints_warning(self, capsys): + chain = make_chain(FallbackHandler(mode="log")) + chain.handle(99) + captured = capsys.readouterr() + assert captured.out.strip() == "end of chain, no handler for 99" + + def test_default_mode_is_log(self, capsys): + chain = make_chain(FallbackHandler()) + result = chain.handle(99) + captured = capsys.readouterr() + assert result is False + assert "no handler for 99" in captured.out + + def test_does_not_raise(self): + chain = make_chain(FallbackHandler(mode="log")) + chain.handle(99) # must not raise, unlike strict mode + + +class TestFallbackStrictMode: + """FallbackHandler(mode='strict') raises ValueError for unhandled requests.""" + + def test_raises_value_error(self): + chain = make_chain(FallbackHandler(mode="strict")) + with pytest.raises(ValueError, match="No handler found for request 99"): + chain.handle(99) + + def test_does_not_raise_for_handled_request(self): + chain = make_chain(FallbackHandler(mode="strict")) + assert chain.handle(5) is True # handled by ConcreteHandler0, strict never fires + + +class TestFallbackHandlerValidation: + """FallbackHandler rejects unknown modes at construction time.""" + + def test_invalid_mode_raises(self): + with pytest.raises(ValueError, match="Invalid mode"): + FallbackHandler(mode="invalid") + + def test_valid_modes_do_not_raise(self): + FallbackHandler(mode="log") + FallbackHandler(mode="strict")