diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20c9145..c55bc69 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,24 @@ on: - master jobs: + typecheck: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + fetch-depth: 50 + submodules: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: "3.8" + + - name: Type check + run: make typecheck + + build: runs-on: ${{ matrix.os }} strategy: diff --git a/Makefile b/Makefile index 9a2596a..2b8df0c 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,14 @@ else endif compile: - $(PIP) install -e . + $(PIP) install -e . --group dev test: compile $(PYTHON) -m unittest -v +typecheck: compile + $(PYTHON) -m pyright + clean: find $(ROOT)/httptools/parser -name '*.c' | xargs rm -f find $(ROOT)/httptools/parser -name '*.so' | xargs rm -f diff --git a/httptools/__init__.py b/httptools/__init__.py index 972053e..8b7b327 100644 --- a/httptools/__init__.py +++ b/httptools/__init__.py @@ -1,6 +1,35 @@ from . import parser -from .parser import * # NOQA +from .parser import ( + HTTPProtocol, + HttpRequestParser, + HttpResponseParser, + HttpParserError, + HttpParserCallbackError, + HttpParserInvalidStatusError, + HttpParserInvalidMethodError, + HttpParserInvalidURLError, + HttpParserUpgrade, + parse_url, +) -from ._version import __version__ # NOQA +from ._version import __version__ -__all__ = parser.__all__ + ('__version__',) # NOQA +__all__ = ( + "parser", + # protocol + "HTTPProtocol", + # parser + "HttpRequestParser", + "HttpResponseParser", + # errors + "HttpParserError", + "HttpParserCallbackError", + "HttpParserInvalidStatusError", + "HttpParserInvalidMethodError", + "HttpParserInvalidURLError", + "HttpParserUpgrade", + # url parser + "parse_url", + # version + "__version__", +) diff --git a/httptools/parser/__init__.py b/httptools/parser/__init__.py index 1d8df43..6f57517 100644 --- a/httptools/parser/__init__.py +++ b/httptools/parser/__init__.py @@ -1,6 +1,28 @@ from .protocol import HTTPProtocol -from .parser import * # NoQA -from .errors import * # NoQA -from .url_parser import * # NoQA +from .parser import HttpRequestParser, HttpResponseParser # NoQA +from .errors import ( + HttpParserError, + HttpParserCallbackError, + HttpParserInvalidStatusError, + HttpParserInvalidMethodError, + HttpParserInvalidURLError, + HttpParserUpgrade, +) +from .url_parser import parse_url -__all__ = parser.__all__ + errors.__all__ + url_parser.__all__ # NoQA +__all__ = ( + # protocol + "HTTPProtocol", + # parser + "HttpRequestParser", + "HttpResponseParser", + # errors + "HttpParserError", + "HttpParserCallbackError", + "HttpParserInvalidStatusError", + "HttpParserInvalidMethodError", + "HttpParserInvalidURLError", + "HttpParserUpgrade", + # url_parser + "parse_url", +) diff --git a/httptools/parser/parser.pyi b/httptools/parser/parser.pyi index 8a714bf..8afd006 100644 --- a/httptools/parser/parser.pyi +++ b/httptools/parser/parser.pyi @@ -1,43 +1,44 @@ -from typing import Union, Any from array import array from .protocol import HTTPProtocol class HttpParser: - def __init__(self, protocol: Union[HTTPProtocol, Any]) -> None: - """ - protocol -- a Python object with the following methods - (all optional): - - - on_message_begin() - - on_url(url: bytes) - - on_header(name: bytes, value: bytes) - - on_headers_complete() - - on_body(body: bytes) - - on_message_complete() - - on_chunk_header() - - on_chunk_complete() - - on_status(status: bytes) + def __init__(self, protocol: HTTPProtocol | object) -> None: + """The HTTP parser. + + Args: + protocol (HTTPProtocol): Callback interface for the parser. """ + def set_dangerous_leniencies( + self, + lenient_headers: bool | None = None, + lenient_chunked_length: bool | None = None, + lenient_keep_alive: bool | None = None, + lenient_transfer_encoding: bool | None = None, + lenient_version: bool | None = None, + lenient_data_after_close: bool | None = None, + lenient_optional_lf_after_cr: bool | None = None, + lenient_optional_cr_before_lf: bool | None = None, + lenient_optional_crlf_after_chunk: bool | None = None, + lenient_spaces_after_chunk_size: bool | None = None, + ) -> None: + """Set dangerous leniencies for the parser.""" + def get_http_version(self) -> str: - """Return an HTTP protocol version.""" - ... + """Retrieve the HTTP protocol version e.g. "1.1".""" def should_keep_alive(self) -> bool: - """Return ``True`` if keep-alive mode is preferred.""" - ... + """Return `True` if keep-alive mode is preferred.""" def should_upgrade(self) -> bool: - """Return ``True`` if the parsed request is a valid Upgrade request. + """Return `True` if the parsed request is a valid Upgrade request. The method exposes a flag set just before on_headers_complete. Calling this method earlier will only yield `False`.""" - ... - def feed_data(self, data: Union[bytes, bytearray, memoryview, array]) -> None: + def feed_data(self, data: bytes | bytearray | memoryview | array[int]) -> None: """Feed data to the parser. - Will eventually trigger callbacks on the ``protocol`` - object. + Will eventually trigger callbacks on the ``protocol`` object. On HTTP upgrade, this method will raise an ``HttpParserUpgrade`` exception, with its sole argument @@ -45,13 +46,13 @@ class HttpParser: """ class HttpRequestParser(HttpParser): - """Used for parsing http requests from the server's side""" + """Used for parsing http requests from the server side.""" def get_method(self) -> bytes: - """Return HTTP request method (GET, HEAD, etc)""" + """Retrieve the HTTP method of the request.""" class HttpResponseParser(HttpParser): - """Used for parsing http requests from the client's side""" + """Used for parsing http responses from the client side.""" def get_status_code(self) -> int: - """Return the status code of the HTTP response""" + """Retrieve the status code of the HTTP response.""" diff --git a/httptools/parser/protocol.py b/httptools/parser/protocol.py index c3b4234..ae00523 100644 --- a/httptools/parser/protocol.py +++ b/httptools/parser/protocol.py @@ -4,12 +4,12 @@ class HTTPProtocol(Protocol): """Used for providing static type-checking when parsing through the http protocol""" - def on_message_begin() -> None: ... - def on_url(url: bytes) -> None: ... - def on_header(name: bytes, value: bytes) -> None: ... - def on_headers_complete() -> None: ... - def on_body(body: bytes) -> None: ... - def on_message_complete() -> None: ... - def on_chunk_header() -> None: ... - def on_chunk_complete() -> None: ... - def on_status(status: bytes) -> None: ... + def on_message_begin(self) -> None: ... + def on_url(self, url: bytes) -> None: ... + def on_header(self, name: bytes, value: bytes) -> None: ... + def on_headers_complete(self) -> None: ... + def on_body(self, body: bytes) -> None: ... + def on_message_complete(self) -> None: ... + def on_chunk_header(self) -> None: ... + def on_chunk_complete(self) -> None: ... + def on_status(self, status: bytes) -> None: ... diff --git a/httptools/parser/url_parser.pyi b/httptools/parser/url_parser.pyi index f3d3488..5f04847 100644 --- a/httptools/parser/url_parser.pyi +++ b/httptools/parser/url_parser.pyi @@ -1,4 +1,3 @@ -from typing import Union from array import array class URL: @@ -10,18 +9,5 @@ class URL: fragment: bytes userinfo: bytes -def parse_url(url: Union[bytes, bytearray, memoryview, array]) -> URL: - """Parse URL strings into a structured Python object. - - Returns an instance of ``httptools.URL`` class with the - following attributes: - - - schema: bytes - - host: bytes - - port: int - - path: bytes - - query: bytes - - fragment: bytes - - userinfo: bytes - """ - ... +def parse_url(url: bytes | bytearray | memoryview | array[int]) -> URL: + """Parse a URL string into a structured Python object.""" diff --git a/httptools/py.typed b/httptools/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 659db31..bd0f5d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,5 +25,23 @@ readme = "README.md" [project.urls] Homepage = "https://github.com/MagicStack/httptools" -[project.optional-dependencies] -test = [] # for backward compatibility +[dependency-groups] +dev = [ + # type checker + "pyright >= 1.1.406", + # tests + "pytest", + # build + "setuptools", + "wheel" +] + +[tool.pyright] +pythonVersion = "3.8" +typeCheckingMode = "strict" +reportMissingTypeStubs = false +reportUnnecessaryIsInstance = false +reportUnnecessaryTypeIgnoreComment = true +reportMissingModuleSource = false +include = ["httptools"] +exclude = ["tests"]