From a41066b413489b407b9d99174af697563ad680b9 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Mon, 13 Apr 2020 21:30:34 +0200 Subject: add type hints to all of the code In order to use type hint syntax, we need to bump the minimum Python version to 3.7 and some of the features such as Literal and Protocol are opted in when a sufficiently recent Python is available. This does not make all of the code pass type checking with mypy. A number of typing issues remain, but the output of mypy becomes something one can read through. In adding type hints, a lot of epydoc @type annotations are removed as redundant. This update also adopts black-style line breaking. --- wsgitools/scgi/asynchronous.py | 98 ++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 28 deletions(-) (limited to 'wsgitools/scgi/asynchronous.py') diff --git a/wsgitools/scgi/asynchronous.py b/wsgitools/scgi/asynchronous.py index 61bbc6b..264e43e 100644 --- a/wsgitools/scgi/asynchronous.py +++ b/wsgitools/scgi/asynchronous.py @@ -5,10 +5,24 @@ import io import socket import sys import errno +import typing -from wsgitools.internal import bytes2str, str2bytes +from wsgitools.internal import ( + bytes2str, + Environ, + HeaderList, + OptExcInfo, + str2bytes, + WriteCallback, + WsgiApp, +) from wsgitools.scgi import _convert_environ, FileWrapper +if sys.version_info >= (3, 8): + LiteralTrue = typing.Literal[True] +else: + LiteralTrue = bool + class SCGIConnection(asyncore.dispatcher): """SCGI connection class used by L{SCGIServer}.""" # connection states @@ -19,11 +33,27 @@ class SCGIConnection(asyncore.dispatcher): RESP = 3*4 | 2 # sending response, end state RESPH = 4*4 | 2 # buffered response headers, sending headers only, to TRANS TRANS = 5*4 | 2 # transferring using FileWrapper, end state - def __init__(self, server, connection, maxrequestsize=65536, - maxpostsize=8<<20, blocksize=4096, config={}): + + outheaders: typing.Union[ + typing.Tuple[()], # unset + typing.Tuple[str, HeaderList], # headers + LiteralTrue # sent + ] + wsgihandler: typing.Optional[typing.Iterable[bytes]] + wsgiiterator: typing.Optional[typing.Iterator[bytes]] + + def __init__( + self, + server: "SCGIServer", + connection: socket.socket, + maxrequestsize: int = 65536, + maxpostsize: int = 8<<20, + blocksize: int = 4096, + config: Environ = {}, + ): asyncore.dispatcher.__init__(self, connection) - self.server = server # WSGISCGIServer instance + self.server = server self.maxrequestsize = maxrequestsize self.maxpostsize = maxpostsize self.blocksize = blocksize @@ -35,10 +65,9 @@ class SCGIConnection(asyncore.dispatcher): self.wsgihandler = None # wsgi application self.wsgiiterator = None # wsgi application iterator self.outheaders = () # headers to be sent - # () -> unset, (..,..) -> set, True -> sent self.body = io.BytesIO() # request body - def _try_send_headers(self): + def _try_send_headers(self) -> None: if self.outheaders != True: assert not self.outbuff status, headers = self.outheaders @@ -47,7 +76,7 @@ class SCGIConnection(asyncore.dispatcher): self.outbuff = str2bytes(headdata) self.outheaders = True - def _wsgi_write(self, data): + def _wsgi_write(self, data: bytes) -> None: assert self.state >= SCGIConnection.RESP assert self.state < SCGIConnection.TRANS assert isinstance(data, bytes) @@ -55,15 +84,15 @@ class SCGIConnection(asyncore.dispatcher): self._try_send_headers() self.outbuff += data - def readable(self): + def readable(self) -> bool: """C{asyncore} interface""" return self.state & 1 == 1 - def writable(self): + def writable(self) -> bool: """C{asyncore} interface""" return self.state & 2 == 2 - def handle_read(self): + def handle_read(self) -> None: """C{asyncore} interface""" data = self.recv(self.blocksize) self.inbuff += data @@ -122,6 +151,7 @@ class SCGIConnection(asyncore.dispatcher): self.environ["wsgi.errors"] = self.server.error self.wsgihandler = self.server.wsgiapp(self.environ, self.start_response) + assert self.wsgihandler is not None if isinstance(self.wsgihandler, FileWrapper) and \ self.wsgihandler.can_transfer(): self._try_send_headers() @@ -134,7 +164,12 @@ class SCGIConnection(asyncore.dispatcher): self.reqlen -= len(self.inbuff) self.inbuff = b"" - def start_response(self, status, headers, exc_info=None): + def start_response( + self, + status: str, + headers: HeaderList, + exc_info: OptExcInfo = None, + ) -> WriteCallback: assert isinstance(status, str) assert isinstance(headers, list) if exc_info: @@ -147,7 +182,7 @@ class SCGIConnection(asyncore.dispatcher): self.outheaders = (status, headers) return self._wsgi_write - def send_buff(self): + def send_buff(self) -> None: try: sentbytes = self.send(self.outbuff[:self.blocksize]) except socket.error: @@ -155,11 +190,12 @@ class SCGIConnection(asyncore.dispatcher): else: self.outbuff = self.outbuff[sentbytes:] - def handle_write(self): + def handle_write(self) -> None: """C{asyncore} interface""" if self.state == SCGIConnection.RESP: if len(self.outbuff) < self.blocksize: self._try_send_headers() + assert self.wsgiiterator is not None for data in self.wsgiiterator: assert isinstance(data, bytes) if data: @@ -171,23 +207,26 @@ class SCGIConnection(asyncore.dispatcher): self.send_buff() elif self.state == SCGIConnection.RESPH: assert len(self.outbuff) > 0 + assert isinstance(self.wsgihandler, FileWrapper) self.send_buff() if not self.outbuff: self.state = SCGIConnection.TRANS else: assert self.state == SCGIConnection.TRANS + assert isinstance(self.wsgihandler, FileWrapper) assert self.wsgihandler.can_transfer() sent = self.wsgihandler.transfer(self.socket, self.blocksize) if sent <= 0: self.close() - def close(self): + def close(self) -> None: # None doesn't have a close attribute if hasattr(self.wsgihandler, "close"): + assert self.wsgihandler is not None self.wsgihandler.close() asyncore.dispatcher.close(self) - def handle_close(self): + def handle_close(self) -> None: """C{asyncore} interface""" self.close() @@ -195,33 +234,36 @@ __all__.append("SCGIServer") class SCGIServer(asyncore.dispatcher): """SCGI Server for WSGI applications. It does not use multiple processes or multiple threads.""" - def __init__(self, wsgiapp, port, interface="localhost", error=sys.stderr, - maxrequestsize=None, maxpostsize=None, blocksize=None, - config={}, reusesocket=None): + + def __init__( + self, + wsgiapp: WsgiApp, + port: int, + interface: str = "localhost", + error: typing.TextIO = sys.stderr, + maxrequestsize: typing.Optional[int] = None, + maxpostsize: typing.Optional[int] = None, + blocksize: typing.Optional[int] = None, + config: Environ = {}, + reusesocket: typing.Optional[socket.socket] = None + ): """ @param wsgiapp: is the wsgi application to be run. - @type port: int @param port: is an int representing the TCP port number to be used. - @type interface: str @param interface: is a string specifying the network interface to bind which defaults to C{"localhost"} making the server inaccessible over network. @param error: is a file-like object being passed as C{wsgi.error} in the environ parameter defaulting to stderr. - @type maxrequestsize: int @param maxrequestsize: limit the size of request blocks in scgi connections. Connections are dropped when this limit is hit. - @type maxpostsize: int @param maxpostsize: limit the size of post bodies that may be processed by this instance. Connections are dropped when this limit is hit. - @type blocksize: int @param blocksize: is amount of data to read or write from or to the network at once - @type config: {} @param config: the environ dictionary is updated using these values for each request. - @type reusesocket: None or socket.socket @param reusesocket: If a socket is passed, do not create a socket. Instead use given socket as listen socket. The passed socket must be set up for accepting tcp connections (i.e. C{AF_INET}, @@ -234,7 +276,7 @@ class SCGIServer(asyncore.dispatcher): self.wsgiapp = wsgiapp self.error = error - self.conf = {} + self.conf: Environ = {} if maxrequestsize is not None: self.conf["maxrequestsize"] = maxrequestsize if maxpostsize is not None: @@ -251,7 +293,7 @@ class SCGIServer(asyncore.dispatcher): else: self.accepting = True - def handle_accept(self): + def handle_accept(self) -> None: """asyncore interface""" try: ret = self.accept() @@ -264,7 +306,7 @@ class SCGIServer(asyncore.dispatcher): conn, _ = ret SCGIConnection(self, conn, **self.conf) - def run(self): + def run(self) -> None: """Runs the server. It will not return and you can invoke C{asyncore.loop()} instead achieving the same effect.""" asyncore.loop() -- cgit v1.2.3