Skip to content

Commit 2193620

Browse files
committed
fix Weighting calculation for path converters and add tests
Fix issue #2924 where Rules with dynamic elements would take priority over rules with static elements of the same length. The underlying issue was that the ordering for rules was backwards with regard to number of argument weights. In order to ensure that static elements match first the shortest rule must be tested first. To ensure that a RulePart that contains a path converter does not incorrectly take priority over other RuleParts that are part of other Rules that should have priority, RuleParts containing a path converter are assigned an infinite number of argument weights because they can consume an arbitrary number of url path elements when matching. With this change, two consecutive path converters give priority to the first converter. In general two consecutive path converters with different names cannot have consistent or predicatable behavior (where would you cut?). Tests are updated accordingly. Might consider making back to back path converters an error.
1 parent 7868bef commit 2193620

File tree

2 files changed

+48
-3
lines changed

2 files changed

+48
-3
lines changed

src/werkzeug/routing/rules.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
623623
self._trace.append((False, data["static"]))
624624
content += data["static"] if static else re.escape(data["static"])
625625

626+
haspath = False
626627
if data["variable"] is not None:
627628
if static:
628629
# Switching content to represent regex, hence the need to escape
@@ -640,6 +641,7 @@ def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
640641
convertor_number += 1
641642
argument_weights.append(convobj.weight)
642643
self._trace.append((True, data["variable"]))
644+
haspath = data["converter"] == "path"
643645

644646
if data["slash"] is not None:
645647
self._trace.append((False, "/"))
@@ -651,7 +653,7 @@ def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
651653
weight = Weighting(
652654
-len(static_weights),
653655
static_weights,
654-
-len(argument_weights),
656+
float("+inf") if haspath else len(argument_weights),
655657
argument_weights,
656658
)
657659
yield RulePart(
@@ -681,7 +683,7 @@ def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
681683
weight = Weighting(
682684
-len(static_weights),
683685
static_weights,
684-
-len(argument_weights),
686+
float("+inf") if haspath else len(argument_weights),
685687
argument_weights,
686688
)
687689
yield RulePart(

tests/test_routing.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -381,20 +381,63 @@ def test_greedy():
381381
[
382382
r.Rule("/foo", endpoint="foo"),
383383
r.Rule("/<path:bar>", endpoint="bar"),
384-
r.Rule("/<path:bar>/<path:blub>", endpoint="bar"),
384+
r.Rule("/<path:bar>/<blub>", endpoint="bar"),
385+
r.Rule("/<baz>/static", endpoint="oops"),
385386
]
386387
)
387388
adapter = map.bind("example.org", "/")
388389

389390
assert adapter.match("/foo") == ("foo", {})
390391
assert adapter.match("/blub") == ("bar", {"bar": "blub"})
391392
assert adapter.match("/he/he") == ("bar", {"bar": "he", "blub": "he"})
393+
assert adapter.match("/he/static") == ("oops", {"baz": "he"})
394+
395+
assert adapter.build("foo", {}) == "/foo"
396+
assert adapter.build("bar", {"bar": "blub"}) == "/blub"
397+
assert adapter.build("bar", {"bar": "blub", "blub": "bar"}) == "/blub/bar"
398+
399+
400+
def test_greedy_double_paths():
401+
# two back to back paths do not have any meaning and should
402+
# probably cause an error in _parse_rule
403+
map = r.Map(
404+
[
405+
r.Rule("/foo", endpoint="foo"),
406+
r.Rule("/<path:bar>", endpoint="bar"),
407+
r.Rule("/<path:bar>/<path:blub>", endpoint="bar"),
408+
]
409+
)
410+
adapter = map.bind("example.org", "/")
411+
412+
assert adapter.match("/foo") == ("foo", {})
413+
assert adapter.match("/blub") == ("bar", {"bar": "blub"})
414+
assert adapter.match("/he/he") == ("bar", {"bar": "he/he"})
415+
# can't match ("bar", {"bar": "he", "blub": "he"}) without breaking static matching
416+
# use a rule like "/<path:bar/<blub>" instead
392417

393418
assert adapter.build("foo", {}) == "/foo"
394419
assert adapter.build("bar", {"bar": "blub"}) == "/blub"
395420
assert adapter.build("bar", {"bar": "blub", "blub": "bar"}) == "/blub/bar"
396421

397422

423+
def test_static_priority():
424+
# see https://github.com/pallets/werkzeug/issues/2924
425+
map = r.Map(
426+
[
427+
r.Rule("/<path:dyn2>/<dyn1>", endpoint="file"),
428+
r.Rule("/<dyn1>/statn", endpoint="stat"),
429+
],
430+
)
431+
432+
adapter = map.bind("example.org", "/")
433+
434+
assert adapter.match("/d2/d1", method="GET") == (
435+
"file",
436+
{"dyn2": "d2", "dyn1": "d1"},
437+
)
438+
assert adapter.match("/d1/statn", method="GET") == ("stat", {"dyn1": "d1"})
439+
440+
398441
def test_path():
399442
map = r.Map(
400443
[

0 commit comments

Comments
 (0)