Add a type checking pipeline (#1682)

* Integrate with mypy
This commit is contained in:
Vinícius Dantas 2019-09-22 17:55:36 -03:00 committed by 7
parent 927c0e082e
commit 6fc3381229
16 changed files with 115 additions and 53 deletions

View File

@ -21,6 +21,12 @@ matrix:
dist: xenial
sudo: true
name: "Python 3.7 without Extensions"
- env: TOX_ENV=type-checking
python: 3.6
name: "Python 3.6 Type checks"
- env: TOX_ENV=type-checking
python: 3.7
name: "Python 3.7 Type checks"
- env: TOX_ENV=lint
python: 3.6
name: "Python 3.6 Linter checks"

View File

@ -1,5 +1,6 @@
from argparse import ArgumentParser
from importlib import import_module
from typing import Any, Dict, Optional
from sanic.app import Sanic
from sanic.log import logger
@ -35,7 +36,10 @@ if __name__ == "__main__":
)
)
if args.cert is not None or args.key is not None:
ssl = {"cert": args.cert, "key": args.key}
ssl = {
"cert": args.cert,
"key": args.key,
} # type: Optional[Dict[str, Any]]
else:
ssl = None

View File

@ -11,7 +11,7 @@ from inspect import getmodulename, isawaitable, signature, stack
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from traceback import format_exc
from typing import Any, Optional, Type, Union
from typing import Any, Dict, Optional, Type, Union
from urllib.parse import urlencode, urlunparse
from sanic import reloader_helpers
@ -768,7 +768,7 @@ class Sanic:
URLBuildError
"""
# find the route by the supplied view name
kw = {}
kw: Dict[str, str] = {}
# special static files url_for
if view_name == "static":
kw.update(name=kwargs.pop("name", "static"))

View File

@ -2,9 +2,23 @@ import asyncio
import warnings
from inspect import isawaitable
from typing import Any, Awaitable, Callable, MutableMapping, Union
from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
MutableMapping,
Optional,
Tuple,
Union,
)
from urllib.parse import quote
from requests_async import ASGISession # type: ignore
import sanic.app # noqa
from sanic.compat import Header
from sanic.exceptions import InvalidUsage, ServerError
from sanic.log import logger
@ -54,6 +68,8 @@ class MockProtocol:
class MockTransport:
_protocol: Optional[MockProtocol]
def __init__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> None:
@ -68,11 +84,12 @@ class MockTransport:
self._protocol = MockProtocol(self, self.loop)
return self._protocol
def get_extra_info(self, info: str) -> Union[str, bool]:
def get_extra_info(self, info: str) -> Union[str, bool, None]:
if info == "peername":
return self.scope.get("server")
elif info == "sslcontext":
return self.scope.get("scheme") in ["https", "wss"]
return None
def get_websocket_connection(self) -> WebSocketConnection:
try:
@ -172,6 +189,13 @@ class Lifespan:
class ASGIApp:
sanic_app: Union[ASGISession, "sanic.app.Sanic"]
request: Request
transport: MockTransport
do_stream: bool
lifespan: Lifespan
ws: Optional[WebSocketConnection]
def __init__(self) -> None:
self.ws = None
@ -182,8 +206,8 @@ class ASGIApp:
instance = cls()
instance.sanic_app = sanic_app
instance.transport = MockTransport(scope, receive, send)
instance.transport.add_task = sanic_app.loop.create_task
instance.transport.loop = sanic_app.loop
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
headers = Header(
[
@ -286,8 +310,8 @@ class ASGIApp:
"""
Write the response.
"""
headers = []
cookies = {}
headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {}
try:
cookies = {
v.key: v

View File

@ -56,7 +56,7 @@ class BlueprintGroup(MutableSequence):
"""
return self._blueprints[item]
def __setitem__(self, index: int, item: object) -> None:
def __setitem__(self, index, item) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
@ -69,7 +69,7 @@ class BlueprintGroup(MutableSequence):
"""
self._blueprints[index] = item
def __delitem__(self, index: int) -> None:
def __delitem__(self, index) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.

View File

@ -1,4 +1,4 @@
from multidict import CIMultiDict
from multidict import CIMultiDict # type: ignore
class Header(CIMultiDict):

View File

@ -1,10 +1,10 @@
import re
from typing import Dict, Iterable, Optional, Tuple
from typing import Dict, Iterable, List, Optional, Tuple, Union
from urllib.parse import unquote
Options = Dict[str, str] # key=value fields in various headers
Options = Dict[str, Union[int, str]] # key=value fields in various headers
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
@ -35,7 +35,7 @@ def parse_content_header(value: str) -> Tuple[str, Options]:
value = _firefox_quote_escape.sub("%22", value)
pos = value.find(";")
if pos == -1:
options = {}
options: Dict[str, Union[int, str]] = {}
else:
options = {
m.group(1).lower(): m.group(2) or m.group(3).replace("%22", '"')
@ -67,7 +67,7 @@ def parse_forwarded(headers, config) -> Optional[Options]:
return None
# Loop over <separator><key>=<value> elements from right to left
sep = pos = None
options = []
options: List[Tuple[str, str]] = []
found = False
for m in _rparam.finditer(header[::-1]):
# Start of new element? (on parser skips and non-semicolon right sep)
@ -101,8 +101,13 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
try:
# Combine, split and filter multiple headers' entries
forwarded_for = headers.getall(config.FORWARDED_FOR_HEADER)
proxies = (p.strip() for h in forwarded_for for p in h.split(","))
proxies = [p for p in proxies if p]
proxies = [
p
for p in (
p.strip() for h in forwarded_for for p in h.split(",")
)
if p
]
addr = proxies[-proxies_count]
except (KeyError, IndexError):
pass
@ -126,7 +131,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
def fwd_normalize(fwd: OptionsIterable) -> Options:
"""Normalize and convert values extracted from forwarded headers."""
ret = {}
ret: Dict[str, Union[int, str]] = {}
for key, val in fwd:
if val is not None:
try:
@ -164,4 +169,4 @@ def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
if not m:
return None, None
host, port = m.groups()
return host.lower(), port and int(port)
return host.lower(), int(port) if port is not None else None

View File

@ -6,7 +6,7 @@ from collections import defaultdict, namedtuple
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from httptools import parse_url
from httptools import parse_url # type: ignore
from sanic.exceptions import InvalidUsage
from sanic.headers import (
@ -19,9 +19,9 @@ from sanic.log import error_logger, logger
try:
from ujson import loads as json_loads
from ujson import loads as json_loads # type: ignore
except ImportError:
from json import loads as json_loads
from json import loads as json_loads # type: ignore
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
EXPECT_HEADER = "EXPECT"

View File

@ -3,7 +3,7 @@ from mimetypes import guess_type
from os import path
from urllib.parse import quote_plus
from aiofiles import open as open_async
from aiofiles import open as open_async # type: ignore
from sanic.compat import Header
from sanic.cookies import CookieJar

View File

@ -10,8 +10,8 @@ from signal import signal as signal_func
from socket import SO_REUSEADDR, SOL_SOCKET, socket
from time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header
from sanic.exceptions import (
@ -28,7 +28,7 @@ from sanic.response import HTTPResponse
try:
import uvloop
import uvloop # type: ignore
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

View File

@ -4,7 +4,7 @@ from re import sub
from time import gmtime, strftime
from urllib.parse import unquote
from aiofiles.os import stat
from aiofiles.os import stat # type: ignore
from sanic.exceptions import (
ContentRangeError,

View File

@ -6,9 +6,9 @@ from json import JSONDecodeError
from socket import socket
from urllib.parse import unquote, urlsplit
import httpcore
import requests_async as requests
import websockets
import httpcore # type: ignore
import requests_async as requests # type: ignore
import websockets # type: ignore
from sanic.asgi import ASGIApp
from sanic.exceptions import MethodNotSupported
@ -288,6 +288,14 @@ class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa
request_complete = True
return {"type": "http.request", "body": body_bytes}
request_complete = False
response_started = False
response_complete = False
raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any]
template = None
context = None
return_value = None
async def send(message) -> None:
nonlocal raw_kwargs, response_started, response_complete, template, context # noqa
@ -316,14 +324,6 @@ class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa
template = message["template"]
context = message["context"]
request_complete = False
response_started = False
response_complete = False
raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any]
template = None
context = None
return_value = None
try:
return_value = await self.app(scope, receive, send)
except BaseException as exc:

View File

@ -1,3 +1,5 @@
from typing import Any, Callable, List
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
@ -37,7 +39,7 @@ class HTTPMethodView:
To add any decorator you could set it into decorators variable
"""
decorators = []
decorators: List[Callable[[Callable[..., Any]], Callable[..., Any]]] = []
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)

View File

@ -1,13 +1,27 @@
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
from typing import (
Any,
Awaitable,
Callable,
Dict,
MutableMapping,
Optional,
Union,
)
from httptools import HttpParserUpgrade
from websockets import ConnectionClosed # noqa
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
from httptools import HttpParserUpgrade # type: ignore
from websockets import ( # type: ignore
ConnectionClosed,
InvalidHandshake,
WebSocketCommonProtocol,
handshake,
)
from sanic.exceptions import InvalidUsage
from sanic.server import HttpProtocol
__all__ = ["ConnectionClosed", "WebSocketProtocol", "WebSocketConnection"]
ASIMessage = MutableMapping[str, Any]
@ -125,14 +139,12 @@ class WebSocketConnection:
self._receive = receive
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
message = {"type": "websocket.send"}
message: Dict[str, Union[str, bytes]] = {"type": "websocket.send"}
try:
data.decode()
except AttributeError:
message.update({"text": str(data)})
else:
if isinstance(data, bytes):
message.update({"bytes": data})
else:
message.update({"text": str(data)})
await self._send(message)
@ -144,6 +156,8 @@ class WebSocketConnection:
elif message["type"] == "websocket.disconnect":
pass
return None
receive = recv
async def accept(self) -> None:

View File

@ -5,19 +5,19 @@ import signal
import sys
import traceback
import gunicorn.workers.base as base
import gunicorn.workers.base as base # type: ignore
from sanic.server import HttpProtocol, Signal, serve, trigger_events
from sanic.websocket import WebSocketProtocol
try:
import ssl
import ssl # type: ignore
except ImportError:
ssl = None
ssl = None # type: ignore
try:
import uvloop
import uvloop # type: ignore
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:

View File

@ -38,6 +38,13 @@ commands =
black --config ./.black.toml --check --verbose sanic/
isort --check-only --recursive sanic
[testenv:type-checking]
deps =
mypy
commands =
mypy sanic
[testenv:check]
deps =
docutils