Repository: cherrypy/cheroot (language: python)

Issue:
Add type hints
❓ **I'm submitting a ...**
- [ ] 🐞 bug report
- [X] 🐣 feature request
- [ ] ❓ question about the decisions made in the repository

🐣 **Describe the solution you'd like**
It would be great to see the python 3.5+ type hints in the repository, especially for the ``Server`` api.

📋 **Describe alternatives you've considered**
Adding type stubs to typeshed (https://github.com/python/typeshed/tree/master/stubs).
If you think this makes more sense, that's possible. But I first wanted to ask here.



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 29bb337ef9..027fba1bca 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -85,6 +85,38 @@ repos:
   hooks:
   - id: forbid-html-img-without-alt-text
 
+- repo: https://github.com/pre-commit/mirrors-mypy
+  rev: v0.910-1
+  hooks:
+  - id: mypy
+    name: MyPy, for Python 3.5
+    additional_dependencies:
+    - types-backports
+    - types-pyOpenSSL
+    - types-requests
+    - types-six
+    args:
+    - --install-types
+    - --non-interactive
+    - --python-version=3.5
+    - cheroot/
+    pass_filenames: false
+  - id: mypy
+    name: MyPy, for Python 2.7
+    additional_dependencies:
+    - typed-ast
+    - types-backports
+    - types-pyOpenSSL
+    - types-requests
+    - types-six
+    - typing
+    args:
+    - --install-types
+    - --non-interactive
+    - --python-version=2.7
+    - cheroot/
+    pass_filenames: false
+
 - repo: https://github.com/PyCQA/pylint.git
   rev: v2.11.1
   hooks:
diff --git a/cheroot/__init__.pyi b/cheroot/__init__.pyi
new file mode 100644
index 0000000000..bda5b5a7f4
--- /dev/null
+++ b/cheroot/__init__.pyi
@@ -0,0 +1,1 @@
+__version__: str
diff --git a/cheroot/cli.pyi b/cheroot/cli.pyi
new file mode 100644
index 0000000000..b9803b3e0a
--- /dev/null
+++ b/cheroot/cli.pyi
@@ -0,0 +1,32 @@
+from typing import Any
+
+class BindLocation: ...
+
+class TCPSocket(BindLocation):
+    bind_addr: Any
+    def __init__(self, address, port) -> None: ...
+
+class UnixSocket(BindLocation):
+    bind_addr: Any
+    def __init__(self, path) -> None: ...
+
+class AbstractSocket(BindLocation):
+    bind_addr: Any
+    def __init__(self, abstract_socket) -> None: ...
+
+class Application:
+    @classmethod
+    def resolve(cls, full_path): ...
+    wsgi_app: Any
+    def __init__(self, wsgi_app) -> None: ...
+    def server_args(self, parsed_args): ...
+    def server(self, parsed_args): ...
+
+class GatewayYo:
+    gateway: Any
+    def __init__(self, gateway) -> None: ...
+    def server(self, parsed_args): ...
+
+def parse_wsgi_bind_location(bind_addr_string: str): ...
+def parse_wsgi_bind_addr(bind_addr_string: str): ...
+def main() -> None: ...
diff --git a/cheroot/connections.pyi b/cheroot/connections.pyi
new file mode 100644
index 0000000000..528ad76519
--- /dev/null
+++ b/cheroot/connections.pyi
@@ -0,0 +1,23 @@
+from typing import Any
+
+def prevent_socket_inheritance(sock) -> None: ...
+
+class _ThreadsafeSelector:
+    def __init__(self) -> None: ...
+    def __len__(self): ...
+    @property
+    def connections(self) -> None: ...
+    def register(self, fileobj, events, data: Any | None = ...): ...
+    def unregister(self, fileobj): ...
+    def select(self, timeout: Any | None = ...): ...
+    def close(self) -> None: ...
+
+class ConnectionManager:
+    server: Any
+    def __init__(self, server) -> None: ...
+    def put(self, conn) -> None: ...
+    def stop(self) -> None: ...
+    def run(self, expiration_interval) -> None: ...
+    def close(self) -> None: ...
+    @property
+    def can_add_keepalive_connection(self): ...
diff --git a/cheroot/errors.pyi b/cheroot/errors.pyi
new file mode 100644
index 0000000000..e78a758523
--- /dev/null
+++ b/cheroot/errors.pyi
@@ -0,0 +1,13 @@
+from typing import Any, List, Set, Tuple
+
+class MaxSizeExceeded(Exception): ...
+class NoSSLError(Exception): ...
+class FatalSSLAlert(Exception): ...
+
+def plat_specific_errors(*errnames: str) -> List[int]: ...
+
+socket_error_eintr: List[int]
+socket_errors_to_ignore: List[int]
+socket_errors_nonblocking: List[int]
+acceptable_sock_shutdown_error_codes: Set[int]
+acceptable_sock_shutdown_exceptions: Tuple[Exception]
diff --git a/cheroot/makefile.pyi b/cheroot/makefile.pyi
new file mode 100644
index 0000000000..1174850597
--- /dev/null
+++ b/cheroot/makefile.pyi
@@ -0,0 +1,32 @@
+import io
+
+SOCK_WRITE_BLOCKSIZE: int
+
+class BufferedWriter(io.BufferedWriter):
+    def write(self, b): ...
+
+class MakeFile_PY2:
+    bytes_read: int
+    bytes_written: int
+    def __init__(self, *args, **kwargs) -> None: ...
+    def write(self, data) -> None: ...
+    def send(self, data): ...
+    def flush(self) -> None: ...
+    def recv(self, size): ...
+    class FauxSocket: ...
+    def read(self, size: int = ...): ...
+    def readline(self, size: int = ...): ...
+    def has_data(self): ...
+
+class StreamReader(io.BufferedReader):
+    bytes_read: int
+    def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
+    def read(self, *args, **kwargs): ...
+    def has_data(self): ...
+
+class StreamWriter(BufferedWriter):
+    bytes_written: int
+    def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
+    def write(self, val, *args, **kwargs): ...
+
+def MakeFile(sock, mode: str = ..., bufsize=...): ...
diff --git a/cheroot/server.pyi b/cheroot/server.pyi
new file mode 100644
index 0000000000..864adff4ef
--- /dev/null
+++ b/cheroot/server.pyi
@@ -0,0 +1,172 @@
+from typing import Any
+
+class HeaderReader:
+    def __call__(self, rfile, hdict: Any | None = ...): ...
+
+class DropUnderscoreHeaderReader(HeaderReader): ...
+
+class SizeCheckWrapper:
+    rfile: Any
+    maxlen: Any
+    bytes_read: int
+    def __init__(self, rfile, maxlen) -> None: ...
+    def read(self, size: Any | None = ...): ...
+    def readline(self, size: Any | None = ...): ...
+    def readlines(self, sizehint: int = ...): ...
+    def close(self) -> None: ...
+    def __iter__(self): ...
+    def __next__(self): ...
+    next: Any
+
+class KnownLengthRFile:
+    rfile: Any
+    remaining: Any
+    def __init__(self, rfile, content_length) -> None: ...
+    def read(self, size: Any | None = ...): ...
+    def readline(self, size: Any | None = ...): ...
+    def readlines(self, sizehint: int = ...): ...
+    def close(self) -> None: ...
+    def __iter__(self): ...
+    def __next__(self): ...
+    next: Any
+
+class ChunkedRFile:
+    rfile: Any
+    maxlen: Any
+    bytes_read: int
+    buffer: Any
+    bufsize: Any
+    closed: bool
+    def __init__(self, rfile, maxlen, bufsize: int = ...) -> None: ...
+    def read(self, size: Any | None = ...): ...
+    def readline(self, size: Any | None = ...): ...
+    def readlines(self, sizehint: int = ...): ...
+    def read_trailer_lines(self) -> None: ...
+    def close(self) -> None: ...
+
+class HTTPRequest:
+    server: Any
+    conn: Any
+    inheaders: Any
+    outheaders: Any
+    ready: bool
+    close_connection: bool
+    chunked_write: bool
+    header_reader: Any
+    started_request: bool
+    scheme: bytes
+    response_protocol: str
+    status: str
+    sent_headers: bool
+    chunked_read: bool
+    proxy_mode: Any
+    strict_mode: Any
+    def __init__(self, server, conn, proxy_mode: bool = ..., strict_mode: bool = ...) -> None: ...
+    rfile: Any
+    def parse_request(self) -> None: ...
+    uri: Any
+    method: Any
+    authority: Any
+    path: Any
+    qs: Any
+    request_protocol: Any
+    def read_request_line(self): ...
+    def read_request_headers(self): ...
+    def respond(self) -> None: ...
+    def simple_response(self, status, msg: str = ...) -> None: ...
+    def ensure_headers_sent(self) -> None: ...
+    def write(self, chunk) -> None: ...
+    def send_headers(self) -> None: ...
+
+class HTTPConnection:
+    remote_addr: Any
+    remote_port: Any
+    ssl_env: Any
+    rbufsize: Any
+    wbufsize: Any
+    RequestHandlerClass: Any
+    peercreds_enabled: bool
+    peercreds_resolve_enabled: bool
+    last_used: Any
+    server: Any
+    socket: Any
+    rfile: Any
+    wfile: Any
+    requests_seen: int
+    def __init__(self, server, sock, makefile=...) -> None: ...
+    def communicate(self): ...
+    linger: bool
+    def close(self) -> None: ...
+    def get_peer_creds(self): ...
+    @property
+    def peer_pid(self): ...
+    @property
+    def peer_uid(self): ...
+    @property
+    def peer_gid(self): ...
+    def resolve_peer_creds(self): ...
+    @property
+    def peer_user(self): ...
+    @property
+    def peer_group(self): ...
+
+class HTTPServer:
+    gateway: Any
+    minthreads: Any
+    maxthreads: Any
+    server_name: Any
+    protocol: str
+    request_queue_size: int
+    shutdown_timeout: int
+    timeout: int
+    expiration_interval: float
+    version: Any
+    software: Any
+    ready: bool
+    max_request_header_size: int
+    max_request_body_size: int
+    nodelay: bool
+    ConnectionClass: Any
+    ssl_adapter: Any
+    peercreds_enabled: bool
+    peercreds_resolve_enabled: bool
+    keep_alive_conn_limit: int
+    requests: Any
+    def __init__(self, bind_addr, gateway, minthreads: int = ..., maxthreads: int = ..., server_name: Any | None = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ...) -> None: ...
+    stats: Any
+    def clear_stats(self): ...
+    def runtime(self): ...
+    @property
+    def bind_addr(self): ...
+    @bind_addr.setter
+    def bind_addr(self, value) -> None: ...
+    def safe_start(self) -> None: ...
+    socket: Any
+    def prepare(self) -> None: ...
+    def serve(self) -> None: ...
+    def start(self) -> None: ...
+    @property
+    def can_add_keepalive_connection(self): ...
+    def put_conn(self, conn) -> None: ...
+    def error_log(self, msg: str = ..., level: int = ..., traceback: bool = ...) -> None: ...
+    def bind(self, family, type, proto: int = ...): ...
+    def bind_unix_socket(self, bind_addr): ...
+    @staticmethod
+    def prepare_socket(bind_addr, family, type, proto, nodelay, ssl_adapter): ...
+    @staticmethod
+    def bind_socket(socket_, bind_addr): ...
+    @staticmethod
+    def resolve_real_bind_addr(socket_): ...
+    def process_conn(self, conn) -> None: ...
+    @property
+    def interrupt(self): ...
+    @interrupt.setter
+    def interrupt(self, interrupt) -> None: ...
+    def stop(self) -> None: ...
+
+class Gateway:
+    req: Any
+    def __init__(self, req) -> None: ...
+    def respond(self) -> None: ...
+
+def get_ssl_adapter_class(name: str = ...): ...
diff --git a/cheroot/ssl/__init__.pyi b/cheroot/ssl/__init__.pyi
new file mode 100644
index 0000000000..a98076606c
--- /dev/null
+++ b/cheroot/ssl/__init__.pyi
@@ -0,0 +1,19 @@
+from abc import abstractmethod
+from typing import Any
+
+class Adapter():
+    certificate: Any
+    private_key: Any
+    certificate_chain: Any
+    ciphers: Any
+    context: Any
+    @abstractmethod
+    def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...): ...
+    @abstractmethod
+    def bind(self, sock): ...
+    @abstractmethod
+    def wrap(self, sock): ...
+    @abstractmethod
+    def get_environ(self): ...
+    @abstractmethod
+    def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
diff --git a/cheroot/ssl/builtin.pyi b/cheroot/ssl/builtin.pyi
new file mode 100644
index 0000000000..fdc656e03d
--- /dev/null
+++ b/cheroot/ssl/builtin.pyi
@@ -0,0 +1,18 @@
+from typing import Any
+from . import Adapter
+
+generic_socket_error: OSError
+DEFAULT_BUFFER_SIZE: int
+
+class BuiltinSSLAdapter(Adapter):
+    CERT_KEY_TO_ENV: Any
+    CERT_KEY_TO_LDAP_CODE: Any
+    def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...) -> None: ...
+    @property
+    def context(self): ...
+    @context.setter
+    def context(self, context) -> None: ...
+    def bind(self, sock): ...
+    def wrap(self, sock): ...
+    def get_environ(self): ...
+    def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
diff --git a/cheroot/ssl/pyopenssl.pyi b/cheroot/ssl/pyopenssl.pyi
new file mode 100644
index 0000000000..d5b93471ab
--- /dev/null
+++ b/cheroot/ssl/pyopenssl.pyi
@@ -0,0 +1,30 @@
+from . import Adapter
+from ..makefile import StreamReader, StreamWriter
+from OpenSSL import SSL
+from typing import Any
+
+ssl_conn_type: SSL.Connection
+
+class SSLFileobjectMixin:
+    ssl_timeout: int
+    ssl_retry: float
+    def recv(self, size): ...
+    def readline(self, size: int = ...): ...
+    def sendall(self, *args, **kwargs): ...
+    def send(self, *args, **kwargs): ...
+
+class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): ...  # type:ignore
+class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): ...  # type:ignore
+
+class SSLConnectionProxyMeta:
+    def __new__(mcl, name, bases, nmspc): ...
+
+class SSLConnection():
+    def __init__(self, *args) -> None: ...
+
+class pyOpenSSLAdapter(Adapter):
+    def __init__(self, certificate, private_key, certificate_chain: Any | None = ..., ciphers: Any | None = ...) -> None: ...
+    def bind(self, sock): ...
+    def wrap(self, sock): ...
+    def get_environ(self): ...
+    def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...
diff --git a/cheroot/workers/__init__.pyi b/cheroot/workers/__init__.pyi
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cheroot/workers/threadpool.pyi b/cheroot/workers/threadpool.pyi
new file mode 100644
index 0000000000..201d39140b
--- /dev/null
+++ b/cheroot/workers/threadpool.pyi
@@ -0,0 +1,37 @@
+import threading
+from typing import Any
+
+class TrueyZero:
+    def __add__(self, other): ...
+    def __radd__(self, other): ...
+
+trueyzero: TrueyZero
+
+class WorkerThread(threading.Thread):
+    conn: Any
+    server: Any
+    ready: bool
+    requests_seen: int
+    bytes_read: int
+    bytes_written: int
+    start_time: Any
+    work_time: int
+    stats: Any
+    def __init__(self, server): ...
+    def run(self) -> None: ...
+
+class ThreadPool:
+    server: Any
+    min: Any
+    max: Any
+    get: Any
+    def __init__(self, server, min: int = ..., max: int = ..., accepted_queue_size: int = ..., accepted_queue_timeout: int = ...) -> None: ...
+    def start(self) -> None: ...
+    @property
+    def idle(self): ...
+    def put(self, obj) -> None: ...
+    def grow(self, amount) -> None: ...
+    def shrink(self, amount) -> None: ...
+    def stop(self, timeout: int = ...) -> None: ...
+    @property
+    def qsize(self) -> int: ...
diff --git a/cheroot/wsgi.pyi b/cheroot/wsgi.pyi
new file mode 100644
index 0000000000..b4851a3d4d
--- /dev/null
+++ b/cheroot/wsgi.pyi
@@ -0,0 +1,42 @@
+from . import server
+from typing import Any
+
+class Server(server.HTTPServer):
+    wsgi_version: Any
+    wsgi_app: Any
+    request_queue_size: Any
+    timeout: Any
+    shutdown_timeout: Any
+    requests: Any
+    def __init__(self, bind_addr, wsgi_app, numthreads: int = ..., server_name: Any | None = ..., max: int = ..., request_queue_size: int = ..., timeout: int = ..., shutdown_timeout: int = ..., accepted_queue_size: int = ..., accepted_queue_timeout: int = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ...) -> None: ...
+    @property
+    def numthreads(self): ...
+    @numthreads.setter
+    def numthreads(self, value) -> None: ...
+
+class Gateway(server.Gateway):
+    started_response: bool
+    env: Any
+    remaining_bytes_out: Any
+    def __init__(self, req) -> None: ...
+    @classmethod
+    def gateway_map(cls): ...
+    def get_environ(self) -> None: ...
+    def respond(self) -> None: ...
+    def start_response(self, status, headers, exc_info: Any | None = ...): ...
+    def write(self, chunk) -> None: ...
+
+class Gateway_10(Gateway):
+    version: Any
+    def get_environ(self): ...
+
+class Gateway_u0(Gateway_10):
+    version: Any
+    def get_environ(self): ...
+
+wsgi_gateways: Any
+
+class PathInfoDispatcher:
+    apps: Any
+    def __init__(self, apps): ...
+    def __call__(self, environ, start_response): ...
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000000..2e0be78a1c
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,9 @@
+[mypy]
+pretty = true
+show_column_numbers = true
+show_error_codes = true
+warn_no_return = true
+warn_redundant_casts = true
+warn_unused_configs = true
+ignore_missing_imports = true
+exclude = cheroot/test|cheroot/_compat


Test patch:
diff --git a/cheroot/testing.pyi b/cheroot/testing.pyi
new file mode 100644
index 0000000000..4c825f9867
--- /dev/null
+++ b/cheroot/testing.pyi
@@ -0,0 +1,17 @@
+from typing import Any, Iterator, Optional, TypeVar
+
+from .server import HTTPServer
+from .wsgi import Server
+
+T = TypeVar('T', bound=HTTPServer)
+
+EPHEMERAL_PORT: int
+NO_INTERFACE: Optional[str]
+ANY_INTERFACE_IPV4: str
+ANY_INTERFACE_IPV6: str
+config: dict
+
+def cheroot_server(server_factory: T) -> Iterator[T]: ...
+def wsgi_server() -> Iterator[Server]: ...
+def native_server() -> Iterator[HTTPServer]: ...
+def get_server_client(server) -> Any: ...