From 81be290ec8889c157c84bc7ce857f883396c5daf Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 3 Jun 2022 12:54:54 -0700 Subject: [PATCH] view function is actually type checked --- src/flask/app.py | 4 +-- src/flask/blueprints.py | 2 +- src/flask/scaffold.py | 30 +++++++++++++++------- src/flask/typing.py | 25 ++++++++---------- tests/typing/typing_route.py | 49 +++++++++++++++++++++--------------- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 34ea5b29..6b549188 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1033,7 +1033,7 @@ class Flask(Scaffold): self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + view_func: t.Optional[ft.ViewCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: @@ -1681,7 +1681,7 @@ class Flask(Scaffold): if isinstance(rv[1], (Headers, dict, tuple, list)): rv, headers = rv else: - rv, status = rv # type: ignore[misc] + rv, status = rv # type: ignore[assignment,misc] # other sized tuples are not allowed else: raise TypeError( diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index c60183fa..cf04cd78 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -384,7 +384,7 @@ class Blueprint(Scaffold): self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + view_func: t.Optional[ft.ViewCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 7cd8bab5..8400e892 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -363,48 +363,60 @@ class Scaffold: method: str, rule: str, options: dict, - ) -> t.Callable[[F], F]: + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: if "methods" in options: raise TypeError("Use the 'route' decorator to use the 'methods' argument.") return self.route(rule, methods=[method], **options) - def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def get( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["GET"]``. .. versionadded:: 2.0 """ return self._method_route("GET", rule, options) - def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def post( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["POST"]``. .. versionadded:: 2.0 """ return self._method_route("POST", rule, options) - def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def put( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["PUT"]``. .. versionadded:: 2.0 """ return self._method_route("PUT", rule, options) - def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def delete( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["DELETE"]``. .. versionadded:: 2.0 """ return self._method_route("DELETE", rule, options) - def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def patch( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["PATCH"]``. .. versionadded:: 2.0 """ return self._method_route("PATCH", rule, options) - def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def route( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Decorate a view function to register it with the given URL rule and options. Calls :meth:`add_url_rule`, which has more details about the implementation. @@ -428,7 +440,7 @@ class Scaffold: :class:`~werkzeug.routing.Rule` object. """ - def decorator(f: F) -> F: + def decorator(f: ft.RouteDecorator) -> ft.RouteDecorator: endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f @@ -440,7 +452,7 @@ class Scaffold: self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + view_func: t.Optional[ft.ViewCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: diff --git a/src/flask/typing.py b/src/flask/typing.py index ec7c7969..d463a8de 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -1,37 +1,30 @@ import typing as t - if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.wrappers import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. -ResponseValue = t.Union[ - "Response", - str, - bytes, - t.Dict[str, t.Any], # any jsonify-able dict - t.Iterator[str], - t.Iterator[bytes], -] -StatusCode = int +ResponseValue = t.Union["Response", str, bytes, t.Dict[str, t.Any]] # the possible types for an individual HTTP header -HeaderName = str +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] # the possible types for HTTP headers HeadersValue = t.Union[ - "Headers", t.Dict[HeaderName, HeaderValue], t.List[t.Tuple[HeaderName, HeaderValue]] + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[t.Tuple[str, HeaderValue]], ] # The possible types returned by a route function. ResponseReturnValue = t.Union[ ResponseValue, t.Tuple[ResponseValue, HeadersValue], - t.Tuple[ResponseValue, StatusCode], - t.Tuple[ResponseValue, StatusCode, HeadersValue], + t.Tuple[ResponseValue, int], + t.Tuple[ResponseValue, int, HeadersValue], "WSGIApplication", ] @@ -51,6 +44,7 @@ TemplateGlobalCallable = t.Callable[..., t.Any] TemplateTestCallable = t.Callable[..., bool] URLDefaultCallable = t.Callable[[str, dict], None] URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] + # This should take Exception, but that either breaks typing the argument # with a specific exception, or decorating multiple times with different # exceptions (and using a union type on the argument). @@ -58,3 +52,6 @@ URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], N # https://github.com/pallets/flask/issues/4295 # https://github.com/pallets/flask/issues/4297 ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue] + +ViewCallable = t.Callable[..., ResponseReturnValue] +RouteDecorator = t.TypeVar("RouteDecorator", bound=ViewCallable) diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index 0cb45333..ba49d132 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -1,6 +1,6 @@ +from __future__ import annotations + from http import HTTPStatus -from typing import Tuple -from typing import Union from flask import Flask from flask import jsonify @@ -8,42 +8,51 @@ from flask.templating import render_template from flask.views import View from flask.wrappers import Response - app = Flask(__name__) -@app.route("/") -def hello_world() -> str: +@app.route("/str") +def hello_str() -> str: return "

Hello, World!

" +@app.route("/bytes") +def hello_bytes() -> bytes: + return b"

Hello, World!

" + + @app.route("/json") -def hello_world_json() -> Response: +def hello_json() -> Response: return jsonify({"response": "Hello, World!"}) +@app.route("/status") +@app.route("/status/") +def tuple_status(code: int = 200) -> tuple[str, int]: + return "hello", code + + +@app.route("/status-enum") +def tuple_status_enum() -> tuple[str, int]: + return "hello", HTTPStatus.OK + + +@app.route("/headers") +def tuple_headers() -> tuple[str, dict[str, str]]: + return "Hello, World!", {"Content-Type": "text/plain"} + + @app.route("/template") @app.route("/template/") -def return_template(name: Union[str, None] = None) -> str: +def return_template(name: str | None = None) -> str: return render_template("index.html", name=name) -@app.errorhandler(HTTPStatus.INTERNAL_SERVER_ERROR) -def error_500(e) -> Tuple[str, int]: - return "

Sorry, we are having problems

", HTTPStatus.INTERNAL_SERVER_ERROR - - -@app.before_request -def before_request() -> None: - app.logger.debug("Executing a sample before_request function") - return None - - class RenderTemplateView(View): - def __init__(self: "RenderTemplateView", template_name: str) -> None: + def __init__(self: RenderTemplateView, template_name: str) -> None: self.template_name = template_name - def dispatch_request(self: "RenderTemplateView") -> str: + def dispatch_request(self: RenderTemplateView) -> str: return render_template(self.template_name)