Repository: pallets/werkzeug

Issue:
2.2.0 breaks werkzeug.routing.ValidationError import
```
(venv-test) $ pip install werkzeug
Collecting werkzeug
  Downloading Werkzeug-2.2.0-py3-none-any.whl (232 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 232.2/232.2 KB 3.5 MB/s eta 0:00:00
Collecting MarkupSafe>=2.1.1
  Using cached MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
Installing collected packages: MarkupSafe, werkzeug
Successfully installed MarkupSafe-2.1.1 werkzeug-2.2.0
(venv-test) $ python
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from werkzeug.routing import ValidationError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'ValidationError' from 'werkzeug.routing' (/tmp/venv-test/lib/python3.10/site-packages/werkzeug/routing/__init__.py)
```

This is a documented API for 2.2, e.g. https://werkzeug.palletsprojects.com/en/2.2.x/routing/#custom-converters example code.

Environment:

- Python version: 3.10
- Werkzeug version: 2.2.0



Discussion:
Missed this one when we refactored to split into submodules.

Resolving patch:
diff --git a/src/werkzeug/routing/__init__.py b/src/werkzeug/routing/__init__.py
--- a/src/werkzeug/routing/__init__.py
+++ b/src/werkzeug/routing/__init__.py
@@ -112,6 +112,7 @@
 from .converters import PathConverter
 from .converters import UnicodeConverter
 from .converters import UUIDConverter
+from .converters import ValidationError
 from .exceptions import BuildError
 from .exceptions import NoMatch
 from .exceptions import RequestAliasRedirect
diff --git a/src/werkzeug/routing/matcher.py b/src/werkzeug/routing/matcher.py
--- a/src/werkzeug/routing/matcher.py
+++ b/src/werkzeug/routing/matcher.py
@@ -129,6 +129,22 @@ def _match(
                     rv = _match(new_state, remaining, values + list(match.groups()))
                     if rv is not None:
                         return rv
+
+            # If there is no match and the only part left is a
+            # trailing slash ("") consider rules that aren't
+            # strict-slashes as these should match if there is a final
+            # slash part.
+            if parts == [""]:
+                for rule in state.rules:
+                    if rule.strict_slashes:
+                        continue
+                    if rule.methods is not None and method not in rule.methods:
+                        have_match_for.update(rule.methods)
+                    elif rule.websocket != websocket:
+                        websocket_mismatch = True
+                    else:
+                        return rule, values
+
             return None
 
         try:


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -219,6 +219,7 @@ def test_strict_slashes_leaves_dont_consume():
             r.Rule("/path3/", endpoint="branch", strict_slashes=False),
             r.Rule("/path4", endpoint="leaf", strict_slashes=False),
             r.Rule("/path4/", endpoint="branch", strict_slashes=False),
+            r.Rule("/path5", endpoint="leaf"),
         ],
         strict_slashes=False,
     )
@@ -233,6 +234,7 @@ def test_strict_slashes_leaves_dont_consume():
     assert adapter.match("/path3/", method="GET") == ("branch", {})
     assert adapter.match("/path4", method="GET") == ("leaf", {})
     assert adapter.match("/path4/", method="GET") == ("branch", {})
+    assert adapter.match("/path5/", method="GET") == ("leaf", {})
 
 
 def test_environ_defaults():