From 68057982f6ca5503fdf763a8b79f77a00081a37d Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 19 Oct 2025 18:08:48 +0200 Subject: [PATCH 1/7] Fix all typing issues --- httptools/parser/__init__.py | 31 +++++++++++++++++++--- httptools/parser/parser.pyi | 46 +++++++++++++++------------------ httptools/parser/protocol.py | 18 ++++++------- httptools/parser/url_parser.pyi | 15 +++++------ vendor/llhttp | 2 +- 5 files changed, 65 insertions(+), 47 deletions(-) diff --git a/httptools/parser/__init__.py b/httptools/parser/__init__.py index 1d8df43..157b980 100644 --- a/httptools/parser/__init__.py +++ b/httptools/parser/__init__.py @@ -1,6 +1,29 @@ from .protocol import HTTPProtocol -from .parser import * # NoQA -from .errors import * # NoQA -from .url_parser import * # NoQA +from .parser import HttpParser, 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 + "HttpParser", + "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..d9a980b 100644 --- a/httptools/parser/parser.pyi +++ b/httptools/parser/parser.pyi @@ -1,43 +1,39 @@ -from typing import Union, Any +from typing import Union 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) -> None: + """The HTTP parser. + + Args: + protocol(HTTPProtocol): a Python object with the following methods (all optional): + - on_message_begin(self): ... + - on_url(self, url: bytes): ... + - on_header(self, name: bytes, value: bytes): ... + - on_headers_complete(self): ... + - on_body(self, body: bytes): ... + - on_message_complete(self): ... + - on_chunk_header(self): ... + - on_chunk_complete(self): ... + - on_status(self, status: bytes): ... """ def get_http_version(self) -> str: """Return an HTTP protocol version.""" - ... def should_keep_alive(self) -> bool: """Return ``True`` if keep-alive mode is preferred.""" - ... def should_upgrade(self) -> bool: """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: Union[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 +41,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..22a6e97 100644 --- a/httptools/parser/url_parser.pyi +++ b/httptools/parser/url_parser.pyi @@ -10,18 +10,17 @@ class URL: fragment: bytes userinfo: bytes -def parse_url(url: Union[bytes, bytearray, memoryview, array]) -> URL: +def parse_url(url: Union[bytes, bytearray, memoryview, array[int]]) -> URL: """Parse URL strings into a structured Python object. Returns an instance of ``httptools.URL`` class with the following attributes: - - schema: bytes - - host: bytes + - schema(bytes): The schema of the URL. + - host(bytes): The host of the URL. - port: int - - path: bytes - - query: bytes - - fragment: bytes - - userinfo: bytes + - path(bytes): The path of the URL. + - query(bytes): The query of the URL. + - fragment(bytes): The fragment of the URL. + - userinfo(bytes): The userinfo of the URL. """ - ... diff --git a/vendor/llhttp b/vendor/llhttp index 86b83a5..610a87d 160000 --- a/vendor/llhttp +++ b/vendor/llhttp @@ -1 +1 @@ -Subproject commit 86b83a59786caebd581f38d613c64c9e8c52c79e +Subproject commit 610a87d755f6bae466cd871c2ba97574ccac5483 From 623b8feaebc71f145080fd686fc1b9284c035bbd Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 19 Oct 2025 18:21:02 +0200 Subject: [PATCH 2/7] push --- httptools/parser/url_parser.pyi | 14 +------------- vendor/llhttp | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/httptools/parser/url_parser.pyi b/httptools/parser/url_parser.pyi index 22a6e97..a87cab0 100644 --- a/httptools/parser/url_parser.pyi +++ b/httptools/parser/url_parser.pyi @@ -11,16 +11,4 @@ class URL: userinfo: bytes def parse_url(url: Union[bytes, bytearray, memoryview, array[int]]) -> URL: - """Parse URL strings into a structured Python object. - - Returns an instance of ``httptools.URL`` class with the - following attributes: - - - schema(bytes): The schema of the URL. - - host(bytes): The host of the URL. - - port: int - - path(bytes): The path of the URL. - - query(bytes): The query of the URL. - - fragment(bytes): The fragment of the URL. - - userinfo(bytes): The userinfo of the URL. - """ + """Parse a URL string into a structured Python object.""" diff --git a/vendor/llhttp b/vendor/llhttp index 610a87d..86b83a5 160000 --- a/vendor/llhttp +++ b/vendor/llhttp @@ -1 +1 @@ -Subproject commit 610a87d755f6bae466cd871c2ba97574ccac5483 +Subproject commit 86b83a59786caebd581f38d613c64c9e8c52c79e From 8f1af0461278f038279a91d2da7bbd2e6d60a0ad Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 19 Oct 2025 18:36:02 +0200 Subject: [PATCH 3/7] less permissive --- httptools/parser/parser.pyi | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/httptools/parser/parser.pyi b/httptools/parser/parser.pyi index d9a980b..c9721b4 100644 --- a/httptools/parser/parser.pyi +++ b/httptools/parser/parser.pyi @@ -3,30 +3,36 @@ from array import array from .protocol import HTTPProtocol class HttpParser: - def __init__(self, protocol: HTTPProtocol) -> None: + def __init__(self, protocol: HTTPProtocol | object) -> None: """The HTTP parser. Args: - protocol(HTTPProtocol): a Python object with the following methods (all optional): - - on_message_begin(self): ... - - on_url(self, url: bytes): ... - - on_header(self, name: bytes, value: bytes): ... - - on_headers_complete(self): ... - - on_body(self, body: bytes): ... - - on_message_complete(self): ... - - on_chunk_header(self): ... - - on_chunk_complete(self): ... - - on_status(self, status: bytes): ... + 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`.""" From dc27351f4e242a220029b658c6dd25899d947188 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 19 Oct 2025 18:48:01 +0200 Subject: [PATCH 4/7] Add py.typed --- httptools/parser/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 httptools/parser/py.typed diff --git a/httptools/parser/py.typed b/httptools/parser/py.typed new file mode 100644 index 0000000..e69de29 From caa1b515e4e945544c047cf6ff50a9e0a5bd8af5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 19 Oct 2025 18:51:04 +0200 Subject: [PATCH 5/7] drop unneeded Union --- httptools/parser/parser.pyi | 3 +-- httptools/parser/url_parser.pyi | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/httptools/parser/parser.pyi b/httptools/parser/parser.pyi index c9721b4..8afd006 100644 --- a/httptools/parser/parser.pyi +++ b/httptools/parser/parser.pyi @@ -1,4 +1,3 @@ -from typing import Union from array import array from .protocol import HTTPProtocol @@ -36,7 +35,7 @@ class HttpParser: 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[int]]) -> 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. diff --git a/httptools/parser/url_parser.pyi b/httptools/parser/url_parser.pyi index a87cab0..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,5 +9,5 @@ class URL: fragment: bytes userinfo: bytes -def parse_url(url: Union[bytes, bytearray, memoryview, array[int]]) -> URL: +def parse_url(url: bytes | bytearray | memoryview | array[int]) -> URL: """Parse a URL string into a structured Python object.""" From ab8953987fb9c5636348c875b2362a87dd94a058 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 19 Oct 2025 18:54:26 +0200 Subject: [PATCH 6/7] move py.typed up --- httptools/{parser => }/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename httptools/{parser => }/py.typed (100%) diff --git a/httptools/parser/py.typed b/httptools/py.typed similarity index 100% rename from httptools/parser/py.typed rename to httptools/py.typed From c5e1550a68b8eb8782ceebfd5d851412a53c9406 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 20 Oct 2025 10:26:09 +0200 Subject: [PATCH 7/7] Push CI --- .github/workflows/tests.yml | 18 ++++++++++++++++++ Makefile | 5 ++++- httptools/__init__.py | 35 ++++++++++++++++++++++++++++++++--- httptools/parser/__init__.py | 3 +-- pyproject.toml | 22 ++++++++++++++++++++-- 5 files changed, 75 insertions(+), 8 deletions(-) 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 157b980..6f57517 100644 --- a/httptools/parser/__init__.py +++ b/httptools/parser/__init__.py @@ -1,5 +1,5 @@ from .protocol import HTTPProtocol -from .parser import HttpParser, HttpRequestParser, HttpResponseParser # NoQA +from .parser import HttpRequestParser, HttpResponseParser # NoQA from .errors import ( HttpParserError, HttpParserCallbackError, @@ -14,7 +14,6 @@ # protocol "HTTPProtocol", # parser - "HttpParser", "HttpRequestParser", "HttpResponseParser", # errors 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"]