Repository: pallets/werkzeug

Issue:
default `part_isolating=False` if the converter regex contains a `/`
Users didn't know about the `Converter.part_isolating` change as part of the router speedup, so custom converters that matched slashes stopped working. It should be possible to detect whether it should be disabled in most cases, so users won't need to know about it.

If a converter's `regex` has a `/`, we can assume it should set `part_isolating = False` by default. This default can still be overridden by setting `part_isolating` manually.

```python
def __init_subclass__(cls):
    super().__init_subclass__()
    if "part_isolating" not in cls.__dict__:
        cls.part_isolating = "/" not in cls.regex
```

This is a little too aggressive, as a regex could have something like `[^/]`, but that's unlikely since that's already something the router itself is handling, and the router will still work if a converter sets it `False` when it doesn't need to.

Originally discussed in https://github.com/pallets/werkzeug/issues/2506#issuecomment-1238375362


Discussion:

Resolving patch:
diff --git a/src/werkzeug/routing/converters.py b/src/werkzeug/routing/converters.py
--- a/src/werkzeug/routing/converters.py
+++ b/src/werkzeug/routing/converters.py
@@ -15,12 +15,24 @@ class ValidationError(ValueError):
 
 
 class BaseConverter:
-    """Base class for all converters."""
+    """Base class for all converters.
+
+    .. versionchanged:: 2.3
+        ``part_isolating`` defaults to ``False`` if ``regex`` contains a ``/``.
+    """
 
     regex = "[^/]+"
     weight = 100
     part_isolating = True
 
+    def __init_subclass__(cls, **kwargs: t.Any) -> None:
+        super().__init_subclass__(**kwargs)
+
+        # If the converter isn't inheriting its regex, disable part_isolating by default
+        # if the regex contains a / character.
+        if "regex" in cls.__dict__ and "part_isolating" not in cls.__dict__:
+            cls.part_isolating = "/" not in cls.regex
+
     def __init__(self, map: "Map", *args: t.Any, **kwargs: t.Any) -> None:
         self.map = map
 
@@ -51,8 +63,6 @@ class UnicodeConverter(BaseConverter):
     :param length: the exact length of the string.
     """
 
-    part_isolating = True
-
     def __init__(
         self,
         map: "Map",
@@ -86,8 +96,6 @@ class AnyConverter(BaseConverter):
         Value is validated when building a URL.
     """
 
-    part_isolating = True
-
     def __init__(self, map: "Map", *items: str) -> None:
         super().__init__(map)
         self.items = set(items)
@@ -113,7 +121,6 @@ class PathConverter(BaseConverter):
 
     regex = "[^/].*?"
     weight = 200
-    part_isolating = False
 
 
 class NumberConverter(BaseConverter):
@@ -124,7 +131,6 @@ class NumberConverter(BaseConverter):
 
     weight = 50
     num_convert: t.Callable = int
-    part_isolating = True
 
     def __init__(
         self,
@@ -186,7 +192,6 @@ class IntegerConverter(NumberConverter):
     """
 
     regex = r"\d+"
-    part_isolating = True
 
 
 class FloatConverter(NumberConverter):
@@ -210,7 +215,6 @@ class FloatConverter(NumberConverter):
 
     regex = r"\d+\.\d+"
     num_convert = float
-    part_isolating = True
 
     def __init__(
         self,
@@ -236,7 +240,6 @@ class UUIDConverter(BaseConverter):
         r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
         r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}"
     )
-    part_isolating = True
 
     def to_python(self, value: str) -> uuid.UUID:
         return uuid.UUID(value)
diff --git a/src/werkzeug/routing/rules.py b/src/werkzeug/routing/rules.py
--- a/src/werkzeug/routing/rules.py
+++ b/src/werkzeug/routing/rules.py
@@ -42,16 +42,16 @@ class RulePart:
 _part_re = re.compile(
     r"""
     (?:
-        (?P<slash>\/)                                 # a slash
+        (?P<slash>/)                                 # a slash
       |
-        (?P<static>[^<\/]+)                           # static rule data
+        (?P<static>[^</]+)                           # static rule data
       |
         (?:
           <
             (?:
               (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*)   # converter name
               (?:\((?P<arguments>.*?)\))?             # converter arguments
-              \:                                      # variable delimiter
+              :                                       # variable delimiter
             )?
             (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*)      # variable name
            >


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -800,6 +800,20 @@ def test_any_converter_build_validates_value() -> None:
     assert str(exc.value) == "'invalid' is not one of 'patient', 'provider'"
 
 
+def test_part_isolating_default() -> None:
+    class TwoConverter(r.BaseConverter):
+        regex = r"\w+/\w+"
+
+        def to_python(self, value: str) -> t.Any:
+            return value.split("/")
+
+    m = r.Map(
+        [r.Rule("/<two:values>/", endpoint="two")], converters={"two": TwoConverter}
+    )
+    a = m.bind("localhost")
+    assert a.match("/a/b/") == ("two", {"values": ["a", "b"]})
+
+
 @pytest.mark.parametrize(
     ("endpoint", "value", "expect"),
     [