view function is actually type checked

This commit is contained in:
David Lord 2022-06-03 12:54:54 -07:00
parent 8c6f1d96de
commit 81be290ec8
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
5 changed files with 64 additions and 46 deletions

View File

@ -1033,7 +1033,7 @@ class Flask(Scaffold):
self, self,
rule: str, rule: str,
endpoint: t.Optional[str] = None, 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, provide_automatic_options: t.Optional[bool] = None,
**options: t.Any, **options: t.Any,
) -> None: ) -> None:
@ -1681,7 +1681,7 @@ class Flask(Scaffold):
if isinstance(rv[1], (Headers, dict, tuple, list)): if isinstance(rv[1], (Headers, dict, tuple, list)):
rv, headers = rv rv, headers = rv
else: else:
rv, status = rv # type: ignore[misc] rv, status = rv # type: ignore[assignment,misc]
# other sized tuples are not allowed # other sized tuples are not allowed
else: else:
raise TypeError( raise TypeError(

View File

@ -384,7 +384,7 @@ class Blueprint(Scaffold):
self, self,
rule: str, rule: str,
endpoint: t.Optional[str] = None, 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, provide_automatic_options: t.Optional[bool] = None,
**options: t.Any, **options: t.Any,
) -> None: ) -> None:

View File

@ -363,48 +363,60 @@ class Scaffold:
method: str, method: str,
rule: str, rule: str,
options: dict, options: dict,
) -> t.Callable[[F], F]: ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
if "methods" in options: if "methods" in options:
raise TypeError("Use the 'route' decorator to use the 'methods' argument.") raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
return self.route(rule, methods=[method], **options) 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"]``. """Shortcut for :meth:`route` with ``methods=["GET"]``.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
return self._method_route("GET", rule, options) 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"]``. """Shortcut for :meth:`route` with ``methods=["POST"]``.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
return self._method_route("POST", rule, options) 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"]``. """Shortcut for :meth:`route` with ``methods=["PUT"]``.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
return self._method_route("PUT", rule, options) 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"]``. """Shortcut for :meth:`route` with ``methods=["DELETE"]``.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
return self._method_route("DELETE", rule, options) 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"]``. """Shortcut for :meth:`route` with ``methods=["PATCH"]``.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
return self._method_route("PATCH", rule, options) 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 """Decorate a view function to register it with the given URL
rule and options. Calls :meth:`add_url_rule`, which has more rule and options. Calls :meth:`add_url_rule`, which has more
details about the implementation. details about the implementation.
@ -428,7 +440,7 @@ class Scaffold:
:class:`~werkzeug.routing.Rule` object. :class:`~werkzeug.routing.Rule` object.
""" """
def decorator(f: F) -> F: def decorator(f: ft.RouteDecorator) -> ft.RouteDecorator:
endpoint = options.pop("endpoint", None) endpoint = options.pop("endpoint", None)
self.add_url_rule(rule, endpoint, f, **options) self.add_url_rule(rule, endpoint, f, **options)
return f return f
@ -440,7 +452,7 @@ class Scaffold:
self, self,
rule: str, rule: str,
endpoint: t.Optional[str] = None, 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, provide_automatic_options: t.Optional[bool] = None,
**options: t.Any, **options: t.Any,
) -> None: ) -> None:

View File

@ -1,37 +1,30 @@
import typing as t import typing as t
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from _typeshed.wsgi import WSGIApplication # noqa: F401 from _typeshed.wsgi import WSGIApplication # noqa: F401
from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401
from werkzeug.wrappers import Response # noqa: F401 from werkzeug.wrappers import Response # noqa: F401
# The possible types that are directly convertible or are a Response object. # The possible types that are directly convertible or are a Response object.
ResponseValue = t.Union[ ResponseValue = t.Union["Response", str, bytes, t.Dict[str, t.Any]]
"Response",
str,
bytes,
t.Dict[str, t.Any], # any jsonify-able dict
t.Iterator[str],
t.Iterator[bytes],
]
StatusCode = int
# the possible types for an individual HTTP header # 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, ...]] HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]]
# the possible types for HTTP headers # the possible types for HTTP headers
HeadersValue = t.Union[ 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. # The possible types returned by a route function.
ResponseReturnValue = t.Union[ ResponseReturnValue = t.Union[
ResponseValue, ResponseValue,
t.Tuple[ResponseValue, HeadersValue], t.Tuple[ResponseValue, HeadersValue],
t.Tuple[ResponseValue, StatusCode], t.Tuple[ResponseValue, int],
t.Tuple[ResponseValue, StatusCode, HeadersValue], t.Tuple[ResponseValue, int, HeadersValue],
"WSGIApplication", "WSGIApplication",
] ]
@ -51,6 +44,7 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool] TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict], None] URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
# This should take Exception, but that either breaks typing the argument # This should take Exception, but that either breaks typing the argument
# with a specific exception, or decorating multiple times with different # with a specific exception, or decorating multiple times with different
# exceptions (and using a union type on the argument). # 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/4295
# https://github.com/pallets/flask/issues/4297 # https://github.com/pallets/flask/issues/4297
ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue] ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue]
ViewCallable = t.Callable[..., ResponseReturnValue]
RouteDecorator = t.TypeVar("RouteDecorator", bound=ViewCallable)

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from http import HTTPStatus from http import HTTPStatus
from typing import Tuple
from typing import Union
from flask import Flask from flask import Flask
from flask import jsonify from flask import jsonify
@ -8,42 +8,51 @@ from flask.templating import render_template
from flask.views import View from flask.views import View
from flask.wrappers import Response from flask.wrappers import Response
app = Flask(__name__) app = Flask(__name__)
@app.route("/") @app.route("/str")
def hello_world() -> str: def hello_str() -> str:
return "<p>Hello, World!</p>" return "<p>Hello, World!</p>"
@app.route("/bytes")
def hello_bytes() -> bytes:
return b"<p>Hello, World!</p>"
@app.route("/json") @app.route("/json")
def hello_world_json() -> Response: def hello_json() -> Response:
return jsonify({"response": "Hello, World!"}) return jsonify({"response": "Hello, World!"})
@app.route("/status")
@app.route("/status/<int:code>")
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")
@app.route("/template/<name>") @app.route("/template/<name>")
def return_template(name: Union[str, None] = None) -> str: def return_template(name: str | None = None) -> str:
return render_template("index.html", name=name) return render_template("index.html", name=name)
@app.errorhandler(HTTPStatus.INTERNAL_SERVER_ERROR)
def error_500(e) -> Tuple[str, int]:
return "<p>Sorry, we are having problems</p>", 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): class RenderTemplateView(View):
def __init__(self: "RenderTemplateView", template_name: str) -> None: def __init__(self: RenderTemplateView, template_name: str) -> None:
self.template_name = template_name self.template_name = template_name
def dispatch_request(self: "RenderTemplateView") -> str: def dispatch_request(self: RenderTemplateView) -> str:
return render_template(self.template_name) return render_template(self.template_name)