diff --git a/sanic/router.py b/sanic/router.py index 0973a3fa..2661acf5 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -1,5 +1,9 @@ +from __future__ import annotations + from functools import lru_cache +from inspect import signature from typing import Any, Dict, Iterable, List, Optional, Tuple, Union +from uuid import UUID from sanic_routing import BaseRouter # type: ignore from sanic_routing.exceptions import NoMethod # type: ignore @@ -106,6 +110,8 @@ class Router(BaseRouter): version = str(version).strip("/").lstrip("v") uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")]) + uri = self._normalize(uri, handler) + params = dict( path=uri, handler=handler, @@ -187,3 +193,24 @@ class Router(BaseRouter): raise SanicException( f"Invalid route: {route}. Parameter names cannot use '__'." ) + + def _normalize(self, uri: str, handler: RouteHandler) -> str: + if "<" not in uri: + return uri + + sig = signature(handler) + mapping = { + param.name: param.annotation.__name__.lower() + for param in sig.parameters.values() + if param.annotation in (str, int, float, UUID) + } + + reconstruction = [] + for part in uri.split("/"): + if part.startswith("<") and ":" not in part: + name = part[1:-1] + annotation = mapping.get(name) + if annotation: + part = f"<{name}:{annotation}>" + reconstruction.append(part) + return "/".join(reconstruction) diff --git a/tests/test_handler_annotations.py b/tests/test_handler_annotations.py new file mode 100644 index 00000000..14d1d7b7 --- /dev/null +++ b/tests/test_handler_annotations.py @@ -0,0 +1,39 @@ +from uuid import UUID + +import pytest + +from sanic import json + + +@pytest.mark.parametrize( + "idx,path,expectation", + ( + (0, "/abc", "str"), + (1, "/123", "int"), + (2, "/123.5", "float"), + (3, "/8af729fe-2b94-4a95-a168-c07068568429", "UUID"), + ), +) +def test_annotated_handlers(app, idx, path, expectation): + def build_response(num, foo): + return json({"num": num, "type": type(foo).__name__}) + + @app.get("/") + def handler0(_, foo: str): + return build_response(0, foo) + + @app.get("/") + def handler1(_, foo: int): + return build_response(1, foo) + + @app.get("/") + def handler2(_, foo: float): + return build_response(2, foo) + + @app.get("/") + def handler3(_, foo: UUID): + return build_response(3, foo) + + _, response = app.test_client.get(path) + assert response.json["num"] == idx + assert response.json["type"] == expectation