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 dist: xenial
sudo: true sudo: true
name: "Python 3.7 without Extensions" 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 - env: TOX_ENV=lint
python: 3.6 python: 3.6
name: "Python 3.6 Linter checks" name: "Python 3.6 Linter checks"

View File

@ -1,5 +1,6 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from importlib import import_module from importlib import import_module
from typing import Any, Dict, Optional
from sanic.app import Sanic from sanic.app import Sanic
from sanic.log import logger from sanic.log import logger
@ -35,7 +36,10 @@ if __name__ == "__main__":
) )
) )
if args.cert is not None or args.key is not None: 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: else:
ssl = None ssl = None

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ from mimetypes import guess_type
from os import path from os import path
from urllib.parse import quote_plus 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.compat import Header
from sanic.cookies import CookieJar 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 socket import SO_REUSEADDR, SOL_SOCKET, socket
from time import time from time import time
from httptools import HttpRequestParser from httptools import HttpRequestParser # type: ignore
from httptools.parser.errors import HttpParserError from httptools.parser.errors import HttpParserError # type: ignore
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ( from sanic.exceptions import (
@ -28,7 +28,7 @@ from sanic.response import HTTPResponse
try: try:
import uvloop import uvloop # type: ignore
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_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 time import gmtime, strftime
from urllib.parse import unquote from urllib.parse import unquote
from aiofiles.os import stat from aiofiles.os import stat # type: ignore
from sanic.exceptions import ( from sanic.exceptions import (
ContentRangeError, ContentRangeError,

View File

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

View File

@ -1,3 +1,5 @@
from typing import Any, Callable, List
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
@ -37,7 +39,7 @@ class HTTPMethodView:
To add any decorator you could set it into decorators variable 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): def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) 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 httptools import HttpParserUpgrade # type: ignore
from websockets import ConnectionClosed # noqa from websockets import ( # type: ignore
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake ConnectionClosed,
InvalidHandshake,
WebSocketCommonProtocol,
handshake,
)
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
__all__ = ["ConnectionClosed", "WebSocketProtocol", "WebSocketConnection"]
ASIMessage = MutableMapping[str, Any] ASIMessage = MutableMapping[str, Any]
@ -125,14 +139,12 @@ class WebSocketConnection:
self._receive = receive self._receive = receive
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: 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: if isinstance(data, bytes):
data.decode()
except AttributeError:
message.update({"text": str(data)})
else:
message.update({"bytes": data}) message.update({"bytes": data})
else:
message.update({"text": str(data)})
await self._send(message) await self._send(message)
@ -144,6 +156,8 @@ class WebSocketConnection:
elif message["type"] == "websocket.disconnect": elif message["type"] == "websocket.disconnect":
pass pass
return None
receive = recv receive = recv
async def accept(self) -> None: async def accept(self) -> None:

View File

@ -5,19 +5,19 @@ import signal
import sys import sys
import traceback 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.server import HttpProtocol, Signal, serve, trigger_events
from sanic.websocket import WebSocketProtocol from sanic.websocket import WebSocketProtocol
try: try:
import ssl import ssl # type: ignore
except ImportError: except ImportError:
ssl = None ssl = None # type: ignore
try: try:
import uvloop import uvloop # type: ignore
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError: except ImportError:

View File

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