Skip to content

Commit

Permalink
Add a type checking pipeline (#1682)
Browse files Browse the repository at this point in the history
* Integrate with mypy
  • Loading branch information
viniciusd authored and yunstanford committed Sep 22, 2019
1 parent 927c0e0 commit 6fc3381
Show file tree
Hide file tree
Showing 16 changed files with 116 additions and 54 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion sanic/__main__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -35,7 +36,10 @@
)
)
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

Expand Down
4 changes: 2 additions & 2 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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
Expand Down Expand Up @@ -768,7 +768,7 @@ def url_for(self, view_name: str, **kwargs):
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"))
Expand Down
34 changes: 29 additions & 5 deletions sanic/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@
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
Expand Down Expand Up @@ -54,6 +68,8 @@ async def drain(self) -> None:


class MockTransport:
_protocol: Optional[MockProtocol]

def __init__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
) -> None:
Expand All @@ -68,11 +84,12 @@ def get_protocol(self) -> MockProtocol:
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:
Expand Down Expand Up @@ -172,6 +189,13 @@ async def __call__(


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

Expand All @@ -182,8 +206,8 @@ async def create(
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(
[
Expand Down Expand Up @@ -286,8 +310,8 @@ async def stream_callback(self, response: HTTPResponse) -> None:
"""
Write the response.
"""
headers = []
cookies = {}
headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {}
try:
cookies = {
v.key: v
Expand Down
4 changes: 2 additions & 2 deletions sanic/blueprint_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __getitem__(self, 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
into a list like object to support all the existing behavior.
Expand All @@ -69,7 +69,7 @@ def __setitem__(self, index: int, item: object) -> None:
"""
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.
Expand Down
2 changes: 1 addition & 1 deletion sanic/compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from multidict import CIMultiDict
from multidict import CIMultiDict # type: ignore


class Header(CIMultiDict):
Expand Down
21 changes: 13 additions & 8 deletions sanic/headers.py
Original file line number Diff line number Diff line change
@@ -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'"([^"]*)"'
Expand Down Expand Up @@ -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", '"')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -126,7 +131,7 @@ def 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:
Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions sanic/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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 (
Expand All @@ -19,9 +19,9 @@


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"
Expand Down
2 changes: 1 addition & 1 deletion sanic/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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
Expand Down
6 changes: 3 additions & 3 deletions sanic/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
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 (
Expand All @@ -28,7 +28,7 @@


try:
import uvloop
import uvloop # type: ignore

if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
Expand Down
2 changes: 1 addition & 1 deletion sanic/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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,
Expand Down
22 changes: 11 additions & 11 deletions sanic/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
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
Expand Down Expand Up @@ -288,6 +288,14 @@ async def receive():
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

Expand Down Expand Up @@ -316,14 +324,6 @@ async def send(message) -> None:
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:
Expand Down
4 changes: 3 additions & 1 deletion sanic/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any, Callable, List

from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage

Expand Down Expand Up @@ -37,7 +39,7 @@ def get(self, request, my_param_here, *args, **kwargs):
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)
Expand Down
36 changes: 25 additions & 11 deletions sanic/websocket.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union

from httptools import HttpParserUpgrade
from websockets import ConnectionClosed # noqa
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
from typing import (
Any,
Awaitable,
Callable,
Dict,
MutableMapping,
Optional,
Union,
)

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]


Expand Down Expand Up @@ -125,14 +139,12 @@ def __init__(
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)

Expand All @@ -144,6 +156,8 @@ async def recv(self, *args, **kwargs) -> Optional[str]:
elif message["type"] == "websocket.disconnect":
pass

return None

receive = recv

async def accept(self) -> None:
Expand Down
Loading

0 comments on commit 6fc3381

Please sign in to comment.