From b3052f299b1db19ca01d4dbd360288f353506082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87a=C4=9F=C4=B1l=20Ulu=C5=9Fahin?= Date: Mon, 9 Nov 2026 15:35:59 +0000 Subject: [PATCH] Refactor _parsers.py as a module --- src/hip/_body.py | 2 +- src/hip/_server.py | 2 +- src/hip/{parsers.py => parsers/__init__.py} | 2 +- src/hip/parsers/exceptions.py | 2 + src/hip/{_parsers.py => parsers/http.py} | 87 +-------------------- src/hip/parsers/readahead.py | 81 +++++++++++++++++++ src/hip/transports/connection.py | 2 +- 7 files changed, 90 insertions(+), 88 deletions(-) rename src/hip/{parsers.py => parsers/__init__.py} (52%) create mode 100644 src/hip/parsers/exceptions.py rename src/hip/{_parsers.py => parsers/http.py} (84%) create mode 100644 src/hip/parsers/readahead.py diff --git a/src/hip/_body.py b/src/hip/_body.py index 5b1350d..5682ef0 100644 --- a/src/hip/_body.py +++ b/src/hip/_body.py @@ -1,8 +1,8 @@ import io import zlib -from ._parsers import HTTPParser from ._streams import Stream +from .parsers import HTTPParser __all__ = ["GZipStream", "HTTPStream"] diff --git a/src/hip/_server.py b/src/hip/_server.py index baf09d7..e491402 100644 --- a/src/hip/_server.py +++ b/src/hip/_server.py @@ -4,11 +4,11 @@ from ._body import HTTPStream from ._content import Text from ._headers import Headers -from ._parsers import HTTPParser from ._request import Request from ._response import Response from ._timeouts import sleep from .backends.sync import NetworkBackend +from .parsers import HTTPParser __all__ = ["Server"] diff --git a/src/hip/parsers.py b/src/hip/parsers/__init__.py similarity index 52% rename from src/hip/parsers.py rename to src/hip/parsers/__init__.py index 9b54ee0..9b403da 100644 --- a/src/hip/parsers.py +++ b/src/hip/parsers/__init__.py @@ -1,4 +1,4 @@ -from ._parsers import HTTPParser, ProtocolError +from .http import HTTPParser, ProtocolError __all__ = [ "HTTPParser", diff --git a/src/hip/parsers/exceptions.py b/src/hip/parsers/exceptions.py new file mode 100644 index 0000000..339578b --- /dev/null +++ b/src/hip/parsers/exceptions.py @@ -0,0 +1,2 @@ +class ProtocolError(Exception): + pass diff --git a/src/hip/_parsers.py b/src/hip/parsers/http.py similarity index 84% rename from src/hip/_parsers.py rename to src/hip/parsers/http.py index 64391c0..c413c26 100644 --- a/src/hip/_parsers.py +++ b/src/hip/parsers/http.py @@ -1,7 +1,9 @@ import enum import time -from ._streams import Stream +from .._streams import Stream +from .exceptions import ProtocolError +from .readahead import ReadAheadParser __all__ = ["HTTPParser", "Mode", "ProtocolError"] @@ -46,10 +48,6 @@ class Mode(enum.Enum): # 3. ... -class ProtocolError(Exception): - pass - - class HTTPParser: """ Usage... @@ -432,85 +430,6 @@ def __repr__(self) -> str: return f"" -class ReadAheadParser: - """ - A buffered I/O stream, with methods for read-ahead parsing. - """ - - def __init__(self, stream: Stream) -> None: - self._buffer = b"" - self._stream = stream - self._chunk_size = 4096 - - def _read_some(self) -> bytes: - if self._buffer: - ret, self._buffer = self._buffer, b"" - return ret - return self._stream.read(self._chunk_size) - - def _push_back(self, buffer): - assert self._buffer == b"" - self._buffer = buffer - - def read(self, size: int) -> bytes: - """ - Read and return up to 'size' bytes from the stream, with I/O buffering provided. - - * Returns b'' to indicate connection close. - """ - buffer = bytearray() - while len(buffer) < size: - chunk = self._read_some() - if not chunk: - break - buffer.extend(chunk) - - if len(buffer) > size: - buffer, push_back = buffer[:size], buffer[size:] - self._push_back(bytes(push_back)) - return bytes(buffer) - - def read_until( - self, - marker: bytes, - max_size: int, - exc_text: str, - allow_empty_eof: bool = False, - ) -> bytes: - """ - Read and return bytes from the stream, delimited by marker. - - * The marker is not included in the return bytes. - * The marker is consumed from the I/O stream. - * Raises `EOFError` if the stream closes before any bytes are read and - `allow_empty_eof` is set. - * Raises `ProtocolError` if the stream closes before the marker otherwise. - * Raises `ProtocolError` if marker did not occur within 'max_size + len(marker)' bytes. - """ - buffer = bytearray() - while len(buffer) <= max_size: - chunk = self._read_some() - if not chunk: - # EOF is only clean if no part of the line has arrived. - if allow_empty_eof and not buffer: - raise EOFError - # stream closed before marker found. - raise ProtocolError(f"Stream closed early {exc_text}.") - start_search = max(len(buffer) - len(marker), 0) - buffer.extend(chunk) - index = buffer.find(marker, start_search) - - if index > max_size: - # marker was found, though 'max_size' exceeded. - raise ProtocolError(f"Exceeded maximum size {exc_text}") - elif index >= 0: - endindex = index + len(marker) - self._push_back(bytes(buffer[endindex:])) - return bytes(buffer[:index]) - - raise ProtocolError(f"Exceeded maximum size {exc_text}") - - def bounded_int(intstr: bytes, max_digits: int, exc_text: str): if len(intstr) > max_digits: # Length of bytestring exceeds maximum. diff --git a/src/hip/parsers/readahead.py b/src/hip/parsers/readahead.py new file mode 100644 index 0000000..98fa902 --- /dev/null +++ b/src/hip/parsers/readahead.py @@ -0,0 +1,81 @@ +from .._streams import Stream +from .exceptions import ProtocolError + + +class ReadAheadParser: + """ + A buffered I/O stream, with methods for read-ahead parsing. + """ + + def __init__(self, stream: Stream) -> None: + self._buffer = b"" + self._stream = stream + self._chunk_size = 4096 + + def _read_some(self) -> bytes: + if self._buffer: + ret, self._buffer = self._buffer, b"" + return ret + return self._stream.read(self._chunk_size) + + def _push_back(self, buffer): + assert self._buffer == b"" + self._buffer = buffer + + def read(self, size: int) -> bytes: + """ + Read and return up to 'size' bytes from the stream, with I/O buffering provided. + + * Returns b'' to indicate connection close. + """ + buffer = bytearray() + while len(buffer) < size: + chunk = self._read_some() + if not chunk: + break + buffer.extend(chunk) + + if len(buffer) > size: + buffer, push_back = buffer[:size], buffer[size:] + self._push_back(bytes(push_back)) + return bytes(buffer) + + def read_until( + self, + marker: bytes, + max_size: int, + exc_text: str, + allow_empty_eof: bool = False, + ) -> bytes: + """ + Read and return bytes from the stream, delimited by marker. + + * The marker is not included in the return bytes. + * The marker is consumed from the I/O stream. + * Raises `EOFError` if the stream closes before any bytes are read and + `allow_empty_eof` is set. + * Raises `ProtocolError` if the stream closes before the marker otherwise. + * Raises `ProtocolError` if marker did not occur within 'max_size + len(marker)' bytes. + """ + buffer = bytearray() + while len(buffer) <= max_size: + chunk = self._read_some() + if not chunk: + # EOF is only clean if no part of the line has arrived. + if allow_empty_eof and not buffer: + raise EOFError + # stream closed before marker found. + raise ProtocolError(f"Stream closed early {exc_text}.") + start_search = max(len(buffer) - len(marker), 0) + buffer.extend(chunk) + index = buffer.find(marker, start_search) + + if index > max_size: + # marker was found, though 'max_size' exceeded. + raise ProtocolError(f"Exceeded maximum size {exc_text}") + elif index >= 0: + endindex = index + len(marker) + self._push_back(bytes(buffer[endindex:])) + return bytes(buffer[:index]) + + raise ProtocolError(f"Exceeded maximum size {exc_text}") diff --git a/src/hip/transports/connection.py b/src/hip/transports/connection.py index 58680f6..e645ff9 100644 --- a/src/hip/transports/connection.py +++ b/src/hip/transports/connection.py @@ -8,12 +8,12 @@ from .._body import GZipStream, HTTPStream from .._content import Content from .._headers import Headers -from .._parsers import HTTPParser from .._request import Method, Request from .._response import Response, StatusCode from .._streams import Stream from .._urls import URL from ..backends.sync import Lock, NetworkBackend +from ..parsers import HTTPParser from .base import SendContext, Transport __all__ = ["Connection", "open_connection"]