WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 7664662

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. closes #2924
1 parent 7868bef commit 7664662

File tree

2 files changed

+45
-3
lines changed

2 files changed

+45
-3
lines changed

src/werkzeug/routing/rules.py

Lines changed: 4 additions & 2 deletions
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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,20 +381,60 @@ 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"})
392394

393395
assert adapter.build("foo", {}) == "/foo"
394396
assert adapter.build("bar", {"bar": "blub"}) == "/blub"
395397
assert adapter.build("bar", {"bar": "blub", "blub": "bar"}) == "/blub/bar"
396398

397399

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+
# this cannot match ("bar", {"bar": "he", "blub": "he"}) without breaking static matching
416+
# users should use a rule like "/<path:bar/<blub>" instead
417+
418+
assert adapter.build("foo", {}) == "/foo"
419+
assert adapter.build("bar", {"bar": "blub"}) == "/blub"
420+
assert adapter.build("bar", {"bar": "blub", "blub": "bar"}) == "/blub/bar"
421+
422+
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") == ('file', {'dyn2': 'd2', 'dyn1': 'd1'})
435+
assert adapter.match("/d1/statn", method="GET") == ('stat', {'dyn1': 'd1'})
436+
437+
398438
def test_path():
399439
map = r.Map(
400440
[

0 commit comments

Comments
 (0)