Repository: pallets/werkzeug

Issue:
`Rule.endpoint` type annotation doesn't comply with the documentation
<!--
This issue tracker is a tool to address bugs in Werkzeug itself. Please
use Pallets Discord or Stack Overflow for questions about your own code.

Replace this comment with a clear outline of what the bug is.
-->

According to the documentation, anything can be used as a `Rule` endpoint: https://werkzeug.palletsprojects.com/en/3.0.x/routing/#werkzeug.routing.Rule

>    The endpoint for this rule. This can be anything. A reference to a function, a string, a number etc. The preferred way is using a string because the endpoint is used for URL generation.

However, the type is annotated as `str | None`:
https://github.com/pallets/werkzeug/blob/d3dd65a27388fbd39d146caacf2563639ba622f0/src/werkzeug/routing/rules.py#L448
https://github.com/pallets/werkzeug/blob/d3dd65a27388fbd39d146caacf2563639ba622f0/src/werkzeug/routing/rules.py#L488

The same applies to the parameter `endpoint` of `MapAdapter.build()` (and possibly more):
https://github.com/pallets/werkzeug/blob/d3dd65a27388fbd39d146caacf2563639ba622f0/src/werkzeug/routing/map.py#L825

Environment:
- Werkzeug version: 3.0.1



Discussion:

Resolving patch:
diff --git a/src/werkzeug/routing/exceptions.py b/src/werkzeug/routing/exceptions.py
--- a/src/werkzeug/routing/exceptions.py
+++ b/src/werkzeug/routing/exceptions.py
@@ -59,7 +59,7 @@ def __init__(self, path_info: str) -> None:
 class RequestAliasRedirect(RoutingException):  # noqa: B903
     """This rule is an alias and wants to redirect to the canonical URL."""
 
-    def __init__(self, matched_values: t.Mapping[str, t.Any], endpoint: str) -> None:
+    def __init__(self, matched_values: t.Mapping[str, t.Any], endpoint: t.Any) -> None:
         super().__init__()
         self.matched_values = matched_values
         self.endpoint = endpoint
@@ -72,7 +72,7 @@ class BuildError(RoutingException, LookupError):
 
     def __init__(
         self,
-        endpoint: str,
+        endpoint: t.Any,
         values: t.Mapping[str, t.Any],
         method: str | None,
         adapter: MapAdapter | None = None,
@@ -93,7 +93,10 @@ def _score_rule(rule: Rule) -> float:
                 [
                     0.98
                     * difflib.SequenceMatcher(
-                        None, rule.endpoint, self.endpoint
+                        # endpoints can be any type, compare as strings
+                        None,
+                        str(rule.endpoint),
+                        str(self.endpoint),
                     ).ratio(),
                     0.01 * bool(set(self.values or ()).issubset(rule.arguments)),
                     0.01 * bool(rule.methods and self.method in rule.methods),
diff --git a/src/werkzeug/routing/map.py b/src/werkzeug/routing/map.py
--- a/src/werkzeug/routing/map.py
+++ b/src/werkzeug/routing/map.py
@@ -104,7 +104,7 @@ def __init__(
         host_matching: bool = False,
     ) -> None:
         self._matcher = StateMachineMatcher(merge_slashes)
-        self._rules_by_endpoint: dict[str, list[Rule]] = {}
+        self._rules_by_endpoint: dict[t.Any, list[Rule]] = {}
         self._remap = True
         self._remap_lock = self.lock_class()
 
@@ -131,7 +131,7 @@ def merge_slashes(self) -> bool:
     def merge_slashes(self, value: bool) -> None:
         self._matcher.merge_slashes = value
 
-    def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool:
+    def is_endpoint_expecting(self, endpoint: t.Any, *arguments: str) -> bool:
         """Iterate over all rules and check if the endpoint expects
         the arguments provided.  This is for example useful if you have
         some URLs that expect a language code and others that do not and
@@ -155,7 +155,7 @@ def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool:
     def _rules(self) -> list[Rule]:
         return [rule for rules in self._rules_by_endpoint.values() for rule in rules]
 
-    def iter_rules(self, endpoint: str | None = None) -> t.Iterator[Rule]:
+    def iter_rules(self, endpoint: t.Any | None = None) -> t.Iterator[Rule]:
         """Iterate over all rules or the rules of an endpoint.
 
         :param endpoint: if provided only the rules for that endpoint
@@ -470,14 +470,14 @@ def application(environ, start_response):
             raise
 
     @t.overload
-    def match(  # type: ignore
+    def match(
         self,
         path_info: str | None = None,
         method: str | None = None,
         return_rule: t.Literal[False] = False,
         query_args: t.Mapping[str, t.Any] | str | None = None,
         websocket: bool | None = None,
-    ) -> tuple[str, t.Mapping[str, t.Any]]: ...
+    ) -> tuple[t.Any, t.Mapping[str, t.Any]]: ...
 
     @t.overload
     def match(
@@ -496,7 +496,7 @@ def match(
         return_rule: bool = False,
         query_args: t.Mapping[str, t.Any] | str | None = None,
         websocket: bool | None = None,
-    ) -> tuple[str | Rule, t.Mapping[str, t.Any]]:
+    ) -> tuple[t.Any | Rule, t.Mapping[str, t.Any]]:
         """The usage is simple: you just pass the match method the current
         path info as well as the method (which defaults to `GET`).  The
         following things can then happen:
@@ -770,7 +770,7 @@ def make_redirect_url(
     def make_alias_redirect_url(
         self,
         path: str,
-        endpoint: str,
+        endpoint: t.Any,
         values: t.Mapping[str, t.Any],
         method: str,
         query_args: t.Mapping[str, t.Any] | str,
@@ -786,7 +786,7 @@ def make_alias_redirect_url(
 
     def _partial_build(
         self,
-        endpoint: str,
+        endpoint: t.Any,
         values: t.Mapping[str, t.Any],
         method: str | None,
         append_unknown: bool,
@@ -827,7 +827,7 @@ def _partial_build(
 
     def build(
         self,
-        endpoint: str,
+        endpoint: t.Any,
         values: t.Mapping[str, t.Any] | None = None,
         method: str | None = None,
         force_external: bool = False,
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
@@ -453,7 +453,7 @@ def __init__(
         subdomain: str | None = None,
         methods: t.Iterable[str] | None = None,
         build_only: bool = False,
-        endpoint: str | None = None,
+        endpoint: t.Any | None = None,
         strict_slashes: bool | None = None,
         merge_slashes: bool | None = None,
         redirect_to: str | t.Callable[..., str] | None = None,
@@ -493,7 +493,7 @@ def __init__(
                 )
 
         self.methods = methods
-        self.endpoint: str = endpoint  # type: ignore
+        self.endpoint: t.Any = endpoint
         self.redirect_to = redirect_to
 
         if defaults:


Test patch: