Skip to content

Commit ce14daa

Browse files
committed
Add a DuplicatedRulesError
This is raised if an equivalent rule is added to the matcher to an existing rule. This should help users of the router know when they've accidentally duplicated a match.
1 parent ac41a32 commit ce14daa

File tree

4 files changed

+41
-1
lines changed

4 files changed

+41
-1
lines changed

src/werkzeug/routing/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
from .rules import Rule
1818

1919

20+
class DuplicatedRulesError(Exception):
21+
def __init__(self, rules: list[Rule]) -> None:
22+
self.rules = rules
23+
24+
2025
class RoutingException(Exception):
2126
"""Special exceptions that require the application to redirect, notifying
2227
about missing urls, etc.

src/werkzeug/routing/matcher.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from dataclasses import field
77

88
from .converters import ValidationError
9+
from .exceptions import DuplicatedRulesError
910
from .exceptions import NoMatch
1011
from .exceptions import RequestAliasRedirect
1112
from .exceptions import RequestPath
@@ -50,6 +51,11 @@ def add(self, rule: Rule) -> None:
5051
new_state = State()
5152
state.dynamic.append((part, new_state))
5253
state = new_state
54+
55+
for existing in state.rules:
56+
if rule == existing:
57+
raise DuplicatedRulesError([rule, existing])
58+
5359
state.rules.append(rule)
5460

5561
def update(self) -> None:

src/werkzeug/routing/rules.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,11 @@ def build_compare_key(self) -> tuple[int, int, int]:
907907
return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
908908

909909
def __eq__(self, other: object) -> bool:
910-
return isinstance(other, type(self)) and self._trace == other._trace
910+
return (
911+
isinstance(other, type(self))
912+
and self._parts == other._parts
913+
and self.websocket == other.websocket
914+
)
911915

912916
__hash__ = None # type: ignore
913917

tests/test_routing.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from werkzeug.datastructures import MultiDict
1010
from werkzeug.exceptions import MethodNotAllowed
1111
from werkzeug.exceptions import NotFound
12+
from werkzeug.routing.exceptions import DuplicatedRulesError
1213
from werkzeug.test import create_environ
1314
from werkzeug.wrappers import Response
1415

@@ -246,6 +247,30 @@ def test_strict_slashes_leaves_dont_consume():
246247
assert adapter.match("/path5/", method="GET") == ("leaf", {})
247248

248249

250+
def test_duplicated_matches():
251+
r.Map(
252+
[
253+
r.Rule("/", endpoint="leaf"),
254+
r.Rule("/", endpoint="websocket", websocket=True),
255+
r.Rule("/", endpoint="subdomain", subdomain="abc"),
256+
]
257+
)
258+
with pytest.raises(DuplicatedRulesError):
259+
r.Map(
260+
[
261+
r.Rule("/", endpoint="leaf"),
262+
r.Rule("/", endpoint="branch"),
263+
]
264+
)
265+
with pytest.raises(DuplicatedRulesError):
266+
r.Map(
267+
[
268+
r.Rule("/<foo>", endpoint="leaf"),
269+
r.Rule("/<bar>", endpoint="branch"),
270+
]
271+
)
272+
273+
249274
def test_environ_defaults():
250275
environ = create_environ("/foo")
251276
assert environ["PATH_INFO"] == "/foo"

0 commit comments

Comments
 (0)