From f80577fa42fa36a95b1ed8e342ae13d41d888335 Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:54:00 +0200 Subject: [PATCH 01/40] build: drop support for Python 3.8 --- .github/workflows/ci.yml | 2 +- pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df370c1..3fb7eb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/pyproject.toml b/pyproject.toml index 40f427d..0fe8362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -39,7 +38,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] -requires-python = ">=3.8" +requires-python = ">=3.9" [project.optional-dependencies] xml = ["defusedxml"] From 0d30afa6f8093fa1b47dd19efcf2169edfc8acfb Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 02:55:36 +0200 Subject: [PATCH 02/40] build: use dependency groups --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fb7eb7..8f92df4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -Ue .[test] + pip install -Ue . --group test pip install -Ue . - name: Run tests diff --git a/pyproject.toml b/pyproject.toml index 0fe8362..bf8fa9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ requires-python = ">=3.9" [project.optional-dependencies] xml = ["defusedxml"] hal = ["uritemplate"] + +[dependency-groups] test = [ "pytest", "pytest-cov", From d5b7d21d951609f7debb6b3af1994926d65fd840 Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 02:56:02 +0200 Subject: [PATCH 03/40] ci(pyproject): add ty and mypy configuration --- .pre-commit-config.yaml | 5 +++++ Makefile | 25 +++++++++++++++++++++---- pyproject.toml | 15 +++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7a60d2..7490193 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,6 +54,11 @@ repos: - id: conventional-pre-commit stages: [commit-msg] args: ["--strict"] + - repo: https://github.com/astral-sh/ty-pre-commit + rev: v0.0.57 + hooks: + - id: ty + stages: [manual] - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.2.0 hooks: diff --git a/Makefile b/Makefile index f51821b..d8c5eae 100755 --- a/Makefile +++ b/Makefile @@ -1,13 +1,30 @@ #!/usr/bin/make -f +.PHONY: dist pushrelease pushreleasetest clean lint lint-fix format + dist: - python3 setup.py sdist bdist_wheel + python3 setup.py sdist bdist_wheel pushreleasetest: - python3 -m twine upload --repository testpypi dist/* + python3 -m twine upload --repository testpypi dist/* pushrelease: - python3 -m twine upload dist/* + python3 -m twine upload dist/* clean: - $(RM) -r dist + $(RM) -r dist + +lint: + -prek run -a + +lint-fix: + -prek run -a --hook-stage manual ruff-fix + +format: + -prek run -a --hook-stage manual ruff-format + +ty: + -prek run -a --hook-stage manual ty + +mypy: + -prek run -a --hook-stage manual mypy diff --git a/pyproject.toml b/pyproject.toml index bf8fa9d..8e1a93a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,21 @@ exclude_lines = [ "pragma: no cover", ] +[tool.ty.terminal] +output-format = "concise" + +[tool.ty.environment] +python-version = "3.8" + +[tool.mypy] +python_version = "3.10" +strict = true +show_error_codes = true +explicit_package_bases = true +namespace_packages = true +mypy_path = ["httoop"] +files = ["httoop/"] + [tool.ruff] line-length = 180 target-version = "py37" From 9adde07e4ecdcabc13fc087c238def3aa049eb4d Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:55:54 +0200 Subject: [PATCH 04/40] style: fix unsorted-dunder-all --- httoop/header/__init__.py | 2 +- httoop/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/httoop/header/__init__.py b/httoop/header/__init__.py index 5487969..05170e5 100644 --- a/httoop/header/__init__.py +++ b/httoop/header/__init__.py @@ -14,7 +14,7 @@ from httoop.header.messaging import Server, UserAgent -__all__ = ['Headers', 'Server', 'UserAgent', 'auth', 'cache', 'conditional', 'messaging', 'ranges', 'security', 'HeaderElement'] +__all__ = ['HeaderElement', 'Headers', 'Server', 'UserAgent', 'auth', 'cache', 'conditional', 'messaging', 'ranges', 'security'] for member in HEADER.values(): name = member.__name__ __all__.append(name) diff --git a/httoop/util.py b/httoop/util.py index a0de78f..e1bf5a7 100644 --- a/httoop/util.py +++ b/httoop/util.py @@ -18,7 +18,7 @@ def _(x: str) -> str: return x -__all__ = ['CaseInsensitiveDict', 'IFile', '_', 'integer', 'sanitize_encoding', 'to_unicode', 'base64', 'base64_decode', 'Base64Error'] +__all__ = ['Base64Error', 'CaseInsensitiveDict', 'IFile', '_', 'base64', 'base64_decode', 'integer', 'sanitize_encoding', 'to_unicode'] KNOWN_ENCODINGS = { 'cp1254', 'cp949', 'cp865', 'cp1257', 'euc_jp', 'cp1250', 'mac-cyrillic', 'mac-latin2', 'cp866', 'cp857', From b05a086f66feb62290241b01c63016c14a8ea2b3 Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:56:30 +0200 Subject: [PATCH 05/40] style: fix literal-membership --- httoop/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httoop/parser.py b/httoop/parser.py index 9f5f481..70a6aa8 100644 --- a/httoop/parser.py +++ b/httoop/parser.py @@ -177,7 +177,7 @@ def _parse_single_headers(self) -> None: rest += self.buffer[-len(self.line_end):] else: headers, _, rest = self.buffer.rpartition(self.line_end) - if headers and _ and rest[:1] not in (b'', b'\t', b' '): + if headers and _ and rest[:1] not in {b'', b'\t', b' '}: self.buffer = rest self._parse_header(headers) From 246e7c13e083c677ca90e25d31b24d3932e15e16 Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:57:07 +0200 Subject: [PATCH 06/40] style: fix escape-sequence-in-docstring --- httoop/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httoop/util.py b/httoop/util.py index e1bf5a7..088ee4e 100644 --- a/httoop/util.py +++ b/httoop/util.py @@ -76,7 +76,7 @@ def _decorated(self, *args, **kwargs): def integer(number: str | bytes, base=10) -> int: - """ + r""" The native Python integer parsing from string allows to many forms which are not allowed by the protocol. >>> integer(bytearray(b'10')) From bb44598705c989245e163850064d416dd495b39c Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:58:08 +0200 Subject: [PATCH 07/40] style: ignore invalid-first-argument-name-for-method --- httoop/codecs/application/hal_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httoop/codecs/application/hal_json.py b/httoop/codecs/application/hal_json.py index e89cd0d..1bd4c11 100644 --- a/httoop/codecs/application/hal_json.py +++ b/httoop/codecs/application/hal_json.py @@ -95,7 +95,7 @@ def get_resource(self, relation: str) -> Resource | None: except StopIteration: pass - def expand(___self, ___href, **templates): + def expand(___self, ___href, **templates): # noqa: N805 return expand(___href, templates) def get_curie(self, relation: str) -> str: From 2722dd605fdc521fe599ab5b49429ba4e40f016a Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:58:48 +0200 Subject: [PATCH 08/40] style: fix redundant-numeric-union --- httoop/client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httoop/client/__init__.py b/httoop/client/__init__.py index af4e4a2..36caf55 100644 --- a/httoop/client/__init__.py +++ b/httoop/client/__init__.py @@ -9,7 +9,7 @@ class ClientStateMachine(StateMachine): Message = Response - def __init__(self, *, strict: bool = True, max_status_line_length: int | float = 256) -> None: + def __init__(self, *, strict: bool = True, max_status_line_length: float = 256) -> None: super().__init__(strict=strict) self.max_status_line_length = max_status_line_length From 9df0ca4bde2e96cecfcf23d588ebb24c7364c920 Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:59:16 +0200 Subject: [PATCH 09/40] style: fix collapsible-if --- httoop/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/httoop/util.py b/httoop/util.py index 088ee4e..9ec46a0 100644 --- a/httoop/util.py +++ b/httoop/util.py @@ -124,9 +124,8 @@ def integer(number: str | bytes, base=10) -> int: """ if isinstance(number, int): return int(number) - if not number.isdigit(): - if base != 16 or number.strip(string.hexdigits if isinstance(number, str) else string.hexdigits.encode('ASCII')): - raise ValueError() + if not number.isdigit() and (base != 16 or number.strip(string.hexdigits if isinstance(number, str) else string.hexdigits.encode('ASCII'))): + raise ValueError() zero = '0' if isinstance(number, str) else b'0' if number != zero and len(number.lstrip(zero)) != len(number): raise ValueError() From 83b8d73922ee014ada69cc339137a5813e8bf15a Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 13:59:57 +0200 Subject: [PATCH 10/40] style: fix if-else-block-instead-of-if-exp --- httoop/header/headers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/httoop/header/headers.py b/httoop/header/headers.py index 1de4c94..50218d4 100644 --- a/httoop/header/headers.py +++ b/httoop/header/headers.py @@ -24,10 +24,7 @@ def __init__(self, *args, **kwargs) -> None: @staticmethod def formatvalue(value: Any) -> bytes: - if isinstance(value, str): - val = HeaderElement.encode_rfc2047(value) - else: - val = bytes(value) + val = HeaderElement.encode_rfc2047(value) if isinstance(value, str) else bytes(value) if b'\r' in val or b'\n' in val: raise InvalidHeader(_('Invalid header value: %r'), val.decode('ISO8859-1')) return val From 487f6c280625f6e092f8dbc57b422c920c853ebf Mon Sep 17 00:00:00 2001 From: Space One Date: Sat, 11 Jul 2026 14:07:51 +0200 Subject: [PATCH 11/40] style: fix naming --- httoop/exceptions.py | 2 +- httoop/header/headers.py | 46 ++++++++++++++++++++-------------------- httoop/status/types.py | 2 +- httoop/uri/uri.py | 6 +++--- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/httoop/exceptions.py b/httoop/exceptions.py index 7221902..53eb47c 100644 --- a/httoop/exceptions.py +++ b/httoop/exceptions.py @@ -3,7 +3,7 @@ from httoop.util import _Translateable -class Invalid(_Translateable, ValueError): +class Invalid(_Translateable, ValueError): # noqa: N818 """base class for invalid values.""" diff --git a/httoop/header/headers.py b/httoop/header/headers.py index 50218d4..12ec03d 100644 --- a/httoop/header/headers.py +++ b/httoop/header/headers.py @@ -30,13 +30,13 @@ def formatvalue(value: Any) -> bytes: return val def __getitem__(self, key: str) -> str: - Element = HEADER.get(key, HeaderElement) - return Element.decode_rfc2047(super().__getitem__(key)) + element_cls = HEADER.get(key, HeaderElement) + return element_cls.decode_rfc2047(super().__getitem__(key)) def get(self, key: str, default: bytes | str | None = None) -> bytes | str | None: - Element = HEADER.get(key, HeaderElement) + element_cls = HEADER.get(key, HeaderElement) try: - return Element.decode_rfc2047(super().__getitem__(key)) + return element_cls.decode_rfc2047(super().__getitem__(key)) except KeyError: return default @@ -65,14 +65,14 @@ def elements(self, fieldname: bytes | str) -> list[Any]: if not fieldvalue: return [] - Element = HEADER.get(fieldname, HeaderElement) - return Element.sorted([Element.parse(element) for element in Element.split(fieldvalue)]) + element_cls = HEADER.get(fieldname, HeaderElement) + return element_cls.sorted([element_cls.parse(element) for element in element_cls.split(fieldvalue)]) def element(self, fieldname: bytes | str, default: None = None) -> Any: """Treat the field as single element.""" if fieldname in self: - Element = HEADER.get(fieldname, HeaderElement) - return Element.parse(super().__getitem__(fieldname)) + element_cls = HEADER.get(fieldname, HeaderElement) + return element_cls.parse(super().__getitem__(fieldname)) return default def get_element(self, fieldname: str, which: str | None = None, default: object | None = None) -> Any: @@ -88,8 +88,8 @@ def append_element(self, fieldname: str, *args, **kwargs) -> None: self.append(fieldname, bytes(self.create_element(fieldname, *args, **kwargs))) def create_element(self, fieldname: str, *args, **kwargs) -> HeaderElement: - Element = HEADER.get(fieldname, HeaderElement) - return Element(*args, **kwargs) + element_cls = HEADER.get(fieldname, HeaderElement) + return element_cls(*args, **kwargs) def values(self, *key) -> list[Any | str]: if not key: @@ -100,21 +100,21 @@ def values(self, *key) -> list[Any | str]: def append(self, _name: str, _value: bytes | str, **params) -> None: _value = self.formatvalue(_value) if params: - Element = HEADER.get(_name, HeaderElement) - parts = [_value or b''] + [Element.formatparam(k.encode(), v) for k, v in params.items()] + element_cls = HEADER.get(_name, HeaderElement) + parts = [_value or b''] + [element_cls.formatparam(k.encode(), v) for k, v in params.items()] _value = b'; '.join(parts) if _name not in self or not self[_name]: self[_name] = _value else: - Element = HEADER.get(_name, HeaderElement) - self[_name] = Element.join([super().__getitem__(_name), _value]) + element_cls = HEADER.get(_name, HeaderElement) + self[_name] = element_cls.join([super().__getitem__(_name), _value]) def merge(self, other: Headers) -> None: other = self.__class__(other) for key in other: - Element = HEADER.get(key, HeaderElement) - self[key] = Element.merge(self.elements(key), other.elements(key)) + element_cls = HEADER.get(key, HeaderElement) + self[key] = element_cls.merge(self.elements(key), other.elements(key)) def set(self, headers: dict[str, str]) -> None: self.clear() @@ -149,11 +149,11 @@ def parse(self, data: bytes) -> None: while lines and lines[0].startswith((b' ', b'\t')): value.append(lines.pop(0)[1:]) value = b''.join(value).rstrip() - Element = HEADER.get(name, HeaderElement) + element_cls = HEADER.get(name, HeaderElement) name = name.decode('ascii') if name in self: - value = Element.join([super().__getitem__(name), value]) + value = element_cls.join([super().__getitem__(name), value]) super().__setitem__(name, value) if len(self) >= self.max_header_count: raise InvalidHeaderSize(_('Maximum allowed header count (%d) reached.'), self.max_header_count) @@ -170,12 +170,12 @@ def __items(self): def __encoded_items(self): for key, values in self.items(): - Element = HEADER.get(key, HeaderElement) - if Element is not HeaderElement: - key = Element.name + element_cls = HEADER.get(key, HeaderElement) + if element_cls is not HeaderElement: + key = element_cls.name key = key.encode('ascii', 'ignore') - if Element.list_element: - for value in Element.split(values): + if element_cls.list_element: + for value in element_cls.split(values): yield key, value else: yield key, values diff --git a/httoop/status/types.py b/httoop/status/types.py index 5169fcb..fbe2a99 100644 --- a/httoop/status/types.py +++ b/httoop/status/types.py @@ -9,7 +9,7 @@ from httoop.status.status import Status -class StatusException(Status, Exception): +class StatusException(Status, Exception): # noqa: N818 """ This class represents a small HTTP Response message for error handling purposes diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index c957217..013ade0 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -324,10 +324,10 @@ def _compose_authority_iter(self) -> Iterator[bytes]: def _compose_relative_iter(self) -> Iterator[bytes]: """Composes the relative URI beginning with the path.""" scheme, path, query_string, quote, fragment = self.scheme, self.path, self.query_string, self.quote, self.fragment - PATH = Percent.PATH + path_quote_chars = Percent.PATH if not scheme and not path.startswith('/'): - PATH = b''.join({bytes((c,)) for c in iter(PATH)} - {b':', b'@'}) - yield b'/'.join(quote(x, PATH) for x in path.split('/')) + path_quote_chars = b''.join({bytes((c,)) for c in iter(path_quote_chars)} - {b':', b'@'}) + yield b'/'.join(quote(x, path_quote_chars) for x in path.split('/')) if query_string: yield b'?' yield query_string.encode(self.encoding) From c96813a44f7d14c5ecb2ad90f3dc0eaab5c5a42d Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 01:06:22 +0200 Subject: [PATCH 12/40] style: --- httoop/header/cache.py | 2 +- tests/main.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/httoop/header/cache.py b/httoop/header/cache.py index 92550f0..36d248e 100644 --- a/httoop/header/cache.py +++ b/httoop/header/cache.py @@ -27,6 +27,6 @@ class Vary(HeaderElement): is_response_header = True -class Warning(HeaderElement): # pylint: disable=W0622 +class Warning(HeaderElement): # noqa: A001 is_response_header = True diff --git a/tests/main.py b/tests/main.py index 4746e82..b9541ed 100755 --- a/tests/main.py +++ b/tests/main.py @@ -18,9 +18,8 @@ def main(): cmd = ['py.test', '-r', 'fsxX', '--durations=1', '--ignore=tmp', '--color=yes', '--continue-on-collection-errors'] if importable('pytest_cov'): - cmd.append('--cov=httoop') - # cmd.append("--no-cov-on-fail") - cmd.append('--cov-report=html') + cmd.extend(('--cov=httoop', '--cov-report=html')) + # cmd.append("--no-cov-on-fail") cmd.append(pathlib.Path(pathlib.Path(__file__).resolve()).parent) From 702db249f3356ea9530c0d14946f480cc5f02c38 Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 01:07:27 +0200 Subject: [PATCH 13/40] style: fix unnecessary-lambda --- httoop/authentication/digest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/httoop/authentication/digest.py b/httoop/authentication/digest.py index f8bf0de..212778d 100644 --- a/httoop/authentication/digest.py +++ b/httoop/authentication/digest.py @@ -12,10 +12,10 @@ class DigestAuthScheme: algorithms = { - 'MD5': lambda: md5(), # noqa: S324 - 'MD5-sess': lambda: md5(), # noqa: S324 - 'SHA-256': lambda: sha256(), - 'SHA-256-sess': lambda: sha256(), + 'MD5': md5, # noqa: S324 + 'MD5-sess': md5, # noqa: S324 + 'SHA-256': sha256, + 'SHA-256-sess': sha256, 'SHA-512-256': lambda: new('sha512_256'), 'SHA-512-256-sess': lambda: new('sha512_256'), } # not case insensitive per RFC From e9190930b25533c15b9b48a14815a980f95d26a5 Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 01:09:14 +0200 Subject: [PATCH 14/40] style: fix various --- httoop/authentication/digest.py | 4 ++-- httoop/date.py | 15 ++++++++------- tests/api/test_header.py | 2 +- tests/authentication/test_digest.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/httoop/authentication/digest.py b/httoop/authentication/digest.py index 212778d..43988b6 100644 --- a/httoop/authentication/digest.py +++ b/httoop/authentication/digest.py @@ -12,8 +12,8 @@ class DigestAuthScheme: algorithms = { - 'MD5': md5, # noqa: S324 - 'MD5-sess': md5, # noqa: S324 + 'MD5': md5, + 'MD5-sess': md5, 'SHA-256': sha256, 'SHA-256-sess': sha256, 'SHA-512-256': lambda: new('sha512_256'), diff --git a/httoop/date.py b/httoop/date.py index 3157c17..8f4d0e3 100644 --- a/httoop/date.py +++ b/httoop/date.py @@ -11,7 +11,7 @@ # import calendar from datetime import datetime from email.utils import parsedate_tz -from typing import Any +from typing import Any, Self from httoop.exceptions import InvalidDate from httoop.meta import Semantic @@ -50,7 +50,7 @@ def __init__(self, timeval: Any | None = None) -> None: or a timetuple """ self.__composed = None - self.__timestamp = None + self.__timestamp: float = 0.0 self.__datetime = None self.__time_struct = None @@ -104,7 +104,7 @@ def __compose(self) -> bytes: ) @classmethod - def parse(cls, timestr: bytes) -> Date: + def parse(cls, data: bytes) -> Date: """ Parses a HTTP date string and returns a :class:`Date` object. @@ -115,7 +115,7 @@ def parse(cls, timestr: bytes) -> Date: :rtype : :class:`Date` """ - timestr = timestr.decode('ISO8859-1') + timestr = data.decode('ISO8859-1') # parse the most common HTTP Date formats (RFC 2822, RFC 1036, C's asctime) date = parsedate_tz(timestr) @@ -148,17 +148,18 @@ def __lt__(self, other: Date | str | None) -> bool: except NotImplementedError: # pragma: no cover return NotImplemented - def __other(self, other): + @staticmethod + def __other(other) -> Self: if other is None: return Date(0) - raise NotImplementedError() # pragma: no cover + # raise NotImplementedError() if isinstance(other, Date): return other try: return Date(other) except (InvalidDate, TypeError): return Date(0) - raise NotImplementedError() # pragma: no cover + # raise NotImplementedError() def __repr__(self) -> str: return '' % (int(self),) diff --git a/tests/api/test_header.py b/tests/api/test_header.py index bf967c0..d60cc87 100644 --- a/tests/api/test_header.py +++ b/tests/api/test_header.py @@ -104,6 +104,6 @@ def test_header_append_params(headers): def test_headers_composing_rejects_invalid_values(headers): - dict.__setitem__(headers, 'Location', b'Foo\r\nSet-Cookie: foo=bar') # noqa: PLC2801 + dict.__setitem__(headers, 'Location', b'Foo\r\nSet-Cookie: foo=bar') with pytest.raises(InvalidHeader): bytes(headers) diff --git a/tests/authentication/test_digest.py b/tests/authentication/test_digest.py index 0dabb31..e1f8577 100644 --- a/tests/authentication/test_digest.py +++ b/tests/authentication/test_digest.py @@ -85,7 +85,7 @@ def test_digest_authorization(headers): assert elem.username == 'Mufasa' assert elem.password is None elem.username = 'test' - elem.password = 'test' # noqa: S105 + elem.password = 'test' assert elem.username == 'test' assert elem.password is None From bdb36f145fa2c2e5e6431ad77b022d21dbf65423 Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 01:37:52 +0200 Subject: [PATCH 15/40] fixup! style: fix various --- httoop/__main__.py | 31 +++++++++++++++++++------------ httoop/parser.py | 32 +++++++++++++++++++++----------- httoop/util.py | 9 +++++---- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/httoop/__main__.py b/httoop/__main__.py index 64089b9..d7a2163 100644 --- a/httoop/__main__.py +++ b/httoop/__main__.py @@ -9,7 +9,8 @@ import pathlib import sys -from argparse import ArgumentParser, FileType +from argparse import ArgumentParser +from typing import IO from httoop import Request, Response, __name__ as name, __version__ as version from httoop.client import ClientStateMachine @@ -51,7 +52,7 @@ def add_subparsers(self) -> None: request = parse_message_subparsers.add_parser('request', parents=[self.parent_parser]) request.set_defaults(func=self.parse_request) add = request.add_argument - add('--file', default='-', type=FileType('rb')) + add('--file', default='-') add('--scheme', default='http') add('--host', default='www.example.net') add('--port', default=80, type=int) @@ -59,24 +60,27 @@ def add_subparsers(self) -> None: response = parse_message_subparsers.add_parser('response', parents=[self.parent_parser]) add = response.add_argument response.set_defaults(func=self.parse_response) - add('--file', default='-', type=FileType('rb')) + add('--file', default='-') def parse_arguments(self) -> None: self.arguments = self.parser.parse_args() - - if self.arguments.action == 'parse' and hasattr(self.arguments.file, 'buffer'): - # https://bugs.python.org/issue14156 - self.arguments.file = self.arguments.file.buffer self.arguments.func() - def add_common_arguments(self, add) -> None: + @classmethod + def add_common_arguments(cls, add) -> None: add('--protocol') add('-H', '--header', action='append', default=[]) add('-b', '--body', default='') + @classmethod + def get_file(cls, file: str) -> IO: + if file == '-': + return sys.stdin.buffer + return open(file, 'rb') + def parse_request(self) -> None: server = ServerStateMachine(self.arguments.scheme, self.arguments.host, self.arguments.port) - for _request, response in server.parse(self.arguments.file.read()): + for _request, response in server.parse(self.get_file(self.arguments.file)): print(repr(response)) print(repr(response.headers)) print(repr(response.body)) @@ -90,6 +94,7 @@ def parse_response(self) -> None: print(repr(response.body)) print(repr(bytes(response.body))) if client.buffer: + assert client.message print('WARNING: response not yet complete!:') print(repr(client.message)) print(repr(client.message.headers)) @@ -115,10 +120,11 @@ def compose_response(self) -> None: self.common() def common(self) -> None: + assert self.message is not None if self.arguments.protocol: protocol = self.arguments.protocol try: - protocol = [int(x) for x in protocol.split('.', 1)] + protocol = tuple(int(x) for x in protocol.split('.', 1)) except ValueError: pass else: @@ -133,14 +139,15 @@ def common(self) -> None: if body == '-': body = sys.stdin.read() elif body.startswith('@'): - body = pathlib.Path(body[1:]).open('rb') + body = pathlib.Path(body[1:]).open('rb') # noqa: SIM115 self.message.body = body sys.stdout.write(self.decode(bytes(self.message))) sys.stdout.write(self.decode(bytes(self.message.headers))) sys.stdout.write(self.decode(bytes(self.message.body))) - def decode(self, data): + @classmethod + def decode(cls, data): if str is not bytes: data = data.decode('ISO8859-1') return data diff --git a/httoop/parser.py b/httoop/parser.py index 70a6aa8..87ea46f 100644 --- a/httoop/parser.py +++ b/httoop/parser.py @@ -96,6 +96,7 @@ def on_body_complete(self) -> None: def on_message_complete(self) -> Response | Request: message = self.message + assert self.message self.message = None return message @@ -111,7 +112,7 @@ def parse(self, data: bytes) -> tuple[tuple[Request, Response]] | tuple[tuple[Re try: return tuple(x for x in self._parse() if x is not None) except (InvalidHeader, InvalidLine, InvalidURI, InvalidBody) as exc: - raise BAD_REQUEST(str(exc)) + raise BAD_REQUEST(str(exc)) from exc def _parse(self) -> Iterator[Response | tuple[Request, Response] | None]: while self.buffer: @@ -149,10 +150,11 @@ def parse_startline(self) -> bool | None: requestline, self.buffer = self.buffer.split(self.line_end, 1) # parse request line + assert self.message try: self.message.parse(bytes(requestline)) except (InvalidLine, InvalidURI) as exc: - raise BAD_REQUEST(str(exc)) + raise BAD_REQUEST(str(exc)) from exc def parse_headers(self) -> bool | None: # empty headers? @@ -187,11 +189,12 @@ def _parse_header(self, headers: bytearray) -> None: if self.state['_header_section_size'] >= self.max_header_section_size: raise REQUEST_HEADER_FIELDS_TOO_LARGE(_('Maximum allowed header section size (%d) reached.') % (self.max_header_section_size,)) try: + assert self.message self.message.headers.parse(bytes(headers)) except InvalidHeaderSize as exc: - raise REQUEST_HEADER_FIELDS_TOO_LARGE(str(exc)) + raise REQUEST_HEADER_FIELDS_TOO_LARGE(str(exc)) from exc except InvalidHeader as exc: - raise BAD_REQUEST(str(exc)) + raise BAD_REQUEST(str(exc)) from exc def parse_body(self) -> bool | None: if self.message_length is None and not self.chunked: @@ -207,6 +210,7 @@ def determine_message_length(self) -> None: # RFC 2616 Section 4.4 # get message length + assert self.message message = self.message if 'Transfer-Encoding' in message.headers and 'Content-Length' in message.headers: raise BAD_REQUEST(_('Invalid Content-Length and Transfer-Encoding combination header.')) @@ -223,16 +227,17 @@ def determine_message_length(self) -> None: try: self.message_length = integer(message.headers.get('Content-Length', '0')) except ValueError: - raise BAD_REQUEST(_('Invalid Content-Length header.')) + raise BAD_REQUEST(_('Invalid Content-Length header.')) from None if self.message_length > self.max_body_size: raise PAYLOAD_TOO_LARGE(_('Maximum content size (%d) reached') % (self.max_body_size,)) def parse_body_with_message_length(self) -> bool | None: + assert self.message body, self.buffer = self.buffer[:self.message_length], self.buffer[self.message_length:] try: self.message.body.parse(bytes(body)) except InvalidBodySize as exc: - raise PAYLOAD_TOO_LARGE(str(exc)) + raise PAYLOAD_TOO_LARGE(str(exc)) from exc blen = len(body) unfinished = blen < self.message_length @@ -244,6 +249,7 @@ def parse_body_with_message_length(self) -> bool | None: return None def parse_chunked_body(self) -> bool: + assert self.message if self.state['trailer']: return self.parse_trailers() if self.line_end not in self.buffer: @@ -260,7 +266,7 @@ def parse_chunked_body(self) -> bool: try: self.message.body.parse(bytes(body_part)) except InvalidBodySize as exc: - raise PAYLOAD_TOO_LARGE(str(exc)) + raise PAYLOAD_TOO_LARGE(str(exc)) from exc self.buffer = rest_chunk if chunk_size == 0: @@ -283,7 +289,7 @@ def __parse_chunk_size(self): raise ValueError() except (ValueError, OverflowError): exc = InvalidHeader(_('Invalid chunk size: %r'), chunk_size_.decode('ISO8859-1')) - raise BAD_REQUEST(str(exc)) + raise BAD_REQUEST(str(exc)) from exc else: return chunk_size, rest_chunk @@ -305,7 +311,7 @@ def parse_trailers(self) -> bool: self.trailers.parse(bytes(trailers)) except InvalidHeader as exc: exc = InvalidHeader(_('Invalid trailers: %r'), str(exc)) - raise BAD_REQUEST(str(exc)) + raise BAD_REQUEST(str(exc)) from exc for forbidden in Trailer.forbidden_headers: if forbidden in self.trailers: @@ -317,6 +323,7 @@ def parse_trailers(self) -> bool: def merge_trailer_into_header(self) -> None: message = self.message + assert self.message for name in message.headers.values('Trailer'): value = self.trailers.pop(name, None) if value is not None: @@ -327,18 +334,21 @@ def merge_trailer_into_header(self) -> None: del self.trailers def set_body_content_encoding(self) -> None: + assert self.message if 'Content-Encoding' in self.message.headers: try: self.message.body.content_encoding = self.message.headers.element('Content-Encoding') - self.message.body.content_encoding.codec # pylint: disable=W0104 + self.message.body.content_encoding.codec # noqa: B018 except Invalid as exc: - raise NOT_IMPLEMENTED(str(exc)) + raise NOT_IMPLEMENTED(str(exc)) from exc def set_body_content_type(self) -> None: + assert self.message if 'Content-Type' in self.message.headers: self.message.body.mimetype = self.message.headers.element('Content-Type') def set_content_length(self) -> None: + assert self.message if 'Content-Length' not in self.message.headers: self.message.headers['Content-Length'] = str(len(self.message.body)).encode('ASCII') if self.chunked: diff --git a/httoop/util.py b/httoop/util.py index 9ec46a0..89f6a34 100644 --- a/httoop/util.py +++ b/httoop/util.py @@ -5,7 +5,7 @@ import codecs import string from binascii import Error as Base64Error, a2b_base64, b2a_base64 -from typing import Any, Callable +from typing import IO, Any, Callable try: @@ -39,9 +39,9 @@ def base64(data: bytes): def base64_decode(data: bytes): try: - return a2b_base64(data, strict_mode=True) + return a2b_base64(data, strict_mode=True) # type: ignore[unknown-argument] except TypeError: # Py < 3.11 - import base64 + import base64 # noqa: PLC0415 return base64.b64decode(data, validate=True) @@ -68,7 +68,7 @@ def to_unicode(string: bytes | str | None) -> str: def if_has(func: Callable) -> Callable: def _decorated(self, *args, **kwargs): - if hasattr(self.fd, func.__name__): + if hasattr(self.fd, getattr(func, '__name__', '')): return func(self, *args, **kwargs) return False @@ -136,6 +136,7 @@ class IFile: """The file interface.""" __slots__ = ('fd',) + fd: IO @property def name(self) -> None: From 343e3daa355c72ffcd19f70e75c4f6e9b29d81aa Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 02:05:56 +0200 Subject: [PATCH 16/40] remove rules --- pyproject.toml | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8e1a93a..1a0cb02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,52 +158,6 @@ ignore = [ "PLC2801", # unnecessary-dunder-call "TRY003", # raise-vanilla-args "TRY300", # try-consider-else - -# should be fixed! - "A001", # builtin-variable-shadowing - "A003", # builtin-attribute-shadowing - "B015", # useless-comparison - "B018", # useless-expression - "B904", # raise-without-from-inside-except - "D301", # escape-sequence-in-docstring - "DTZ001", # call-datetime-without-tzinfo - "DTZ004", # call-datetime-utcfromtimestamp - "FBT001", # boolean-type-hint-positional-argument - "FBT002", # boolean-default-value-positional-argument - "FBT003", # boolean-positional-value-in-call - "FURB113", # repeated-append - "N802", # invalid-function-name - "N803", # invalid-argument-name - "N805", # invalid-first-argument-name-for-method - "N806", # non-lowercase-variable-in-function - "N818", # error-suffix-on-exception-name - "PERF203", # try-except-in-loop - "PLC0206", # dict-index-missing-items - "PLC0415", # import-outside-top-level - "PLC1901", # compare-to-empty-string - "PLR1704", # redefined-argument-from-local - "PLR2004", # magic-value-comparison - "PLR6201", # literal-membership - "PLR6301", # no-self-use - "PLW0108", # unnecessary-lambda - "PLW0717", # too-many-statements-in-try-clause - "PLW1641", # eq-without-hash - "PLW2901", # redefined-loop-name - "PT011", # pytest-raises-too-broad - "PT012", # pytest-raises-with-multiple-statements - "PYI041", # redundant-numeric-union - "PYI056", # unsupported-method-call-on-all - "RUF005", # collection-literal-concatenation - "RUF012", # mutable-class-default - "RUF022", # unsorted-dunder-all - "RUF039", # unraw-re-pattern - "RUF067", # non-empty-init-module - "RUF069", # float-equality-comparison - "SIM102", # collapsible-if - "SIM108", # if-else-block-instead-of-if-exp - "SIM115", # open-file-with-context-handler - "RUF100", # unused-noqa - "S101", # assert ] From a61ceb7634ba02c082c3e2ad41446e7cf5ded24d Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 02:06:24 +0200 Subject: [PATCH 17/40] fixup! fixup! style: fix various --- httoop/__main__.py | 2 +- httoop/authentication/__init__.py | 14 +++++++------- httoop/authentication/basic.py | 6 +++--- httoop/authentication/digest.py | 5 +++-- httoop/codecs/application/gzip.py | 8 ++++---- httoop/codecs/application/hal_json.py | 2 +- httoop/codecs/application/xml.py | 2 +- httoop/codecs/application/zlib.py | 4 ++-- httoop/codecs/text/plain.py | 4 ++-- httoop/header/__init__.py | 2 +- httoop/header/element.py | 14 +++++++------- httoop/header/headers.py | 3 ++- httoop/header/messaging.py | 16 ++++++++-------- httoop/header/ranges.py | 13 +++++++------ httoop/header/security.py | 8 ++++---- httoop/messages/protocol.py | 2 +- httoop/messages/request.py | 2 +- httoop/messages/response.py | 2 +- httoop/uri/uri.py | 14 +++++++------- 19 files changed, 63 insertions(+), 60 deletions(-) diff --git a/httoop/__main__.py b/httoop/__main__.py index d7a2163..dad6e22 100644 --- a/httoop/__main__.py +++ b/httoop/__main__.py @@ -76,7 +76,7 @@ def add_common_arguments(cls, add) -> None: def get_file(cls, file: str) -> IO: if file == '-': return sys.stdin.buffer - return open(file, 'rb') + return pathlib.Path(file).open('rb') def parse_request(self) -> None: server = ServerStateMachine(self.arguments.scheme, self.arguments.host, self.arguments.port) diff --git a/httoop/authentication/__init__.py b/httoop/authentication/__init__.py index a36ba6a..c39755e 100644 --- a/httoop/authentication/__init__.py +++ b/httoop/authentication/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar from httoop.authentication.basic import BasicAuthRequestScheme, BasicAuthResponseScheme from httoop.authentication.digest import DigestAuthRequestScheme, DigestAuthResponseScheme, SecureDigestAuthRequestScheme @@ -16,7 +16,7 @@ class AuthElement(HeaderElement): - schemes = {} + schemes: ClassVar = {} RE_SPACE_SPLIT = re.compile(rb'\s+(?=(?:[^"]*"[^"]*")*[^"]*$)') def sanitize(self) -> None: @@ -31,16 +31,16 @@ def parseparams(cls, elementstr: bytes) -> tuple[bytes, dict[bytes, str]] | tupl try: scheme, authinfo = elementstr.split(b' ', 1) except ValueError: - raise InvalidHeader(_('Authorization headers must contain authentication scheme')) + raise InvalidHeader(_('Authorization headers must contain authentication scheme')) from None try: parser = cls.schemes[scheme.decode('ISO8859-1').lower()] except KeyError: - raise InvalidHeader(_('Unsupported authentication scheme: %r'), scheme) + raise InvalidHeader(_('Unsupported authentication scheme: %r'), scheme) from None try: authinfo = parser.parse(authinfo) except KeyError as key: - raise InvalidHeader(_('Missing parameter %r for authentication scheme %r'), str(key), scheme) + raise InvalidHeader(_('Missing parameter %r for authentication scheme %r'), str(key), scheme) from None return scheme.title(), authinfo @@ -48,12 +48,12 @@ def compose(self) -> bytes: try: scheme = self.schemes[self.value.lower()] except KeyError: - raise InvalidHeader(_('Unsupported authentication scheme: %r'), self.value) + raise InvalidHeader(_('Unsupported authentication scheme: %r'), self.value) from None try: authinfo = scheme.compose(self.params) except KeyError as key: - raise InvalidHeader(_('Missing parameter %r for authentication scheme %r'), str(key), self.value) + raise InvalidHeader(_('Missing parameter %r for authentication scheme %r'), str(key), self.value) from None return b'%s %s' % (self.value.encode('ASCII').title(), authinfo) diff --git a/httoop/authentication/basic.py b/httoop/authentication/basic.py index 5eb8108..c3aecb0 100644 --- a/httoop/authentication/basic.py +++ b/httoop/authentication/basic.py @@ -19,9 +19,9 @@ def parse(authinfo: bytes) -> dict[str, bytes]: if not username: raise ValueError() except Base64Error: - raise InvalidHeader(_('Basic authentication contains invalid base64')) + raise InvalidHeader(_('Basic authentication contains invalid base64')) from None except ValueError: - raise InvalidHeader(_('No username:password provided')) + raise InvalidHeader(_('No username:password provided')) from None return { # 'username': username.decode('ISO8859-1'), @@ -49,4 +49,4 @@ def parse(authinfo: bytes) -> dict[bytes, str | bytes]: @staticmethod def compose(authinfo: ByteUnicodeDict) -> bytes: - return HeaderElement.formatparam(b'realm', authinfo['realm'], True) + return HeaderElement.formatparam(b'realm', authinfo['realm'], quote=True) diff --git a/httoop/authentication/digest.py b/httoop/authentication/digest.py index 43988b6..d1fc00f 100644 --- a/httoop/authentication/digest.py +++ b/httoop/authentication/digest.py @@ -1,9 +1,10 @@ +# ruff: file-ignore[N806,N802] from __future__ import annotations from hashlib import md5, new, sha256 from hmac import compare_digest from secrets import token_bytes -from typing import Callable +from typing import Callable, ClassVar from httoop.exceptions import InvalidHeader from httoop.header.element import HeaderElement @@ -11,7 +12,7 @@ class DigestAuthScheme: - algorithms = { + algorithms: ClassVar = { 'MD5': md5, 'MD5-sess': md5, 'SHA-256': sha256, diff --git a/httoop/codecs/application/gzip.py b/httoop/codecs/application/gzip.py index 93dc905..e14fa90 100644 --- a/httoop/codecs/application/gzip.py +++ b/httoop/codecs/application/gzip.py @@ -20,7 +20,7 @@ def encode(cls, data: bytes, charset: None = None, mimetype: None = None) -> byt fd.write(Codec.encode(data, charset)) return out.getvalue() except zlib.error: # pragma: no cover - raise EncodeError(_('Invalid gzip data.')) + raise EncodeError(_('Invalid gzip data.')) from None @classmethod def decode(cls, data: bytes, charset: None = None, mimetype: None = None) -> str: @@ -28,7 +28,7 @@ def decode(cls, data: bytes, charset: None = None, mimetype: None = None) -> str with gzip.GzipFile(fileobj=io.BytesIO(data)) as fd: data = fd.read() except (zlib.error, OSError, EOFError): - raise DecodeError(_('Invalid gzip data.')) + raise DecodeError(_('Invalid gzip data.')) from None return Codec.decode(data, charset) @classmethod @@ -43,7 +43,7 @@ def iterencode(cls, data, charset=None, mimetype=None): out.truncate() yield out.getvalue() except zlib.error: # pragma: no cover - raise EncodeError(_('Invalid gzip data.')) + raise EncodeError(_('Invalid gzip data.')) from None @classmethod def iterdecode(cls, data, charset=None, mimetype=None): @@ -62,4 +62,4 @@ def iterdecode(cls, data, charset=None, mimetype=None): fd.seek(0) yield Codec.decode(gzfd.read(), charset) except (zlib.error, OSError, EOFError): - raise DecodeError(_('Invalid gzip data.')) + raise DecodeError(_('Invalid gzip data.')) from None diff --git a/httoop/codecs/application/hal_json.py b/httoop/codecs/application/hal_json.py index 1bd4c11..8064a32 100644 --- a/httoop/codecs/application/hal_json.py +++ b/httoop/codecs/application/hal_json.py @@ -138,6 +138,6 @@ def encode(cls, data: dict[str, None] | Resource, charset: str | None = None, mi try: Resource(data.copy()) except DecodeError as exc: - raise EncodeError(str(exc)) + raise EncodeError(str(exc)) from exc return super().encode(data) diff --git a/httoop/codecs/application/xml.py b/httoop/codecs/application/xml.py index f11983d..a9a2a20 100644 --- a/httoop/codecs/application/xml.py +++ b/httoop/codecs/application/xml.py @@ -33,7 +33,7 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | try: return fromstring(data) except ParseError as exc: - raise DecodeError(f'Could not decode as {mimetype}: {exc}') + raise DecodeError(f'Could not decode as {mimetype}: {exc}') from exc @classmethod def encode(cls, root: Element, charset: str | None = None, mimetype: ContentType | None = None) -> bytes: diff --git a/httoop/codecs/application/zlib.py b/httoop/codecs/application/zlib.py index e497815..68a51a3 100644 --- a/httoop/codecs/application/zlib.py +++ b/httoop/codecs/application/zlib.py @@ -15,12 +15,12 @@ def encode(cls, data: bytes, charset: None = None, mimetype: None = None) -> byt try: return zlib.compress(Codec.encode(data, charset)) except zlib.error: # pragma: no cover - raise EncodeError(_('Invalid zlib/deflate data.')) + raise EncodeError(_('Invalid zlib/deflate data.')) from None @classmethod def decode(cls, data: bytes, charset: str | None = None, mimetype: None = None) -> str: try: data = zlib.decompress(data) except zlib.error: - raise DecodeError(_('Invalid zlib/deflate data.')) + raise DecodeError(_('Invalid zlib/deflate data.')) from None return Codec.decode(data, charset) diff --git a/httoop/codecs/text/plain.py b/httoop/codecs/text/plain.py index b9819bf..143f1f6 100644 --- a/httoop/codecs/text/plain.py +++ b/httoop/codecs/text/plain.py @@ -20,7 +20,7 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | assert isinstance(data, bytes) return data.decode(charset or 'UTF-8') except UnicodeDecodeError: - raise DecodeError('Wrong encoding.') + raise DecodeError('Wrong encoding.') from None @classmethod def encode(cls, data: str, charset: str | None = None, mimetype: ContentType | None = None) -> bytes: @@ -28,4 +28,4 @@ def encode(cls, data: str, charset: str | None = None, mimetype: ContentType | N assert not isinstance(data, bytes) return data.encode(charset or 'UTF-8') except UnicodeEncodeError: - raise EncodeError('Wrong encoding.') + raise EncodeError('Wrong encoding.') from None diff --git a/httoop/header/__init__.py b/httoop/header/__init__.py index 05170e5..39570b2 100644 --- a/httoop/header/__init__.py +++ b/httoop/header/__init__.py @@ -17,5 +17,5 @@ __all__ = ['HeaderElement', 'Headers', 'Server', 'UserAgent', 'auth', 'cache', 'conditional', 'messaging', 'ranges', 'security'] for member in HEADER.values(): name = member.__name__ - __all__.append(name) + __all__ += name globals()[name] = member diff --git a/httoop/header/element.py b/httoop/header/element.py index 5aeb1c9..37eee16 100644 --- a/httoop/header/element.py +++ b/httoop/header/element.py @@ -138,7 +138,7 @@ def _rfc2231_and_continuation_params(cls, params: Iterator[Any]) -> Iterator[tup try: key, value = key[:-1], Percent.unquote(value_).decode(encoding) except UnicodeDecodeError as exc: - raise InvalidHeader(_('%s') % (exc,)) + raise InvalidHeader(_('%s') % (exc,)) from None else: value = value.decode('ISO8859-1') key_, asterisk, num = key.rpartition(b'*') @@ -195,7 +195,7 @@ def merge(cls, elements: list, others: list) -> bytes: return cls.join([bytes(x) for x in cls.sorted(elements + others)]) @classmethod - def formatparam(cls, param: bytes, value: bytes | str | None = None, quote: bool = False) -> bytes: + def formatparam(cls, param: bytes, value: bytes | str | None = None, *, quote: bool = False) -> bytes: """ Convenience function to format and return a key=value pair. @@ -227,7 +227,7 @@ def decode_rfc2047_charset(cls, value: bytes) -> tuple[str, str]: try: return ''.join(atom.decode(cls._sanitize_encoding(charset or 'ISO8859-1')) for atom, charset in decode_header(value.decode('ISO8859-1'))), 'UTF-8' except (UnicodeDecodeError, HeaderParseError) as exc: - raise InvalidHeader(str(exc)) + raise InvalidHeader(str(exc)) from exc try: return value.decode('ASCII'), 'ASCII' except UnicodeDecodeError: @@ -265,7 +265,7 @@ def mimetype(self) -> str: def type(self): return self.value.split('/', 1)[0] - @type.setter + @type.setter # noqa: A003 # https://github.com/astral-sh/ruff/issues/23074 def type(self, type_) -> None: self.value = f'{type_}/{self.subtype}' @@ -317,7 +317,7 @@ class _AcceptElement(HeaderElement): RE_Q_SEPARATOR = re.compile(rb';\s*q\s*=\s*') @property - def quality(self) -> float: + def quality(self) -> float | None: """The quality of this value.""" val = self.params.get('q', '1') if isinstance(val, HeaderElement): # pragma: no cover @@ -329,9 +329,9 @@ def quality(self) -> float: def sanitize(self) -> None: super().sanitize() try: - self.quality + self.quality # noqa: B018 except ValueError: - raise InvalidHeader(_('Quality value must be float.')) + raise InvalidHeader(_('Quality value must be float.')) from None @classmethod def parse(cls, elementstr: bytes) -> HeaderElement: diff --git a/httoop/header/headers.py b/httoop/header/headers.py index 12ec03d..cef5975 100644 --- a/httoop/header/headers.py +++ b/httoop/header/headers.py @@ -87,7 +87,8 @@ def set_element(self, fieldname: str, *args, **kwargs) -> None: def append_element(self, fieldname: str, *args, **kwargs) -> None: self.append(fieldname, bytes(self.create_element(fieldname, *args, **kwargs))) - def create_element(self, fieldname: str, *args, **kwargs) -> HeaderElement: + @classmethod + def create_element(cls, fieldname: str, *args, **kwargs) -> HeaderElement: element_cls = HEADER.get(fieldname, HeaderElement) return element_cls(*args, **kwargs) diff --git a/httoop/header/messaging.py b/httoop/header/messaging.py index a5336b1..4c66880 100644 --- a/httoop/header/messaging.py +++ b/httoop/header/messaging.py @@ -93,7 +93,7 @@ class ContentDisposition(HeaderElement, name='Content-Disposition'): is_response_header = True - from httoop.date import Date + from httoop.date import Date # noqa: PLC0415 @property def filename(self) -> str | None: @@ -279,7 +279,7 @@ class Host(HeaderElement): @property def is_ip4(self) -> bool: - from socket import AF_INET, inet_pton + from socket import AF_INET, inet_pton # noqa: PLC0415 try: inet_pton(AF_INET, self.host) @@ -289,7 +289,7 @@ def is_ip4(self) -> bool: @property def is_ip6(self) -> bool: - from socket import AF_INET6, inet_pton + from socket import AF_INET6, inet_pton # noqa: PLC0415 try: inet_pton(AF_INET6, self.host) @@ -370,7 +370,7 @@ class SetCookie(_ListElement, _CookieElement, name='Set-Cookie'): is_response_header = True - from httoop.date import Date + from httoop.date import Date # noqa: PLC0415 @classmethod def split(cls, fieldvalue: bytes) -> list[bytes]: @@ -398,21 +398,21 @@ def persistent(self) -> bool: return 'max-age' in self.params or 'expires' in self.params @property - def max_age(self) -> None: + def max_age(self) -> int | None: if self.params.get('max-age'): try: return integer(self.params['max-age']) except ValueError: - raise InvalidHeader(_('Cookie: max-age is not a number: %r'), self.params['max-age']) + raise InvalidHeader(_('Cookie: max-age is not a number: %r'), self.params['max-age']) from None return None @property - def expires(self) -> Date: + def expires(self) -> Date | None: if self.params.get('expires'): try: return self.Date(self.params['expires']) except InvalidDate: - raise InvalidHeader(_('Cookie: expires is not a valid date: %r'), self.params['expires']) + raise InvalidHeader(_('Cookie: expires is not a valid date: %r'), self.params['expires']) from None return None diff --git a/httoop/header/ranges.py b/httoop/header/ranges.py index f5cd436..31881cc 100644 --- a/httoop/header/ranges.py +++ b/httoop/header/ranges.py @@ -38,7 +38,7 @@ def compose(self) -> bytes: @classmethod def parse(cls, elementstr: bytes) -> ContentRange: value, start, end, complete_length = None, None, None, None - try: + try: # noqa: PLW0717 value, content_range = elementstr.split(None, 1) if value != b'bytes': raise InvalidHeader(_('Only "bytes" Content-Range supported')) @@ -56,8 +56,8 @@ def parse(cls, elementstr: bytes) -> ContentRange: raise ValueError() if complete_length is None and start is None: raise ValueError() - except ValueError: - raise InvalidHeader(_('Content-Range: %r'), elementstr) + except ValueError as exc: + raise InvalidHeader(_('Content-Range: %r'), elementstr) from exc return cls(value.decode('ISO8859-1'), (start, end), complete_length) @@ -91,8 +91,8 @@ def parse(cls, elementstr: bytes) -> Range: stop = integer(stop) if stop else None if (start and start < 0) or (stop and stop < 0): raise ValueError() - except ValueError: - raise InvalidHeader(_('no range number.')) + except ValueError as exc: + raise InvalidHeader(_('no range number.')) from exc if start is not None and stop is not None and stop <= start: raise InvalidHeader(_('range start must be smaller than end.')) if not start and not stop: @@ -117,7 +117,8 @@ def prevent_denial_of_service(self) -> None: if self.stddev([(x[1] or float('inf')) - (x[0] or 0) for x in self.ranges]) > 2.0: raise InvalidHeader(_('ranges exceeding high standard deviation')) - def stddev(self, xs: list[int | float]) -> float: + @classmethod + def stddev(cls, xs: list[int | float]) -> float: def average(xs): return sum(xs) * 1.0 / len(xs) diff --git a/httoop/header/security.py b/httoop/header/security.py index 12460bf..6598038 100644 --- a/httoop/header/security.py +++ b/httoop/header/security.py @@ -25,7 +25,7 @@ class ContentSecurityPolicy(HeaderElement, name='Content-Security-Policy'): is_response_header = True RE_SPLIT = re.compile(rb';') - RE_PARAMS = re.compile(b'\\s+') + RE_PARAMS = re.compile(rb'\\s+') def compose(self) -> bytes: return b'%s %s; ' % (self.value.encode('ISO8859-1'), b' '.join(self.params.keys())) @@ -82,7 +82,7 @@ class FrameOptions(HeaderElement, name='X-Frame-Options'): is_response_header = True - RE_PARAMS = re.compile(b'\\s+') + RE_PARAMS = re.compile(rb'\\s+') @property def deny(self) -> bool: @@ -93,9 +93,9 @@ def same_origin(self) -> bool: return self.value.upper() == 'SAMEORIGIN' @property - def allow_from(self) -> list[HTTPS]: + def allow_from(self) -> list[HTTPS] | None: if self.value.upper() == 'ALLOW-FROM': - from httoop.uri import URI + from httoop.uri import URI # noqa: PLC0415 return [URI(uri) for uri in self.params] return None diff --git a/httoop/messages/protocol.py b/httoop/messages/protocol.py index 33f9f39..52f6de3 100644 --- a/httoop/messages/protocol.py +++ b/httoop/messages/protocol.py @@ -57,7 +57,7 @@ def parse(self, protocol: bytes) -> None: try: self.__protocol = (integer(major), integer(minor)) except ValueError: - raise InvalidLine(_('Invalid HTTP protocol: %r'), protocol.decode('ISO8859-1')) + raise InvalidLine(_('Invalid HTTP protocol: %r'), protocol.decode('ISO8859-1')) from None def compose(self) -> bytes: return b'%s/%d.%d' % (self.name, self.major, self.minor) diff --git a/httoop/messages/request.py b/httoop/messages/request.py index 0effd8a..b2d0a1d 100644 --- a/httoop/messages/request.py +++ b/httoop/messages/request.py @@ -64,7 +64,7 @@ def parse(self, line: bytes) -> None: try: method, uri, version = bits except ValueError: - raise InvalidLine(_('Invalid request line: %r'), line.decode('ISO8859-1')) + raise InvalidLine(_('Invalid request line: %r'), line.decode('ISO8859-1')) from None # protocol version super().parse(version) diff --git a/httoop/messages/response.py b/httoop/messages/response.py index 38b25cb..0f0a28c 100644 --- a/httoop/messages/response.py +++ b/httoop/messages/response.py @@ -49,7 +49,7 @@ def parse(self, line: bytes) -> None: try: version, status = bits except ValueError: - raise InvalidLine(_('Invalid response line: %r'), line.decode('ISO8859-1')) + raise InvalidLine(_('Invalid response line: %r'), line.decode('ISO8859-1')) from None # version super().parse(version) diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index 013ade0..f1e2201 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -68,7 +68,7 @@ def port(self, port) -> None: if not 0 < port <= 65535: raise ValueError except ValueError: - raise InvalidURI(_('Invalid port: %r'), port) # TODO: TypeError + raise InvalidURI(_('Invalid port: %r'), port) from None # TODO: TypeError self._port = port def __init__(self, uri: Any | None = None, *args, **kwargs) -> None: @@ -240,10 +240,10 @@ def parse(self, uri: bytes) -> None: try: scheme = scheme.decode('ascii').lower() except UnicodeDecodeError: # pragma: no cover - raise InvalidURI(_('Invalid scheme: must be ASCII.')) + raise InvalidURI(_('Invalid scheme: must be ASCII.')) from None if scheme and scheme.strip('abcdefghijklmnopqrstuvwxyz0123456789.-+'): - raise InvalidURI(_('Invalid scheme: must only contain alphanumeric letters or plus, dash, dot.')) + raise InvalidURI(_('Invalid scheme: must only contain alphanumeric letters or plus, dash, dot.')) from None if query_string: query_string = QueryString.encode(QueryString.decode(query_string, self.encoding), self.encoding) @@ -273,14 +273,14 @@ def _unquote_host(self, host: bytes) -> str: try: return '[%s]' % host.decode('ascii') except UnicodeDecodeError: # pragma: no cover - raise InvalidURI(_('Invalid IPvFuture address: must be ASCII.')) - raise InvalidURI(_('Invalid IP address in URI.')) + raise InvalidURI(_('Invalid IPvFuture address: must be ASCII.')) from None + raise InvalidURI(_('Invalid IP address in URI.')) from None # IPv4 if all(x.isdigit() for x in host.split(b'.')): try: return inet_ntop(AF_INET, inet_pton(AF_INET, host.decode('ascii'))) except (OSError, UnicodeDecodeError): - raise InvalidURI(_('Invalid IPv4 address in URI.')) + raise InvalidURI(_('Invalid IPv4 address in URI.')) from None if host.strip(Percent.UNRESERVED + Percent.SUB_DELIMS + b'%'): raise InvalidURI(_('Invalid URI host.')) @@ -290,7 +290,7 @@ def _unquote_host(self, host: bytes) -> str: try: return host.encode('ascii').decode('idna').lower() except UnicodeError: # pragma: no cover - raise InvalidURI(_('Invalid host.')) + raise InvalidURI(_('Invalid host.')) from None def compose(self) -> bytes: return b''.join(self._compose_absolute_iter()) From c5224dd3e6a3b995e4179e3e5b37044bf3cafc54 Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 02:39:08 +0200 Subject: [PATCH 18/40] test: fix comparison --- tests/headers/test_accept_headers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/headers/test_accept_headers.py b/tests/headers/test_accept_headers.py index bf0bfc0..aced76d 100644 --- a/tests/headers/test_accept_headers.py +++ b/tests/headers/test_accept_headers.py @@ -8,8 +8,8 @@ def test_quality_parameter_in_accept_header(headers): def test_comparing_accept(headers): headers.parse(b'Accept: application/json; q=0.2, text/plain, text/html; q=0.5, *; q=0') - headers.elements('Accept')[0] < 'text/html' - headers.elements('Accept')[0] < '*' + assert headers.elements('Accept')[0] < 'text/html' + assert headers.elements('Accept')[0] < '*' @pytest.mark.parametrize('header_name', ['Accept', 'Content-Type']) From 192a5f5ec6e3b7a847c267012658504784a65e46 Mon Sep 17 00:00:00 2001 From: Space One Date: Wed, 15 Jul 2026 02:39:23 +0200 Subject: [PATCH 19/40] fixup! fixup! fixup! style: fix various --- httoop/header/element.py | 4 ++-- httoop/uri/uri.py | 2 +- tests/api/test_slots.py | 2 +- tests/headers/test_cookies.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/httoop/header/element.py b/httoop/header/element.py index 37eee16..cc9fafd 100644 --- a/httoop/header/element.py +++ b/httoop/header/element.py @@ -38,7 +38,7 @@ class HeaderElement: # Regular expression that matches `special' characters in parameters, the # existence of which force quoting of the parameter value. - RE_TSPECIALS = re.compile(b'[ \\(\\)<>@,;:\\\\"/\\[\\]\\?=]') + RE_TSPECIALS = re.compile(rb'[ \(\)<>@,;:\\"/\[\]\?=]') RE_SPLIT = re.compile(rb',(?=(?:[^"]*"[^"]*")*[^"]*$)') RE_PARAMS = re.compile(rb';(?=(?:[^"]*"[^"]*")*[^"]*$)') @@ -111,7 +111,7 @@ def unescape_key(cls, key: bytes) -> bytes: def unescape_param(cls, value: bytes) -> tuple[bytes, bool]: quoted = value.startswith(b'"') and value.endswith(b'"') if quoted: - value = re.sub(b'\\\\(?!\\\\)', b'', value[1:-1]) + value = re.sub(rb'\\(?!\\)', b'', value[1:-1]) elif cls.RE_TSPECIALS.search(value): raise InvalidHeader(_('Unquoted parameter in %r containing TSPECIALS: %r'), cls.name, value) return value, quoted diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index f1e2201..f11ea9a 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -138,7 +138,7 @@ def abspath(self) -> None: >>> u = URI(b'/foo/../bar/.'); u.abspath(); u.path == u'/bar/' True """ - path = re.sub('\\/{2,}', '/', self.path) # remove // + path = re.sub(r'\/{2,}', '/', self.path) # remove // if not path: return unsplit = [] diff --git a/tests/api/test_slots.py b/tests/api/test_slots.py index 9e2f6d6..031705e 100644 --- a/tests/api/test_slots.py +++ b/tests/api/test_slots.py @@ -33,6 +33,6 @@ def test_slots(class_, args): obj = class_(*args) assert obj.__slots__ is not None with pytest.raises(AttributeError): - obj.__dict__ + _ = obj.__dict__ with pytest.raises(AttributeError): obj.foo = True diff --git a/tests/headers/test_cookies.py b/tests/headers/test_cookies.py index c61f10b..76808b8 100644 --- a/tests/headers/test_cookies.py +++ b/tests/headers/test_cookies.py @@ -56,13 +56,13 @@ def test_set_cookie(cookie, values, headers): def test_invalid_expires(headers): headers['Set-Cookie'] = 'foo=bar; expires=xyz' with pytest.raises(InvalidHeader): - headers.get_element('Set-Cookie').expires + _ = headers.get_element('Set-Cookie').expires def test_invalid_max_age(headers): headers['Set-Cookie'] = 'foo=bar; max-age=1.1' with pytest.raises(InvalidHeader): - headers.get_element('Set-Cookie').max_age + _ = headers.get_element('Set-Cookie').max_age def test_persistent(headers): From c0a251dfef17c50f0d2334f0c626d87756e91fbe Mon Sep 17 00:00:00 2001 From: Space One Date: Thu, 23 Jul 2026 23:58:00 +0200 Subject: [PATCH 20/40] fixup! ci(pyproject): add ty and mypy configuration --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7490193..a87ae78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.21 + rev: v0.16.0 hooks: - id: ruff-check alias: "ruff" @@ -55,12 +55,12 @@ repos: stages: [commit-msg] args: ["--strict"] - repo: https://github.com/astral-sh/ty-pre-commit - rev: v0.0.57 + rev: v0.0.63 hooks: - id: ty stages: [manual] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v2.2.0 + rev: v2.3.0 hooks: - id: mypy stages: [manual] From a4e153fc000f3d90612a1b426590bb7c0bf15976 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:04:27 +0200 Subject: [PATCH 21/40] ci(ruff): ignore latest ruff selectors --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1a0cb02..6cb3a7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,9 @@ preview = true select = ["E", "W", "F", "I", "D", "TD", "ALL"] ignore = [ # Invalid / don't care: + "RUF105", # noqa-comments + "RUF106", # rule-codes-in-suppression-comments: + "RUF201", # rule-codes-in-selectors "E501", # line-too-long "E203", # whitespace-before-punctuation "COM812", # missing-trailing-comma From 0ccdacc9f2040200ad11de620643d84011e34b3b Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:10:28 +0200 Subject: [PATCH 22/40] style: ruff --- tests/api/test_body.py | 17 ++++++++--------- tests/api/test_wsgi.py | 2 +- tests/authentication/test_auth.py | 3 ++- tests/headers/test_host_header.py | 2 +- tests/headers/test_invalid_headers.py | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/api/test_body.py b/tests/api/test_body.py index e4da942..cb7b580 100644 --- a/tests/api/test_body.py +++ b/tests/api/test_body.py @@ -37,14 +37,13 @@ def test_body_set_stringio(request_): def test_body_set_tempfile(request_): - tempfile = NamedTemporaryFile() - tempfile.write(b'ThisIsANamedTemporaryFile') - tempfile.flush() - request_.body = tempfile - assert len(request_.body) == 25 - assert request_.body == b'ThisIsANamedTemporaryFile' - assert request_.body.fileable - tempfile.close() + with NamedTemporaryFile() as tempfile: + tempfile.write(b'ThisIsANamedTemporaryFile') + tempfile.flush() + request_.body = tempfile + assert len(request_.body) == 25 + assert request_.body == b'ThisIsANamedTemporaryFile' + assert request_.body.fileable def test_body_set_bytearray(request_): @@ -109,7 +108,7 @@ def tets_body_set_none(request_): def test_closed_body(request_): b = BytesIO() b.close() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r'I/O operation on closed file\.'): request_.body = b diff --git a/tests/api/test_wsgi.py b/tests/api/test_wsgi.py index e0df872..621ba10 100644 --- a/tests/api/test_wsgi.py +++ b/tests/api/test_wsgi.py @@ -149,7 +149,7 @@ def application14(environ, start_response): def test_eror_reraising(): client = WSGIClient() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match='test'): client(application14) diff --git a/tests/authentication/test_auth.py b/tests/authentication/test_auth.py index f03b517..eada592 100644 --- a/tests/authentication/test_auth.py +++ b/tests/authentication/test_auth.py @@ -48,7 +48,8 @@ def test_order(headers): def test_no_realm(headers): headers.parse(b'WWW-Authenticate: basic foo="bar"') - assert headers.get_element('WWW-Authenticate').realm == '' + assert not headers.get_element('WWW-Authenticate').realm + assert isinstance(headers.get_element('WWW-Authenticate').realm, str) def test_multiple_headers(headers): diff --git a/tests/headers/test_host_header.py b/tests/headers/test_host_header.py index 906799b..82ddca4 100644 --- a/tests/headers/test_host_header.py +++ b/tests/headers/test_host_header.py @@ -56,7 +56,7 @@ def _test_iter(header, host, port, headers): @pytest.mark.parametrize('invalid', list( - list((set('\x7F()<>@,;:/\\[\\]={} \t\\\\^"\'') | set(map(chr, range(0x1F)))) - set(';\x00')) + [ + (set('\x7F()<>@,;:/\\[\\]={} \t\\\\^"\'') | set(map(chr, range(0x1F)))) - set(';\x00'), *[ ', ', pytest.param(';', marks=pytest.mark.xfail), # FIXME: fails pytest.param('\x00', marks=pytest.mark.xfail), # FIXME: fails diff --git a/tests/headers/test_invalid_headers.py b/tests/headers/test_invalid_headers.py index 8069d44..fdd26ff 100644 --- a/tests/headers/test_invalid_headers.py +++ b/tests/headers/test_invalid_headers.py @@ -9,8 +9,8 @@ @pytest.mark.parametrize('invalid', iter(INVALID_HEADER_FIELD_NAMES + LATIN_CHARS)) def test_parse_invalid_characters(invalid, request_): + invalid = b'foo%sbaz: blub' % (bytes((invalid,)),) with pytest.raises(InvalidHeader): - invalid = b'foo%sbaz: blub' % (bytes((invalid,)),) request_.headers.parse(invalid) From 531e072b3d0ff3b64dd35a673d605180f5224086 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:11:32 +0200 Subject: [PATCH 23/40] fixup! remove rules --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6cb3a7c..7e870cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -201,6 +201,7 @@ task-tags = ["TODO", "FIXME"] "S404", # suspicious-subprocess-import "S405", # suspicious-xml-etree-import "S603", # subprocess-without-shell-equals-true + "PLR2004", # magic-value-comparison ] "httoop/status/*.py" = [ "N801", From eb32cd4c1d469820ef42c1be725b09823bffa84b Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:13:27 +0200 Subject: [PATCH 24/40] fixup! style: ruff --- tests/api/test_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_date.py b/tests/api/test_date.py index f698bb8..63961f0 100644 --- a/tests/api/test_date.py +++ b/tests/api/test_date.py @@ -35,7 +35,7 @@ def test_date_datetime(date, expected, lt, gt): def test_date_timestamp(date, expected, lt, gt): for d in (Date.parse(date.encode('utf-8')), Date(date)): assert d is not None - assert float(d) == expected + assert float(d) == expected # noqa: RUF069 assert d == expected assert d == Date(date) From f228e26095711e5f8e1eca334054c08141526b13 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:15:04 +0200 Subject: [PATCH 25/40] NOQA --- httoop/__main__.py | 4 ++-- httoop/authentication/__init__.py | 4 ++-- httoop/codecs/__init__.py | 2 +- httoop/codecs/application/gzip.py | 2 +- httoop/codecs/message/http.py | 2 +- httoop/codecs/multipart/multipart.py | 4 ++-- httoop/codecs/text/plain.py | 4 ++-- httoop/date.py | 4 ++-- httoop/gateway/wsgi.py | 4 ++-- httoop/header/__init__.py | 2 +- httoop/header/conditional.py | 6 +++--- httoop/header/element.py | 14 +++++++------- httoop/header/headers.py | 4 ++-- httoop/header/messaging.py | 6 +++--- httoop/header/ranges.py | 4 ++-- httoop/messages/body.py | 2 +- httoop/messages/protocol.py | 2 +- httoop/meta.py | 2 +- httoop/parser.py | 20 ++++++++++---------- httoop/semantic/message.py | 2 +- httoop/semantic/response.py | 14 +++++++------- httoop/status/__init__.py | 2 +- httoop/status/status.py | 24 ++++++++++++------------ httoop/status/types.py | 2 +- httoop/uri/percent_encoding.py | 4 ++-- httoop/uri/uri.py | 12 ++++++------ httoop/util.py | 6 +++--- pyproject.toml | 1 + tests/api/test_date.py | 4 ++-- tests/api/test_header.py | 2 +- tests/api/test_statemachine.py | 4 ++-- tests/api/test_status.py | 4 ++-- tests/api/test_wsgi.py | 2 +- tests/headers/test_cookies.py | 4 ++-- tests/messaging/test_request_codecs.py | 4 ++-- tests/tc2231/test_tc2231.py | 4 ++-- tests/uri/test_uri_query.py | 10 +++++----- tests/uri/test_uri_schemes.py | 2 +- 38 files changed, 100 insertions(+), 99 deletions(-) diff --git a/httoop/__main__.py b/httoop/__main__.py index dad6e22..f5cf200 100644 --- a/httoop/__main__.py +++ b/httoop/__main__.py @@ -94,7 +94,7 @@ def parse_response(self) -> None: print(repr(response.body)) print(repr(bytes(response.body))) if client.buffer: - assert client.message + assert client.message # noqa: S101 print('WARNING: response not yet complete!:') print(repr(client.message)) print(repr(client.message.headers)) @@ -120,7 +120,7 @@ def compose_response(self) -> None: self.common() def common(self) -> None: - assert self.message is not None + assert self.message is not None # noqa: S101 if self.arguments.protocol: protocol = self.arguments.protocol try: diff --git a/httoop/authentication/__init__.py b/httoop/authentication/__init__.py index c39755e..a8cf160 100644 --- a/httoop/authentication/__init__.py +++ b/httoop/authentication/__init__.py @@ -68,7 +68,7 @@ class AuthRequestElement(AuthElement): encoding = 'ASCII' - schemes = { + schemes = { # noqa: RUF012 'basic': BasicAuthRequestScheme, 'digest': SecureDigestAuthRequestScheme, } @@ -103,7 +103,7 @@ def password(self, password) -> None: class AuthResponseElement(AuthElement): - schemes = { + schemes = { # noqa: RUF012 'basic': BasicAuthResponseScheme, 'digest': DigestAuthResponseScheme, } diff --git a/httoop/codecs/__init__.py b/httoop/codecs/__init__.py index 1ddd3e3..f742034 100644 --- a/httoop/codecs/__init__.py +++ b/httoop/codecs/__init__.py @@ -18,7 +18,7 @@ __all__ = ['CODECS', 'Codec', 'application', 'audio', 'example', 'image', 'message', 'model', 'multipart', 'text', 'video'] -def lookup(encoding: str, raise_errors: bool = True) -> Any: +def lookup(encoding: str, raise_errors: bool = True) -> Any: # noqa: FBT001, FBT002 type_ = '%s/*' % (encoding.split('/', 1)[0],) return CODECS.get(encoding) or CODECS.get(type_) or (raise_errors and CODECS[encoding]) or None diff --git a/httoop/codecs/application/gzip.py b/httoop/codecs/application/gzip.py index e14fa90..8670025 100644 --- a/httoop/codecs/application/gzip.py +++ b/httoop/codecs/application/gzip.py @@ -33,7 +33,7 @@ def decode(cls, data: bytes, charset: None = None, mimetype: None = None) -> str @classmethod def iterencode(cls, data, charset=None, mimetype=None): - try: + try: # noqa: PLW0717 out = io.BytesIO() with gzip.GzipFile(fileobj=out, mode='w', compresslevel=cls.compression_level) as fd: for part in data: diff --git a/httoop/codecs/message/http.py b/httoop/codecs/message/http.py index ec79fce..4f11ba0 100644 --- a/httoop/codecs/message/http.py +++ b/httoop/codecs/message/http.py @@ -21,7 +21,7 @@ def encode(cls, data: Request | Response, charset: str | None = None, mimetype: @classmethod def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | None = None) -> Request | Response: - from httoop.messages import Request, Response + from httoop.messages import Request, Response # noqa: PLC0415 line, data = data.split(b'\r\n', 1) message = Request() diff --git a/httoop/codecs/multipart/multipart.py b/httoop/codecs/multipart/multipart.py index 59bf42f..9fe570a 100644 --- a/httoop/codecs/multipart/multipart.py +++ b/httoop/codecs/multipart/multipart.py @@ -37,13 +37,13 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | if part not in {b'--', b'--\r\n'}: raise DecodeError(_('Invalid multipart end: %r'), part.decode('ISO8859-1')) - from httoop.messages.body import Body + from httoop.messages.body import Body # noqa: PLC0415 multiparts = [] for part in parts: if not part.startswith(b'\r\n'): raise DecodeError(_('Invalid boundary end: %r'), part[:2].decode('ISO8859-1')) - part = part[2:] + part = part[2:] # noqa: PLW2901 headers, separator, content = part.partition(b'\r\n\r\n') if not separator: raise DecodeError(_('Multipart does not contain CRLF header separator')) diff --git a/httoop/codecs/text/plain.py b/httoop/codecs/text/plain.py index 143f1f6..bf80972 100644 --- a/httoop/codecs/text/plain.py +++ b/httoop/codecs/text/plain.py @@ -17,7 +17,7 @@ class PlainText(Codec): @classmethod def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | None = None) -> str: try: - assert isinstance(data, bytes) + assert isinstance(data, bytes) # noqa: S101 return data.decode(charset or 'UTF-8') except UnicodeDecodeError: raise DecodeError('Wrong encoding.') from None @@ -25,7 +25,7 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | @classmethod def encode(cls, data: str, charset: str | None = None, mimetype: ContentType | None = None) -> bytes: try: - assert not isinstance(data, bytes) + assert not isinstance(data, bytes) # noqa: S101 return data.encode(charset or 'UTF-8') except UnicodeEncodeError: raise EncodeError('Wrong encoding.') from None diff --git a/httoop/date.py b/httoop/date.py index 8f4d0e3..cc0b65b 100644 --- a/httoop/date.py +++ b/httoop/date.py @@ -21,7 +21,7 @@ __all__ = ['Date'] -class Date(Semantic): +class Date(Semantic): # noqa: PLW1641 """ A HTTP Date string. @@ -77,7 +77,7 @@ def __init__(self, timeval: Any | None = None) -> None: @property def datetime(self) -> datetime: if self.__datetime is None: - self.__datetime = datetime.utcfromtimestamp(int(self)) + self.__datetime = datetime.utcfromtimestamp(int(self)) # noqa: DTZ004 return self.__datetime @property diff --git a/httoop/gateway/wsgi.py b/httoop/gateway/wsgi.py index 2d80365..1b56e09 100644 --- a/httoop/gateway/wsgi.py +++ b/httoop/gateway/wsgi.py @@ -32,7 +32,7 @@ def read(self, *size): class WSGI: """A mixin class which implements the WSGI interface.""" - def __init__(self, environ: dict[str, str] | None = None, use_path_info: bool = False, *args, **kwargs) -> None: + def __init__(self, environ: dict[str, str] | None = None, use_path_info: bool = False, *args, **kwargs) -> None: # noqa: FBT001, FBT002 self.use_path_info = use_path_info super().__init__() self.exc_info = None @@ -112,7 +112,7 @@ def start_response(status, response_headers, exc_info=None): def buffered(data): try: yield data - for data in result: + for data in result: # noqa: PLR1704 if data: yield data finally: diff --git a/httoop/header/__init__.py b/httoop/header/__init__.py index 39570b2..b5c04d1 100644 --- a/httoop/header/__init__.py +++ b/httoop/header/__init__.py @@ -17,5 +17,5 @@ __all__ = ['HeaderElement', 'Headers', 'Server', 'UserAgent', 'auth', 'cache', 'conditional', 'messaging', 'ranges', 'security'] for member in HEADER.values(): name = member.__name__ - __all__ += name + __all__ += name # noqa: PLE0605 globals()[name] = member diff --git a/httoop/header/conditional.py b/httoop/header/conditional.py index 8a5ea94..9658702 100644 --- a/httoop/header/conditional.py +++ b/httoop/header/conditional.py @@ -3,7 +3,7 @@ from httoop.header.element import HeaderElement -class _DateComparable: +class _DateComparable: # noqa: PLW1641 Date = Date @@ -25,7 +25,7 @@ def __int__(self) -> int: return int(self.value) -class _MatchElement: +class _MatchElement: # noqa: PLW1641 def __eq__(self, other: object) -> bool: return self.value in {other, '*'} @@ -51,7 +51,7 @@ def matches_etag(self, etag, *, strong: bool = True): return value[1:-1] == etag -class ETag(HeaderElement): +class ETag(HeaderElement): # noqa: PLW1641 is_response_header = True diff --git a/httoop/header/element.py b/httoop/header/element.py index cc9fafd..4a56581 100644 --- a/httoop/header/element.py +++ b/httoop/header/element.py @@ -25,7 +25,7 @@ HEADER = CaseInsensitiveDict() -class HeaderElement: +class HeaderElement: # noqa: PLW1641 """An element (with parameters) from an HTTP header's element list.""" name = '' @@ -100,7 +100,7 @@ def parseparam(cls, atom: bytes) -> tuple[bytes, bytes, bool]: try: val, quoted = cls.unescape_param(val.strip()) except InvalidHeader: - raise InvalidHeader(_('Unquoted parameter %r in %r containing TSPECIALS: %r'), key, cls.name, val) + raise InvalidHeader(_('Unquoted parameter %r in %r containing TSPECIALS: %r'), key, cls.name, val) # noqa: B904 return cls.unescape_key(key), val, quoted @classmethod @@ -132,15 +132,15 @@ def _rfc2231_and_continuation_params(cls, params: Iterator[Any]) -> Iterator[tup raise InvalidHeader(_('Parameter given twice: %r'), key.decode('ISO8859-1')) count.add(key) if b'*' in key: - if key.endswith(b'*') and not quoted and not value.startswith(b"'") and value.count(b"'") >= 2: + if key.endswith(b'*') and not quoted and not value.startswith(b"'") and value.count(b"'") >= 2: # noqa: PLR2004 charset, _language, value_ = value.split(b"'", 2) encoding = cls._sanitize_encoding(charset.decode('ASCII', 'replace')) try: - key, value = key[:-1], Percent.unquote(value_).decode(encoding) + key, value = key[:-1], Percent.unquote(value_).decode(encoding) # noqa: PLW2901 except UnicodeDecodeError as exc: raise InvalidHeader(_('%s') % (exc,)) from None else: - value = value.decode('ISO8859-1') + value = value.decode('ISO8859-1') # noqa: PLW2901 key_, asterisk, num = key.rpartition(b'*') if asterisk: try: @@ -153,7 +153,7 @@ def _rfc2231_and_continuation_params(cls, params: Iterator[Any]) -> Iterator[tup continuations.setdefault(key_, {})[num] = value continue else: - value = value.decode('ISO8859-1') + value = value.decode('ISO8859-1') # noqa: PLW2901 yield key, value for key, lines in continuations.items(): @@ -305,7 +305,7 @@ def version(self, version) -> None: self.params['version'] = str(version) -class _AcceptElement(HeaderElement): +class _AcceptElement(HeaderElement): # noqa: PLW1641 """ An Accept element with quality value. diff --git a/httoop/header/headers.py b/httoop/header/headers.py index cef5975..949e68f 100644 --- a/httoop/header/headers.py +++ b/httoop/header/headers.py @@ -173,8 +173,8 @@ def __encoded_items(self): for key, values in self.items(): element_cls = HEADER.get(key, HeaderElement) if element_cls is not HeaderElement: - key = element_cls.name - key = key.encode('ascii', 'ignore') + key = element_cls.name # noqa: PLW2901 + key = key.encode('ascii', 'ignore') # noqa: PLW2901 if element_cls.list_element: for value in element_cls.split(values): yield key, value diff --git a/httoop/header/messaging.py b/httoop/header/messaging.py index 4c66880..842e883 100644 --- a/httoop/header/messaging.py +++ b/httoop/header/messaging.py @@ -30,7 +30,7 @@ def codec(self) -> Any: for encoding in (self.value, mimetype): if self.CODECS is not None: - encoding = self.CODECS.get(encoding) + encoding = self.CODECS.get(encoding) # noqa: PLW2901 if not isinstance(encoding, (bytes, str)): return encoding try: @@ -143,7 +143,7 @@ class ContentEncoding(CodecElement, HeaderElement, name='Content-Encoding'): is_response_header = True # IANA assigned HTTP Content-Encoding values - CODECS = { + CODECS = { # noqa: RUF012 'gzip': 'application/gzip', 'deflate': 'application/zlib', # TODO: implement the following @@ -453,7 +453,7 @@ class TransferEncoding(_HopByHopElement, CodecElement, HeaderElement, name='Tran is_request_header = True # IANA assigned HTTP Transfer-Encoding values - CODECS = { + CODECS = { # noqa: RUF012 'chunked': False, 'compress': NotImplementedError, 'deflate': 'application/zlib', diff --git a/httoop/header/ranges.py b/httoop/header/ranges.py index 31881cc..de68fdc 100644 --- a/httoop/header/ranges.py +++ b/httoop/header/ranges.py @@ -114,7 +114,7 @@ def prevent_denial_of_service(self) -> None: raise InvalidHeader(_('duplicated range.')) byterange.update(range_) - if self.stddev([(x[1] or float('inf')) - (x[0] or 0) for x in self.ranges]) > 2.0: + if self.stddev([(x[1] or float('inf')) - (x[0] or 0) for x in self.ranges]) > 2.0: # noqa: PLR2004 raise InvalidHeader(_('ranges exceeding high standard deviation')) @classmethod @@ -141,5 +141,5 @@ def positions(self) -> Iterator[tuple[int, int, int] | tuple[int, int, None]]: def get_range_content(self, fd: BytesIO) -> Iterator[bytes]: for offset, whence, length in self.positions: fd.seek(offset, whence) - length = () if length is None else (length,) + length = () if length is None else (length,) # noqa: PLW2901 yield fd.read(*length) diff --git a/httoop/messages/body.py b/httoop/messages/body.py index ea5aa50..809b505 100644 --- a/httoop/messages/body.py +++ b/httoop/messages/body.py @@ -246,7 +246,7 @@ def __content_iter(self): if not data: continue if isinstance(data, str): - data = data.encode(self.encoding) + data = data.encode(self.encoding) # noqa: PLW2901 elif not isinstance(data, bytes): # pragma: no cover raise TypeError(f'Iterable contained non-bytes: {type(data).__name__!r}') yield data diff --git a/httoop/messages/protocol.py b/httoop/messages/protocol.py index 52f6de3..abb41b8 100644 --- a/httoop/messages/protocol.py +++ b/httoop/messages/protocol.py @@ -16,7 +16,7 @@ __all__ = ('Protocol',) -class Protocol(Semantic): +class Protocol(Semantic): # noqa: PLW1641 """The HTTP protocol version.""" __slots__ = ('__protocol', 'name') diff --git a/httoop/meta.py b/httoop/meta.py index 98d72be..911c82f 100644 --- a/httoop/meta.py +++ b/httoop/meta.py @@ -6,7 +6,7 @@ __all__ = ['Semantic'] -class Semantic: +class Semantic: # noqa: PLW1641 """Implements the HTTP Semantic interface.""" __slots__ = () diff --git a/httoop/parser.py b/httoop/parser.py index 87ea46f..d7e089b 100644 --- a/httoop/parser.py +++ b/httoop/parser.py @@ -96,7 +96,7 @@ def on_body_complete(self) -> None: def on_message_complete(self) -> Response | Request: message = self.message - assert self.message + assert self.message # noqa: S101 self.message = None return message @@ -150,7 +150,7 @@ def parse_startline(self) -> bool | None: requestline, self.buffer = self.buffer.split(self.line_end, 1) # parse request line - assert self.message + assert self.message # noqa: S101 try: self.message.parse(bytes(requestline)) except (InvalidLine, InvalidURI) as exc: @@ -189,7 +189,7 @@ def _parse_header(self, headers: bytearray) -> None: if self.state['_header_section_size'] >= self.max_header_section_size: raise REQUEST_HEADER_FIELDS_TOO_LARGE(_('Maximum allowed header section size (%d) reached.') % (self.max_header_section_size,)) try: - assert self.message + assert self.message # noqa: S101 self.message.headers.parse(bytes(headers)) except InvalidHeaderSize as exc: raise REQUEST_HEADER_FIELDS_TOO_LARGE(str(exc)) from exc @@ -210,7 +210,7 @@ def determine_message_length(self) -> None: # RFC 2616 Section 4.4 # get message length - assert self.message + assert self.message # noqa: S101 message = self.message if 'Transfer-Encoding' in message.headers and 'Content-Length' in message.headers: raise BAD_REQUEST(_('Invalid Content-Length and Transfer-Encoding combination header.')) @@ -232,7 +232,7 @@ def determine_message_length(self) -> None: raise PAYLOAD_TOO_LARGE(_('Maximum content size (%d) reached') % (self.max_body_size,)) def parse_body_with_message_length(self) -> bool | None: - assert self.message + assert self.message # noqa: S101 body, self.buffer = self.buffer[:self.message_length], self.buffer[self.message_length:] try: self.message.body.parse(bytes(body)) @@ -249,7 +249,7 @@ def parse_body_with_message_length(self) -> bool | None: return None def parse_chunked_body(self) -> bool: - assert self.message + assert self.message # noqa: S101 if self.state['trailer']: return self.parse_trailers() if self.line_end not in self.buffer: @@ -323,7 +323,7 @@ def parse_trailers(self) -> bool: def merge_trailer_into_header(self) -> None: message = self.message - assert self.message + assert self.message # noqa: S101 for name in message.headers.values('Trailer'): value = self.trailers.pop(name, None) if value is not None: @@ -334,7 +334,7 @@ def merge_trailer_into_header(self) -> None: del self.trailers def set_body_content_encoding(self) -> None: - assert self.message + assert self.message # noqa: S101 if 'Content-Encoding' in self.message.headers: try: self.message.body.content_encoding = self.message.headers.element('Content-Encoding') @@ -343,12 +343,12 @@ def set_body_content_encoding(self) -> None: raise NOT_IMPLEMENTED(str(exc)) from exc def set_body_content_type(self) -> None: - assert self.message + assert self.message # noqa: S101 if 'Content-Type' in self.message.headers: self.message.body.mimetype = self.message.headers.element('Content-Type') def set_content_length(self) -> None: - assert self.message + assert self.message # noqa: S101 if 'Content-Length' not in self.message.headers: self.message.headers['Content-Length'] = str(len(self.message.body)).encode('ASCII') if self.chunked: diff --git a/httoop/semantic/message.py b/httoop/semantic/message.py index 8a27753..82361e4 100644 --- a/httoop/semantic/message.py +++ b/httoop/semantic/message.py @@ -45,7 +45,7 @@ def chunked(self, chunked) -> None: self.message.headers.pop('Transfer-Encoding') @contextmanager - def _composing(self) -> Iterator[None]: + def _composing(self) -> Iterator[None]: # noqa: PLR6301 yield def __iter__(self) -> Iterator[bytes]: diff --git a/httoop/semantic/response.py b/httoop/semantic/response.py index 00b0c4d..5fa6081 100644 --- a/httoop/semantic/response.py +++ b/httoop/semantic/response.py @@ -28,7 +28,7 @@ def prepare(self) -> None: response = self.response status = int(response.status) - if status < 200 or status in {204, 205, 304}: + if status < 200 or status in {204, 205, 304}: # noqa: PLR2004 # 1XX, 204 NO_CONTENT, 205 RESET_CONTENT, 304 NOT_MODIFIED response.body = None @@ -47,7 +47,7 @@ def prepare(self) -> None: for header in STATUSES[status].header_to_remove: response.headers.pop(header, None) - if status == 405: + if status == 405: # noqa: PLR2004 response.headers.setdefault('Allow', 'GET, HEAD') self.close = self.close @@ -55,12 +55,12 @@ def prepare(self) -> None: if 'Content-Type' not in response.headers and response.body.mimetype and response.body: response.headers['Content-Type'] = bytes(response.body.mimetype) - if (response.status == 200 and response.body.fileable and not self.chunked and 'Etag' in response.headers) or 'Last-Modified' in response.headers: + if (response.status == 200 and response.body.fileable and not self.chunked and 'Etag' in response.headers) or 'Last-Modified' in response.headers: # noqa: PLR2004 response.headers.setdefault('Accept-Ranges', b'bytes') self.prepare_ranges() - if response.status == 416: + if response.status == 416: # noqa: PLR2004 response.headers.set_element('Content-Range', 'bytes', None, response.headers.get('Content-Length')) if self.request.method == 'TRACE': @@ -85,7 +85,7 @@ def range_conditions(self) -> Iterator[bool | Body]: response = self.response yield response.protocol >= (1, 1) yield self.request.protocol >= (1, 1) - yield response.status == 200 + yield response.status == 200 # noqa: PLR2004 yield 'Range' in self.request.headers yield self.request.method == 'GET' yield response.headers.element('Accept-Ranges') == 'bytes' @@ -111,7 +111,7 @@ def prepare_range(self, range_: Range) -> bool: response.headers['Content-Length'] = str(len(response.body)).encode('ASCII') # FIXME: len(response.body) causes the whole body to be generated return True - def multipart_byteranges(self, range_body: Iterator[Any], range_: Range, content_length: str, content_type: str) -> Iterator[Body]: + def multipart_byteranges(self, range_body: Iterator[Any], range_: Range, content_length: str, content_type: str) -> Iterator[Body]: # noqa: PLR6301 for content, byterange in zip(range_body, range_.ranges): body = Body(content) body.headers['Content-Type'] = content_type @@ -127,7 +127,7 @@ def __close_constraints(self): # TODO: 100 Continue # 413 Request Entity Too Large # RFC 2616 Section 10.4.14 - yield response.status == 413 + yield response.status == 413 # noqa: PLR2004 yield response.headers.get('Connection') == 'close' diff --git a/httoop/status/__init__.py b/httoop/status/__init__.py index a15a94a..44f8190 100644 --- a/httoop/status/__init__.py +++ b/httoop/status/__init__.py @@ -17,5 +17,5 @@ __all__ = ['REASONS', 'ClientErrorStatus', 'InformationalStatus', 'RedirectStatus', 'ServerErrorStatus', 'Status', 'StatusException', 'SuccessStatus'] for member in STATUSES.values(): - __all__.append(member.__name__) + __all__.append(member.__name__) # noqa: PYI056 globals()[member.__name__] = member diff --git a/httoop/status/status.py b/httoop/status/status.py index d89a50a..4b33798 100644 --- a/httoop/status/status.py +++ b/httoop/status/status.py @@ -18,7 +18,7 @@ STATUSES = {} -class Status(Semantic): +class Status(Semantic): # noqa: PLW1641 """ A HTTP Status. @@ -29,23 +29,23 @@ class Status(Semantic): @property def informational(self) -> bool: - return 99 < self.__code < 200 + return 99 < self.__code < 200 # noqa: PLR2004 @property def successful(self) -> bool: - return 199 < self.__code < 300 + return 199 < self.__code < 300 # noqa: PLR2004 @property def redirection(self) -> bool: - return 299 < self.__code < 400 + return 299 < self.__code < 400 # noqa: PLR2004 @property def client_error(self) -> bool: - return 399 < self.__code < 500 + return 399 < self.__code < 500 # noqa: PLR2004 @property def server_error(self) -> bool: - return 499 < self.__code < 600 + return 499 < self.__code < 600 # noqa: PLR2004 # aliases @property @@ -96,16 +96,16 @@ def __init_subclass__(cls, code=None, **kwargs): if code is None: return - if not (100 <= code <= 599): + if not (100 <= code <= 599): # noqa: PLR2004 raise RuntimeError('HTTP status code must be between 100 and 599', code, cls) - if code < 200: + if code < 200: # noqa: PLR2004 expected = 'InformationalStatus' - elif code < 300: + elif code < 300: # noqa: PLR2004 expected = 'SuccessStatus' - elif code < 400: + elif code < 400: # noqa: PLR2004 expected = 'RedirectStatus' - elif code < 500: + elif code < 500: # noqa: PLR2004 expected = 'ClientErrorStatus' else: expected = 'ServerErrorStatus' @@ -183,7 +183,7 @@ def set(self, status: Any) -> None: self.__code, self.__reason = status.code, status.reason else: raise TypeError('invalid status') - if not (99 < self.__code < 600): + if not (99 < self.__code < 600): # noqa: PLR2004 raise TypeError('invalid status') def __repr__(self) -> str: diff --git a/httoop/status/types.py b/httoop/status/types.py index fbe2a99..0890f63 100644 --- a/httoop/status/types.py +++ b/httoop/status/types.py @@ -23,7 +23,7 @@ def headers(self) -> dict[str, str]: @property def body(self): if not hasattr(self, '_body'): - from httoop.messages.body import Body + from httoop.messages.body import Body # noqa: PLC0415 self._body = Body(mimetype='application/json') self._body.data = self.to_dict() diff --git a/httoop/uri/percent_encoding.py b/httoop/uri/percent_encoding.py index 8482ccc..b46705d 100644 --- a/httoop/uri/percent_encoding.py +++ b/httoop/uri/percent_encoding.py @@ -11,7 +11,7 @@ class Percent: b"!#$&'()*+,/:;=?@[]" """ - HEX_MAP = {(a + b).encode('ASCII'): bytes((int(a + b, 16),)) for a in '0123456789ABCDEFabcdef' for b in '0123456789ABCDEFabcdef'} + HEX_MAP = {(a + b).encode('ASCII'): bytes((int(a + b, 16),)) for a in '0123456789ABCDEFabcdef' for b in '0123456789ABCDEFabcdef'} # noqa: RUF012 # ABNF GEN_DELIMS = b':/?#[]@' @@ -41,7 +41,7 @@ def _decode_iter(cls, data: bytes) -> Iterator[bytes]: try: yield cls.HEX_MAP[item[:2]] yield item[2:] - except KeyError: + except KeyError: # noqa: PERF203 yield b'%' yield item diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index f11ea9a..ac61e00 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -22,13 +22,13 @@ from httoop.uri.http import HTTP -class URI(Semantic): +class URI(Semantic): # noqa: PLW1641 """Uniform Resource Identifier.""" __slots__ = ('scheme', 'username', 'password', 'host', '_port', 'path', 'query_string', 'fragment') # noqa: RUF023 slots = __slots__ - SCHEMES = {} + SCHEMES = {} # noqa: RUF012 PORT = None encoding = 'UTF-8' @@ -65,7 +65,7 @@ def port(self, port) -> None: if port: try: port = integer(port) - if not 0 < port <= 65535: + if not 0 < port <= 65535: # noqa: PLR2004 raise ValueError except ValueError: raise InvalidURI(_('Invalid port: %r'), port) from None # TODO: TypeError @@ -176,17 +176,17 @@ def dict(self): slots = (key.lstrip('_') for key in self.slots) return {key: getattr(self, key) for key in slots} - @dict.setter + @dict.setter # noqa: A003 def dict(self, uri) -> None: for key in self.slots: - key = key.lstrip('_') + key = key.lstrip('_') # noqa: PLW2901 setattr(self, key, uri.get(key, '')) @property def tuple(self): return tuple(getattr(self, key) for key in self.slots) - @tuple.setter + @tuple.setter # noqa: A003 def tuple(self, tuple_) -> None: (self.scheme, self.username, self.password, self.host, self.port, self.path, self.query_string, self.fragment) = tuple_ diff --git a/httoop/util.py b/httoop/util.py index 89f6a34..1699c90 100644 --- a/httoop/util.py +++ b/httoop/util.py @@ -124,7 +124,7 @@ def integer(number: str | bytes, base=10) -> int: """ if isinstance(number, int): return int(number) - if not number.isdigit() and (base != 16 or number.strip(string.hexdigits if isinstance(number, str) else string.hexdigits.encode('ASCII'))): + if not number.isdigit() and (base != 16 or number.strip(string.hexdigits if isinstance(number, str) else string.hexdigits.encode('ASCII'))): # noqa: PLR2004 raise ValueError() zero = '0' if isinstance(number, str) else b'0' if number != zero and len(number.lstrip(zero)) != len(number): @@ -222,8 +222,8 @@ def __contains__(self, key: bytes | str) -> bool: def get(self, key: bytes | str, default: Any | None = None) -> Any: return dict.get(self, self.formatkey(key), default) - def update(self, E: dict[str, str]) -> None: - for key in E: + def update(self, E: dict[str, str]) -> None: # noqa: N803 + for key in E: # noqa: PLC0206 self[self.formatkey(key)] = self.formatvalue(E[key]) # def setdefault(self, key: str, x: Optional[Union[UserAgent, Server, str, bytes]]=None) -> bytes: diff --git a/pyproject.toml b/pyproject.toml index 7e870cc..34d5303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,7 @@ ignore = [ # "I", "S101", "Q000", "ANN", "ERA", "UP004", "ARG001", "ARG002", "FURB189", # subclass-builtin "TRY301", # raise-within-try + "RUF067", # non-empty-init-module # too much effort: "ANN001", # missing-type-function-argument diff --git a/tests/api/test_date.py b/tests/api/test_date.py index 63961f0..299dd49 100644 --- a/tests/api/test_date.py +++ b/tests/api/test_date.py @@ -6,7 +6,7 @@ dates = [{ - 'datetime': datetime.datetime(1994, 11, 6, 8, 49, 37), + 'datetime': datetime.datetime(1994, 11, 6, 8, 49, 37), # noqa: DTZ001 'timestamp': 784111777.0, 'lt': Date(784111776.0), 'gt': Date(784111778.0), @@ -56,7 +56,7 @@ def test_date_gmtime(date, expected, lt, gt): def test_date_comparing_none(): - d = Date(datetime.datetime(1994, 11, 6, 8, 49, 37)) + d = Date(datetime.datetime(1994, 11, 6, 8, 49, 37)) # noqa: DTZ001 assert d == Date(d) assert not d == None # noqa: E711, SIM201 assert d > None diff --git a/tests/api/test_header.py b/tests/api/test_header.py index d60cc87..95db8cb 100644 --- a/tests/api/test_header.py +++ b/tests/api/test_header.py @@ -74,7 +74,7 @@ def test_pop_header(headers): def test_util_to_unicode(): - assert to_unicode(None) == '' + assert to_unicode(None) == '' # noqa: PLC1901 assert to_unicode(b'\xff') == '\xff' diff --git a/tests/api/test_statemachine.py b/tests/api/test_statemachine.py index b91e1dd..f3a0081 100644 --- a/tests/api/test_statemachine.py +++ b/tests/api/test_statemachine.py @@ -42,7 +42,7 @@ def test_max_uri_length(statemachine): statemachine.MAX_URI_LENGTH = 100 path = b'A' * (statemachine.MAX_URI_LENGTH - 5) statemachine.parse(b'GET /%s HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n' % (path,)) - with pytest.raises(URI_TOO_LONG) as exc: + with pytest.raises(URI_TOO_LONG) as exc: # noqa: PT012 path = b'A' * statemachine.MAX_URI_LENGTH statemachine.parse(b'GET /%s HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n' % (path,)) assert 'The maximum length of the request is 100' in str(exc.value.description) @@ -50,7 +50,7 @@ def test_max_uri_length(statemachine): def test_max_request_line_length(statemachine): statemachine.MAX_URI_LENGTH = 100 - with pytest.raises(URI_TOO_LONG) as exc: + with pytest.raises(URI_TOO_LONG) as exc: # noqa: PT012 path = b'A' * statemachine.MAX_URI_LENGTH statemachine.parse(b'GET /%s HTTP/' % (path,)) statemachine.parse(b'1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n') diff --git a/tests/api/test_status.py b/tests/api/test_status.py index eef8d27..350b758 100644 --- a/tests/api/test_status.py +++ b/tests/api/test_status.py @@ -165,13 +165,13 @@ def test_invalid_status_code(code, response): response.status = code with pytest.raises(TypeError): response.status.code = code - from httoop.status import Status + from httoop.status import Status # noqa: PLC0415 with pytest.raises(TypeError): Status(code) def test_invalid_status_subclasses(): - from httoop.status import ServerErrorStatus + from httoop.status import ServerErrorStatus # noqa: PLC0415 with pytest.raises(RuntimeError): diff --git a/tests/api/test_wsgi.py b/tests/api/test_wsgi.py index 621ba10..6044581 100644 --- a/tests/api/test_wsgi.py +++ b/tests/api/test_wsgi.py @@ -171,7 +171,7 @@ def test_essential_parameters(): def test_client_reraising(): client = WSGIClient({'CONTENT_TYPE': 'text/html', 'CONTENT_LENGTH': '0', 'HTTP_HOST': 'foobar'}) client.headers_sent = True - with pytest.raises(ValueError) as exc: + with pytest.raises(ValueError) as exc: # noqa: PT011 client(application15) assert exc.value.args[0] is True diff --git a/tests/headers/test_cookies.py b/tests/headers/test_cookies.py index 76808b8..5c71786 100644 --- a/tests/headers/test_cookies.py +++ b/tests/headers/test_cookies.py @@ -23,7 +23,7 @@ def test_cookie(cookie, values, headers): (('PREF=ID=1111111111111111:FF=0:TM=1442750337:LM=1442750337:V=1:S=OzLcd0aN7JCguSAs; expires=Thu, 31-Dec-2015 16:02:17 GMT; path=/; domain=.google.de, NID=71=qx1aDrIv1ZCfe9nzprBX_6_GMe5jmnD2RniOFz5UINXwR_3TQU0Kon20XczY4aUNlt75z_2r1wHOJw4FKL9RMUCo5QIEbmKGw3W4U7nkpZZolbPBCGbw6RN2N0p7D3q6fhXQbg; expires=Mon, 21-Mar-2016 11:58:57 GMT; path=/; domain=.google.de; HttpOnly', { 'PREF': { 'cookie_value': 'ID=1111111111111111:FF=0:TM=1442750337:LM=1442750337:V=1:S=OzLcd0aN7JCguSAs', - 'expires': datetime(2015, 12, 31, 16, 2, 17), + 'expires': datetime(2015, 12, 31, 16, 2, 17), # noqa: DTZ001 'path': '/', 'domain': '.google.de', 'httponly': False, @@ -33,7 +33,7 @@ def test_cookie(cookie, values, headers): }, 'NID': { 'cookie_value': '71=qx1aDrIv1ZCfe9nzprBX_6_GMe5jmnD2RniOFz5UINXwR_3TQU0Kon20XczY4aUNlt75z_2r1wHOJw4FKL9RMUCo5QIEbmKGw3W4U7nkpZZolbPBCGbw6RN2N0p7D3q6fhXQbg', - 'expires': datetime(2016, 3, 21, 11, 58, 57), + 'expires': datetime(2016, 3, 21, 11, 58, 57), # noqa: DTZ001 'path': '/', 'domain': '.google.de', 'httponly': True, diff --git a/tests/messaging/test_request_codecs.py b/tests/messaging/test_request_codecs.py index 5d76d48..6aa7b03 100644 --- a/tests/messaging/test_request_codecs.py +++ b/tests/messaging/test_request_codecs.py @@ -16,7 +16,7 @@ def test_json(body): def test_invalid_json(body): body.mimetype = 'application/json' - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 body.decode('{"foo" "bar"}') @@ -159,7 +159,7 @@ def check_encoding_dict(body, data): def check_raises(body, chars, type_, exception): for chr_ in chars: - with pytest.raises(exception): + with pytest.raises(exception): # noqa: PT012 body.set(chr_) type_(body) diff --git a/tests/tc2231/test_tc2231.py b/tests/tc2231/test_tc2231.py index 1b402e7..dcf603a 100644 --- a/tests/tc2231/test_tc2231.py +++ b/tests/tc2231/test_tc2231.py @@ -355,13 +355,13 @@ def test_attabspathwin(content_disposition): def test_attcdate(content_disposition): h = content_disposition(b'Content-Disposition: attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500"') - assert h.creation_date == datetime.datetime(1997, 2, 12, 16, 29, 51) + assert h.creation_date == datetime.datetime(1997, 2, 12, 16, 29, 51) # noqa: DTZ001 # assert h.creation_date == 855761391.0 # FIXME: assert == 855761391.0 def test_attmdate(content_disposition): h = content_disposition(b'Content-Disposition: attachment; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"') - assert h.modification_date == datetime.datetime(1997, 2, 12, 16, 29, 51) + assert h.modification_date == datetime.datetime(1997, 2, 12, 16, 29, 51) # noqa: DTZ001 # assert h.modification_date == 855761391.0 # FIXME: assert == 855761391.0 diff --git a/tests/uri/test_uri_query.py b/tests/uri/test_uri_query.py index cdb6264..aa28240 100644 --- a/tests/uri/test_uri_query.py +++ b/tests/uri/test_uri_query.py @@ -7,8 +7,8 @@ (b'', ()), (b'&', ()), (b'&&', ()), - pytest.param(b'=', (('', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), - pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skipif(True, reason='Dunno')), + pytest.param(b'=', (('', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 + pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 (b'a', (('a', ''),)), (b'a=', (('a', ''),)), (b'&a=b', (('a', 'b'),)), @@ -26,10 +26,10 @@ def test_query_string_parse(query_string, query): @pytest.mark.parametrize('query_string,query', [ (b'', ()), - pytest.param(b'=', (('', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), - pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skipif(True, reason='Dunno')), + pytest.param(b'=', (('', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 + pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 (b'a', (('a', ''),)), - pytest.param(b'a=', (('a', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), + pytest.param(b'a=', (('a', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 (b'a=b', (('a', 'b'),)), (b'a=b&b=c&d=f', (('a', 'b'), ('b', 'c'), ('d', 'f'))), (b'a=a+b&b=b+c', (('a', 'a b'), ('b', 'b c'))), diff --git a/tests/uri/test_uri_schemes.py b/tests/uri/test_uri_schemes.py index 9ae85ab..9f6d4a6 100644 --- a/tests/uri/test_uri_schemes.py +++ b/tests/uri/test_uri_schemes.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize('url,expected', [ ('ftp://ftp.is.co.za/rfc/rfc1808.txt', ('ftp', '', '', 'ftp.is.co.za', 21, '/rfc/rfc1808.txt', '', '')), ('http://www.ietf.org/rfc/rfc2396.txt', ('http', '', '', 'www.ietf.org', 80, '/rfc/rfc2396.txt', '', '')), - pytest.param('ldap://[2001:db8::7]/c=GB?objectClass?one', ('ldap', '', '', '[2001:db8::7]', 389, '/c=GB', 'objectClass?one', ''), marks=pytest.mark.skipif(True, reason='Parse query in ldap URI?')), + pytest.param('ldap://[2001:db8::7]/c=GB?objectClass?one', ('ldap', '', '', '[2001:db8::7]', 389, '/c=GB', 'objectClass?one', ''), marks=pytest.mark.skipif(True, reason='Parse query in ldap URI?')), # noqa: FBT003 ('mailto:John.Doe@example.com', ('mailto', '', '', '', None, 'John.Doe@example.com', '', '')), ('news:comp.infosystems.www.servers.unix', ('news', '', '', '', None, 'comp.infosystems.www.servers.unix', '', '')), ('tel:+1-816-555-1212', ('tel', '', '', '', None, '+1-816-555-1212', '', '')), From e6867603831679730db91138ac5e5b4e2b950a1a Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:17:01 +0200 Subject: [PATCH 26/40] RUF012 --- httoop/authentication/__init__.py | 4 ++-- httoop/header/messaging.py | 6 +++--- httoop/uri/percent_encoding.py | 4 ++-- httoop/uri/uri.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/httoop/authentication/__init__.py b/httoop/authentication/__init__.py index a8cf160..8004ff6 100644 --- a/httoop/authentication/__init__.py +++ b/httoop/authentication/__init__.py @@ -68,7 +68,7 @@ class AuthRequestElement(AuthElement): encoding = 'ASCII' - schemes = { # noqa: RUF012 + schemes: ClassVar = { 'basic': BasicAuthRequestScheme, 'digest': SecureDigestAuthRequestScheme, } @@ -103,7 +103,7 @@ def password(self, password) -> None: class AuthResponseElement(AuthElement): - schemes = { # noqa: RUF012 + schemes: ClassVar = { 'basic': BasicAuthResponseScheme, 'digest': DigestAuthResponseScheme, } diff --git a/httoop/header/messaging.py b/httoop/header/messaging.py index 842e883..6d16c9f 100644 --- a/httoop/header/messaging.py +++ b/httoop/header/messaging.py @@ -2,7 +2,7 @@ from __future__ import annotations import re -from typing import Any +from typing import Any, ClassVar from httoop.codecs import lookup from httoop.exceptions import InvalidDate, InvalidHeader @@ -143,7 +143,7 @@ class ContentEncoding(CodecElement, HeaderElement, name='Content-Encoding'): is_response_header = True # IANA assigned HTTP Content-Encoding values - CODECS = { # noqa: RUF012 + CODECS: ClassVar = { 'gzip': 'application/gzip', 'deflate': 'application/zlib', # TODO: implement the following @@ -453,7 +453,7 @@ class TransferEncoding(_HopByHopElement, CodecElement, HeaderElement, name='Tran is_request_header = True # IANA assigned HTTP Transfer-Encoding values - CODECS = { # noqa: RUF012 + CODECS: ClassVar = { 'chunked': False, 'compress': NotImplementedError, 'deflate': 'application/zlib', diff --git a/httoop/uri/percent_encoding.py b/httoop/uri/percent_encoding.py index b46705d..560d46b 100644 --- a/httoop/uri/percent_encoding.py +++ b/httoop/uri/percent_encoding.py @@ -1,4 +1,4 @@ -from typing import Iterator +from typing import ClassVar, Iterator class Percent: @@ -11,7 +11,7 @@ class Percent: b"!#$&'()*+,/:;=?@[]" """ - HEX_MAP = {(a + b).encode('ASCII'): bytes((int(a + b, 16),)) for a in '0123456789ABCDEFabcdef' for b in '0123456789ABCDEFabcdef'} # noqa: RUF012 + HEX_MAP: ClassVar = {(a + b).encode('ASCII'): bytes((int(a + b, 16),)) for a in '0123456789ABCDEFabcdef' for b in '0123456789ABCDEFabcdef'} # ABNF GEN_DELIMS = b':/?#[]@' diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index ac61e00..339eb69 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -9,7 +9,7 @@ import re from socket import AF_INET, AF_INET6, inet_ntop, inet_pton -from typing import TYPE_CHECKING, Any, Iterator +from typing import TYPE_CHECKING, Any, ClassVar, Iterator from httoop.exceptions import InvalidURI from httoop.meta import Semantic @@ -28,7 +28,7 @@ class URI(Semantic): # noqa: PLW1641 __slots__ = ('scheme', 'username', 'password', 'host', '_port', 'path', 'query_string', 'fragment') # noqa: RUF023 slots = __slots__ - SCHEMES = {} # noqa: RUF012 + SCHEMES: ClassVar = {} PORT = None encoding = 'UTF-8' From a19c4d4186f3aff8a5d54df3157f2e32dccbb527 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:23:37 +0200 Subject: [PATCH 27/40] FBT001 --- httoop/codecs/__init__.py | 2 +- httoop/gateway/wsgi.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/httoop/codecs/__init__.py b/httoop/codecs/__init__.py index f742034..bb9c007 100644 --- a/httoop/codecs/__init__.py +++ b/httoop/codecs/__init__.py @@ -18,7 +18,7 @@ __all__ = ['CODECS', 'Codec', 'application', 'audio', 'example', 'image', 'message', 'model', 'multipart', 'text', 'video'] -def lookup(encoding: str, raise_errors: bool = True) -> Any: # noqa: FBT001, FBT002 +def lookup(encoding: str, *, raise_errors: bool = True) -> Any: type_ = '%s/*' % (encoding.split('/', 1)[0],) return CODECS.get(encoding) or CODECS.get(type_) or (raise_errors and CODECS[encoding]) or None diff --git a/httoop/gateway/wsgi.py b/httoop/gateway/wsgi.py index 1b56e09..e83be9b 100644 --- a/httoop/gateway/wsgi.py +++ b/httoop/gateway/wsgi.py @@ -32,7 +32,7 @@ def read(self, *size): class WSGI: """A mixin class which implements the WSGI interface.""" - def __init__(self, environ: dict[str, str] | None = None, use_path_info: bool = False, *args, **kwargs) -> None: # noqa: FBT001, FBT002 + def __init__(self, environ: dict[str, str] | None = None, *args, use_path_info: bool = False, **kwargs) -> None: self.use_path_info = use_path_info super().__init__() self.exc_info = None From 65e2753fd5f18e392f8ece9a81c8fcfa200e116b Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:28:34 +0200 Subject: [PATCH 28/40] ruff --- httoop/codecs/application/gzip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/httoop/codecs/application/gzip.py b/httoop/codecs/application/gzip.py index 8670025..56eb335 100644 --- a/httoop/codecs/application/gzip.py +++ b/httoop/codecs/application/gzip.py @@ -33,17 +33,17 @@ def decode(cls, data: bytes, charset: None = None, mimetype: None = None) -> str @classmethod def iterencode(cls, data, charset=None, mimetype=None): - try: # noqa: PLW0717 - out = io.BytesIO() + out = io.BytesIO() + try: with gzip.GzipFile(fileobj=out, mode='w', compresslevel=cls.compression_level) as fd: for part in data: fd.write(Codec.encode(part, charset)) yield out.getvalue() out.seek(0) out.truncate() - yield out.getvalue() except zlib.error: # pragma: no cover raise EncodeError(_('Invalid gzip data.')) from None + yield out.getvalue() @classmethod def iterdecode(cls, data, charset=None, mimetype=None): From f10e54973d13bc2950c5c5a2d7e8190a8a4f36b7 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:45:05 +0200 Subject: [PATCH 29/40] PLW2901 --- httoop/codecs/multipart/multipart.py | 3 +-- httoop/header/element.py | 10 +++++----- httoop/header/headers.py | 8 +++----- httoop/header/messaging.py | 8 ++++++-- httoop/header/ranges.py | 4 ++-- httoop/messages/body.py | 8 ++++---- httoop/uri/uri.py | 4 ++-- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/httoop/codecs/multipart/multipart.py b/httoop/codecs/multipart/multipart.py index 9fe570a..207a30f 100644 --- a/httoop/codecs/multipart/multipart.py +++ b/httoop/codecs/multipart/multipart.py @@ -43,8 +43,7 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | for part in parts: if not part.startswith(b'\r\n'): raise DecodeError(_('Invalid boundary end: %r'), part[:2].decode('ISO8859-1')) - part = part[2:] # noqa: PLW2901 - headers, separator, content = part.partition(b'\r\n\r\n') + headers, separator, content = part[2:].partition(b'\r\n\r\n') if not separator: raise DecodeError(_('Multipart does not contain CRLF header separator')) if not content.endswith(b'\r\n'): diff --git a/httoop/header/element.py b/httoop/header/element.py index 4a56581..ba61a4a 100644 --- a/httoop/header/element.py +++ b/httoop/header/element.py @@ -127,20 +127,20 @@ def _sanitize_encoding(cls, charset: str) -> str: def _rfc2231_and_continuation_params(cls, params: Iterator[Any]) -> Iterator[tuple[bytes, str]]: # TODO: complexity count = set() continuations = {} - for key, value, quoted in params: + for key, val, quoted in params: if key in count: raise InvalidHeader(_('Parameter given twice: %r'), key.decode('ISO8859-1')) count.add(key) if b'*' in key: - if key.endswith(b'*') and not quoted and not value.startswith(b"'") and value.count(b"'") >= 2: # noqa: PLR2004 - charset, _language, value_ = value.split(b"'", 2) + if key.endswith(b'*') and not quoted and not val.startswith(b"'") and val.count(b"'") >= 2: # noqa: PLR2004 + charset, _language, value_ = val.split(b"'", 2) encoding = cls._sanitize_encoding(charset.decode('ASCII', 'replace')) try: key, value = key[:-1], Percent.unquote(value_).decode(encoding) # noqa: PLW2901 except UnicodeDecodeError as exc: raise InvalidHeader(_('%s') % (exc,)) from None else: - value = value.decode('ISO8859-1') # noqa: PLW2901 + value = val.decode('ISO8859-1') key_, asterisk, num = key.rpartition(b'*') if asterisk: try: @@ -153,7 +153,7 @@ def _rfc2231_and_continuation_params(cls, params: Iterator[Any]) -> Iterator[tup continuations.setdefault(key_, {})[num] = value continue else: - value = value.decode('ISO8859-1') # noqa: PLW2901 + value = val.decode('ISO8859-1') yield key, value for key, lines in continuations.items(): diff --git a/httoop/header/headers.py b/httoop/header/headers.py index 949e68f..0a2372c 100644 --- a/httoop/header/headers.py +++ b/httoop/header/headers.py @@ -172,14 +172,12 @@ def __items(self): def __encoded_items(self): for key, values in self.items(): element_cls = HEADER.get(key, HeaderElement) - if element_cls is not HeaderElement: - key = element_cls.name # noqa: PLW2901 - key = key.encode('ascii', 'ignore') # noqa: PLW2901 + name = (element_cls.name if element_cls is not HeaderElement else key).encode('ascii', 'ignore') if element_cls.list_element: for value in element_cls.split(values): - yield key, value + yield name, value else: - yield key, values + yield name, values def __repr__(self) -> str: return f'' diff --git a/httoop/header/messaging.py b/httoop/header/messaging.py index 6d16c9f..7533c88 100644 --- a/httoop/header/messaging.py +++ b/httoop/header/messaging.py @@ -12,6 +12,9 @@ class CodecElement: + value: str + mimetype: str + name: str CODECS = None raise_on_missing_codec = True @@ -28,9 +31,10 @@ def codec(self) -> Any: except AttributeError: mimetype = self.value - for encoding in (self.value, mimetype): + for val in (self.value, mimetype): + encoding = val if self.CODECS is not None: - encoding = self.CODECS.get(encoding) # noqa: PLW2901 + encoding = self.CODECS.get(encoding) if not isinstance(encoding, (bytes, str)): return encoding try: diff --git a/httoop/header/ranges.py b/httoop/header/ranges.py index de68fdc..570a98f 100644 --- a/httoop/header/ranges.py +++ b/httoop/header/ranges.py @@ -141,5 +141,5 @@ def positions(self) -> Iterator[tuple[int, int, int] | tuple[int, int, None]]: def get_range_content(self, fd: BytesIO) -> Iterator[bytes]: for offset, whence, length in self.positions: fd.seek(offset, whence) - length = () if length is None else (length,) # noqa: PLW2901 - yield fd.read(*length) + args = () if length is None else (length,) + yield fd.read(*args) diff --git a/httoop/messages/body.py b/httoop/messages/body.py index 809b505..f639b66 100644 --- a/httoop/messages/body.py +++ b/httoop/messages/body.py @@ -246,10 +246,10 @@ def __content_iter(self): if not data: continue if isinstance(data, str): - data = data.encode(self.encoding) # noqa: PLW2901 - elif not isinstance(data, bytes): # pragma: no cover - raise TypeError(f'Iterable contained non-bytes: {type(data).__name__!r}') - yield data + yield data.encode(self.encoding) + elif isinstance(data, bytes): + yield data + raise TypeError(f'Iterable contained non-bytes: {type(data).__name__!r}') # pragma: no cover finally: self.seek(t) diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index 339eb69..b741a03 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -178,8 +178,8 @@ def dict(self): @dict.setter # noqa: A003 def dict(self, uri) -> None: - for key in self.slots: - key = key.lstrip('_') # noqa: PLW2901 + for name in self.slots: + key = name.lstrip('_') setattr(self, key, uri.get(key, '')) @property From a71880552c4424efd54cdebbdafeaff2eb96a41a Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 00:56:37 +0200 Subject: [PATCH 30/40] fixup! ruff --- httoop/header/ranges.py | 3 ++- httoop/semantic/message.py | 5 +++-- httoop/semantic/response.py | 16 +++++++++------- httoop/status/__init__.py | 2 +- httoop/status/status.py | 23 ++++++++++++----------- httoop/uri/percent_encoding.py | 7 ++++--- httoop/uri/uri.py | 4 +++- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/httoop/header/ranges.py b/httoop/header/ranges.py index 570a98f..322f4f1 100644 --- a/httoop/header/ranges.py +++ b/httoop/header/ranges.py @@ -114,7 +114,8 @@ def prevent_denial_of_service(self) -> None: raise InvalidHeader(_('duplicated range.')) byterange.update(range_) - if self.stddev([(x[1] or float('inf')) - (x[0] or 0) for x in self.ranges]) > 2.0: # noqa: PLR2004 + max_dev = 2.0 + if self.stddev([(x[1] or float('inf')) - (x[0] or 0) for x in self.ranges]) > max_dev: raise InvalidHeader(_('ranges exceeding high standard deviation')) @classmethod diff --git a/httoop/semantic/message.py b/httoop/semantic/message.py index 82361e4..3dcb508 100644 --- a/httoop/semantic/message.py +++ b/httoop/semantic/message.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from typing import Iterator +from typing import Iterator, override class ComposedMessage: @@ -44,8 +44,9 @@ def chunked(self, chunked) -> None: if not te: self.message.headers.pop('Transfer-Encoding') + @override @contextmanager - def _composing(self) -> Iterator[None]: # noqa: PLR6301 + def _composing(self) -> Iterator[None]: yield def __iter__(self) -> Iterator[bytes]: diff --git a/httoop/semantic/response.py b/httoop/semantic/response.py index 5fa6081..6d1e248 100644 --- a/httoop/semantic/response.py +++ b/httoop/semantic/response.py @@ -1,3 +1,4 @@ +# ruff: file-ignore[PLR2004] from __future__ import annotations from email.generator import _make_boundary as make_boundary @@ -28,7 +29,7 @@ def prepare(self) -> None: response = self.response status = int(response.status) - if status < 200 or status in {204, 205, 304}: # noqa: PLR2004 + if status < 200 or status in {204, 205, 304}: # 1XX, 204 NO_CONTENT, 205 RESET_CONTENT, 304 NOT_MODIFIED response.body = None @@ -47,7 +48,7 @@ def prepare(self) -> None: for header in STATUSES[status].header_to_remove: response.headers.pop(header, None) - if status == 405: # noqa: PLR2004 + if status == 405: response.headers.setdefault('Allow', 'GET, HEAD') self.close = self.close @@ -55,12 +56,12 @@ def prepare(self) -> None: if 'Content-Type' not in response.headers and response.body.mimetype and response.body: response.headers['Content-Type'] = bytes(response.body.mimetype) - if (response.status == 200 and response.body.fileable and not self.chunked and 'Etag' in response.headers) or 'Last-Modified' in response.headers: # noqa: PLR2004 + if (response.status == 200 and response.body.fileable and not self.chunked and 'Etag' in response.headers) or 'Last-Modified' in response.headers: response.headers.setdefault('Accept-Ranges', b'bytes') self.prepare_ranges() - if response.status == 416: # noqa: PLR2004 + if response.status == 416: response.headers.set_element('Content-Range', 'bytes', None, response.headers.get('Content-Length')) if self.request.method == 'TRACE': @@ -85,7 +86,7 @@ def range_conditions(self) -> Iterator[bool | Body]: response = self.response yield response.protocol >= (1, 1) yield self.request.protocol >= (1, 1) - yield response.status == 200 # noqa: PLR2004 + yield response.status == 200 yield 'Range' in self.request.headers yield self.request.method == 'GET' yield response.headers.element('Accept-Ranges') == 'bytes' @@ -111,7 +112,8 @@ def prepare_range(self, range_: Range) -> bool: response.headers['Content-Length'] = str(len(response.body)).encode('ASCII') # FIXME: len(response.body) causes the whole body to be generated return True - def multipart_byteranges(self, range_body: Iterator[Any], range_: Range, content_length: str, content_type: str) -> Iterator[Body]: # noqa: PLR6301 + @staticmethod + def multipart_byteranges(range_body: Iterator[Any], range_: Range, content_length: str, content_type: str) -> Iterator[Body]: for content, byterange in zip(range_body, range_.ranges): body = Body(content) body.headers['Content-Type'] = content_type @@ -127,7 +129,7 @@ def __close_constraints(self): # TODO: 100 Continue # 413 Request Entity Too Large # RFC 2616 Section 10.4.14 - yield response.status == 413 # noqa: PLR2004 + yield response.status == 413 yield response.headers.get('Connection') == 'close' diff --git a/httoop/status/__init__.py b/httoop/status/__init__.py index 44f8190..719917c 100644 --- a/httoop/status/__init__.py +++ b/httoop/status/__init__.py @@ -17,5 +17,5 @@ __all__ = ['REASONS', 'ClientErrorStatus', 'InformationalStatus', 'RedirectStatus', 'ServerErrorStatus', 'Status', 'StatusException', 'SuccessStatus'] for member in STATUSES.values(): - __all__.append(member.__name__) # noqa: PYI056 + __all__ += member.__name__ # noqa: PLE0605 globals()[member.__name__] = member diff --git a/httoop/status/status.py b/httoop/status/status.py index 4b33798..779bcc4 100644 --- a/httoop/status/status.py +++ b/httoop/status/status.py @@ -4,6 +4,7 @@ .. seealso:: :rfc:`2616#section-6.2` .. seealso:: :rfc:`2616#section-10` """ +# ruff: file-ignore[PLR2004] from __future__ import annotations @@ -29,23 +30,23 @@ class Status(Semantic): # noqa: PLW1641 @property def informational(self) -> bool: - return 99 < self.__code < 200 # noqa: PLR2004 + return 99 < self.__code < 200 @property def successful(self) -> bool: - return 199 < self.__code < 300 # noqa: PLR2004 + return 199 < self.__code < 300 @property def redirection(self) -> bool: - return 299 < self.__code < 400 # noqa: PLR2004 + return 299 < self.__code < 400 @property def client_error(self) -> bool: - return 399 < self.__code < 500 # noqa: PLR2004 + return 399 < self.__code < 500 @property def server_error(self) -> bool: - return 499 < self.__code < 600 # noqa: PLR2004 + return 499 < self.__code < 600 # aliases @property @@ -96,16 +97,16 @@ def __init_subclass__(cls, code=None, **kwargs): if code is None: return - if not (100 <= code <= 599): # noqa: PLR2004 + if not (100 <= code <= 599): raise RuntimeError('HTTP status code must be between 100 and 599', code, cls) - if code < 200: # noqa: PLR2004 + if code < 200: expected = 'InformationalStatus' - elif code < 300: # noqa: PLR2004 + elif code < 300: expected = 'SuccessStatus' - elif code < 400: # noqa: PLR2004 + elif code < 400: expected = 'RedirectStatus' - elif code < 500: # noqa: PLR2004 + elif code < 500: expected = 'ClientErrorStatus' else: expected = 'ServerErrorStatus' @@ -183,7 +184,7 @@ def set(self, status: Any) -> None: self.__code, self.__reason = status.code, status.reason else: raise TypeError('invalid status') - if not (99 < self.__code < 600): # noqa: PLR2004 + if not (99 < self.__code < 600): raise TypeError('invalid status') def __repr__(self) -> str: diff --git a/httoop/uri/percent_encoding.py b/httoop/uri/percent_encoding.py index 560d46b..0e066a2 100644 --- a/httoop/uri/percent_encoding.py +++ b/httoop/uri/percent_encoding.py @@ -38,10 +38,11 @@ def _decode_iter(cls, data: bytes) -> Iterator[bytes]: data = data.split(b'%') yield data.pop(0) for item in data: - try: - yield cls.HEX_MAP[item[:2]] + mapped = cls.HEX_MAP.get(item[:2]) + if mapped is not None: + yield mapped yield item[2:] - except KeyError: # noqa: PERF203 + else: yield b'%' yield item diff --git a/httoop/uri/uri.py b/httoop/uri/uri.py index b741a03..201ef41 100644 --- a/httoop/uri/uri.py +++ b/httoop/uri/uri.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: from httoop.uri.http import HTTP +_MAX_PORT = 65535 + class URI(Semantic): # noqa: PLW1641 """Uniform Resource Identifier.""" @@ -65,7 +67,7 @@ def port(self, port) -> None: if port: try: port = integer(port) - if not 0 < port <= 65535: # noqa: PLR2004 + if not 0 < port <= _MAX_PORT: raise ValueError except ValueError: raise InvalidURI(_('Invalid port: %r'), port) from None # TODO: TypeError From 36b22211300a58e705db14e9ec98f4827d0873ea Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:07:43 +0200 Subject: [PATCH 31/40] fixup! PLW2901 --- tests/uri/test_uri_query.py | 10 +++++----- tests/uri/test_uri_schemes.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/uri/test_uri_query.py b/tests/uri/test_uri_query.py index aa28240..2d17f88 100644 --- a/tests/uri/test_uri_query.py +++ b/tests/uri/test_uri_query.py @@ -7,8 +7,8 @@ (b'', ()), (b'&', ()), (b'&&', ()), - pytest.param(b'=', (('', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 - pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 + pytest.param(b'=', (('', ''),), marks=pytest.mark.skip(reason='Dunno')), + pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skip(reason='Dunno')), (b'a', (('a', ''),)), (b'a=', (('a', ''),)), (b'&a=b', (('a', 'b'),)), @@ -26,10 +26,10 @@ def test_query_string_parse(query_string, query): @pytest.mark.parametrize('query_string,query', [ (b'', ()), - pytest.param(b'=', (('', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 - pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 + pytest.param(b'=', (('', ''),), marks=pytest.mark.skip(reason='Dunno')), + pytest.param(b'=a', (('', 'a'),), marks=pytest.mark.skip(reason='Dunno')), (b'a', (('a', ''),)), - pytest.param(b'a=', (('a', ''),), marks=pytest.mark.skipif(True, reason='Dunno')), # noqa: FBT003 + pytest.param(b'a=', (('a', ''),), marks=pytest.mark.skip(reason='Dunno')), (b'a=b', (('a', 'b'),)), (b'a=b&b=c&d=f', (('a', 'b'), ('b', 'c'), ('d', 'f'))), (b'a=a+b&b=b+c', (('a', 'a b'), ('b', 'b c'))), diff --git a/tests/uri/test_uri_schemes.py b/tests/uri/test_uri_schemes.py index 9f6d4a6..e167ddb 100644 --- a/tests/uri/test_uri_schemes.py +++ b/tests/uri/test_uri_schemes.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize('url,expected', [ ('ftp://ftp.is.co.za/rfc/rfc1808.txt', ('ftp', '', '', 'ftp.is.co.za', 21, '/rfc/rfc1808.txt', '', '')), ('http://www.ietf.org/rfc/rfc2396.txt', ('http', '', '', 'www.ietf.org', 80, '/rfc/rfc2396.txt', '', '')), - pytest.param('ldap://[2001:db8::7]/c=GB?objectClass?one', ('ldap', '', '', '[2001:db8::7]', 389, '/c=GB', 'objectClass?one', ''), marks=pytest.mark.skipif(True, reason='Parse query in ldap URI?')), # noqa: FBT003 + pytest.param('ldap://[2001:db8::7]/c=GB?objectClass?one', ('ldap', '', '', '[2001:db8::7]', 389, '/c=GB', 'objectClass?one', ''), marks=pytest.mark.skip(reason='Parse query in ldap URI?')), ('mailto:John.Doe@example.com', ('mailto', '', '', '', None, 'John.Doe@example.com', '', '')), ('news:comp.infosystems.www.servers.unix', ('news', '', '', '', None, 'comp.infosystems.www.servers.unix', '', '')), ('tel:+1-816-555-1212', ('tel', '', '', '', None, '+1-816-555-1212', '', '')), From a16f12390554b5b15560afb8130fe887f930c73b Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:14:32 +0200 Subject: [PATCH 32/40] date --- httoop/date.py | 4 ++-- tests/api/test_date.py | 4 ++-- tests/headers/test_cookies.py | 6 +++--- tests/tc2231/test_tc2231.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/httoop/date.py b/httoop/date.py index cc0b65b..46b6348 100644 --- a/httoop/date.py +++ b/httoop/date.py @@ -9,7 +9,7 @@ import time # import calendar -from datetime import datetime +from datetime import datetime, timezone from email.utils import parsedate_tz from typing import Any, Self @@ -77,7 +77,7 @@ def __init__(self, timeval: Any | None = None) -> None: @property def datetime(self) -> datetime: if self.__datetime is None: - self.__datetime = datetime.utcfromtimestamp(int(self)) # noqa: DTZ004 + self.__datetime = datetime.fromtimestamp(int(self), tz=timezone.utc) return self.__datetime @property diff --git a/tests/api/test_date.py b/tests/api/test_date.py index 299dd49..53f4665 100644 --- a/tests/api/test_date.py +++ b/tests/api/test_date.py @@ -6,7 +6,7 @@ dates = [{ - 'datetime': datetime.datetime(1994, 11, 6, 8, 49, 37), # noqa: DTZ001 + 'datetime': datetime.datetime(1994, 11, 6, 8, 49, 37, tzinfo=datetime.timezone.utc), 'timestamp': 784111777.0, 'lt': Date(784111776.0), 'gt': Date(784111778.0), @@ -56,7 +56,7 @@ def test_date_gmtime(date, expected, lt, gt): def test_date_comparing_none(): - d = Date(datetime.datetime(1994, 11, 6, 8, 49, 37)) # noqa: DTZ001 + d = Date(datetime.datetime(1994, 11, 6, 8, 49, 37, tzinfo=datetime.timezone.utc)) assert d == Date(d) assert not d == None # noqa: E711, SIM201 assert d > None diff --git a/tests/headers/test_cookies.py b/tests/headers/test_cookies.py index 5c71786..5550ad8 100644 --- a/tests/headers/test_cookies.py +++ b/tests/headers/test_cookies.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone import pytest @@ -23,7 +23,7 @@ def test_cookie(cookie, values, headers): (('PREF=ID=1111111111111111:FF=0:TM=1442750337:LM=1442750337:V=1:S=OzLcd0aN7JCguSAs; expires=Thu, 31-Dec-2015 16:02:17 GMT; path=/; domain=.google.de, NID=71=qx1aDrIv1ZCfe9nzprBX_6_GMe5jmnD2RniOFz5UINXwR_3TQU0Kon20XczY4aUNlt75z_2r1wHOJw4FKL9RMUCo5QIEbmKGw3W4U7nkpZZolbPBCGbw6RN2N0p7D3q6fhXQbg; expires=Mon, 21-Mar-2016 11:58:57 GMT; path=/; domain=.google.de; HttpOnly', { 'PREF': { 'cookie_value': 'ID=1111111111111111:FF=0:TM=1442750337:LM=1442750337:V=1:S=OzLcd0aN7JCguSAs', - 'expires': datetime(2015, 12, 31, 16, 2, 17), # noqa: DTZ001 + 'expires': datetime(2015, 12, 31, 16, 2, 17, tzinfo=timezone.utc), 'path': '/', 'domain': '.google.de', 'httponly': False, @@ -33,7 +33,7 @@ def test_cookie(cookie, values, headers): }, 'NID': { 'cookie_value': '71=qx1aDrIv1ZCfe9nzprBX_6_GMe5jmnD2RniOFz5UINXwR_3TQU0Kon20XczY4aUNlt75z_2r1wHOJw4FKL9RMUCo5QIEbmKGw3W4U7nkpZZolbPBCGbw6RN2N0p7D3q6fhXQbg', - 'expires': datetime(2016, 3, 21, 11, 58, 57), # noqa: DTZ001 + 'expires': datetime(2016, 3, 21, 11, 58, 57, tzinfo=timezone.utc), 'path': '/', 'domain': '.google.de', 'httponly': True, diff --git a/tests/tc2231/test_tc2231.py b/tests/tc2231/test_tc2231.py index dcf603a..9c79f77 100644 --- a/tests/tc2231/test_tc2231.py +++ b/tests/tc2231/test_tc2231.py @@ -355,13 +355,13 @@ def test_attabspathwin(content_disposition): def test_attcdate(content_disposition): h = content_disposition(b'Content-Disposition: attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500"') - assert h.creation_date == datetime.datetime(1997, 2, 12, 16, 29, 51) # noqa: DTZ001 + assert h.creation_date == datetime.datetime(1997, 2, 12, 16, 29, 51, tzinfo=datetime.timezone.utc) # assert h.creation_date == 855761391.0 # FIXME: assert == 855761391.0 def test_attmdate(content_disposition): h = content_disposition(b'Content-Disposition: attachment; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"') - assert h.modification_date == datetime.datetime(1997, 2, 12, 16, 29, 51) # noqa: DTZ001 + assert h.modification_date == datetime.datetime(1997, 2, 12, 16, 29, 51, tzinfo=datetime.timezone.utc) # assert h.modification_date == 855761391.0 # FIXME: assert == 855761391.0 From 81f8b1970acb4b17091c1e7adea9f6b981bc1507 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:20:46 +0200 Subject: [PATCH 33/40] ruff --- tests/api/test_statemachine.py | 4 ++-- tests/api/test_wsgi.py | 2 +- tests/messaging/test_request_codecs.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/api/test_statemachine.py b/tests/api/test_statemachine.py index f3a0081..8e41993 100644 --- a/tests/api/test_statemachine.py +++ b/tests/api/test_statemachine.py @@ -42,8 +42,8 @@ def test_max_uri_length(statemachine): statemachine.MAX_URI_LENGTH = 100 path = b'A' * (statemachine.MAX_URI_LENGTH - 5) statemachine.parse(b'GET /%s HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n' % (path,)) - with pytest.raises(URI_TOO_LONG) as exc: # noqa: PT012 - path = b'A' * statemachine.MAX_URI_LENGTH + path = b'A' * statemachine.MAX_URI_LENGTH + with pytest.raises(URI_TOO_LONG) as exc: statemachine.parse(b'GET /%s HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n' % (path,)) assert 'The maximum length of the request is 100' in str(exc.value.description) diff --git a/tests/api/test_wsgi.py b/tests/api/test_wsgi.py index 6044581..84c02d5 100644 --- a/tests/api/test_wsgi.py +++ b/tests/api/test_wsgi.py @@ -171,7 +171,7 @@ def test_essential_parameters(): def test_client_reraising(): client = WSGIClient({'CONTENT_TYPE': 'text/html', 'CONTENT_LENGTH': '0', 'HTTP_HOST': 'foobar'}) client.headers_sent = True - with pytest.raises(ValueError) as exc: # noqa: PT011 + with pytest.raises(ValueError, match='True') as exc: client(application15) assert exc.value.args[0] is True diff --git a/tests/messaging/test_request_codecs.py b/tests/messaging/test_request_codecs.py index 6aa7b03..2c25cb2 100644 --- a/tests/messaging/test_request_codecs.py +++ b/tests/messaging/test_request_codecs.py @@ -16,7 +16,7 @@ def test_json(body): def test_invalid_json(body): body.mimetype = 'application/json' - with pytest.raises(ValueError): # noqa: PT011 + with pytest.raises(ValueError, match='Iterable contained non-bytes'): body.decode('{"foo" "bar"}') @@ -159,8 +159,8 @@ def check_encoding_dict(body, data): def check_raises(body, chars, type_, exception): for chr_ in chars: - with pytest.raises(exception): # noqa: PT012 - body.set(chr_) + body.set(chr_) + with pytest.raises(exception): type_(body) From 636796222aa4461b9a547adb5699146ae0e0130d Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:36:17 +0200 Subject: [PATCH 34/40] fixup! style: ruff --- tests/headers/test_host_header.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/headers/test_host_header.py b/tests/headers/test_host_header.py index 82ddca4..0eda230 100644 --- a/tests/headers/test_host_header.py +++ b/tests/headers/test_host_header.py @@ -55,13 +55,12 @@ def _test_iter(header, host, port, headers): return element -@pytest.mark.parametrize('invalid', list( - (set('\x7F()<>@,;:/\\[\\]={} \t\\\\^"\'') | set(map(chr, range(0x1F)))) - set(';\x00'), *[ - ', ', - pytest.param(';', marks=pytest.mark.xfail), # FIXME: fails - pytest.param('\x00', marks=pytest.mark.xfail), # FIXME: fails - ] -)) +@pytest.mark.parametrize('invalid', [ + *((set('\x7F()<>@,;:/\\[\\]={} \t\\\\^"\'') | set(map(chr, range(0x1F)))) - {';', '\x00'}), + ', ', + pytest.param(';', marks=pytest.mark.xfail(reason='FIXME: fails')), + pytest.param('\x00', marks=pytest.mark.xfail(reason='FIXME: fails')), +]) def test_invalid_host_header(invalid, headers): dict.__setitem__(headers, 'Host', f'foo{invalid}bar'.encode('ISO8859-1')) with pytest.raises(InvalidHeader): From cf027c8c37056f67c9f708202e3b1d41bb6a517a Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:40:42 +0200 Subject: [PATCH 35/40] fixup! fixup! ruff --- httoop/date.py | 7 ++++++- httoop/semantic/message.py | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/httoop/date.py b/httoop/date.py index 46b6348..dc3ef11 100644 --- a/httoop/date.py +++ b/httoop/date.py @@ -11,13 +11,18 @@ # import calendar from datetime import datetime, timezone from email.utils import parsedate_tz -from typing import Any, Self +from typing import Any from httoop.exceptions import InvalidDate from httoop.meta import Semantic from httoop.util import _ +try: + from typing import Self +except ImportError: # < Py 3.11 + from typing_extensions import Self + __all__ = ['Date'] diff --git a/httoop/semantic/message.py b/httoop/semantic/message.py index 3dcb508..b58be17 100644 --- a/httoop/semantic/message.py +++ b/httoop/semantic/message.py @@ -1,5 +1,11 @@ from contextlib import contextmanager -from typing import Iterator, override +from typing import Iterator + + +try: + from typing import override +except ImportError: # < Py 3.11 + from typing_extensions import override class ComposedMessage: From 0607766728b024aa26e29a47b2523d169c2babfe Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:42:10 +0200 Subject: [PATCH 36/40] typing_extensions --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 34d5303..eca3b78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,10 @@ classifiers = [ requires-python = ">=3.9" +dependencies = [ + "typing_extensions>=4.4", +] + [project.optional-dependencies] xml = ["defusedxml"] hal = ["uritemplate"] From 60fbea13b5811526b270cccaf7b00aeaa767cae0 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 01:48:04 +0200 Subject: [PATCH 37/40] fixup! PLW2901 --- httoop/messages/body.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httoop/messages/body.py b/httoop/messages/body.py index f639b66..e13712b 100644 --- a/httoop/messages/body.py +++ b/httoop/messages/body.py @@ -249,7 +249,8 @@ def __content_iter(self): yield data.encode(self.encoding) elif isinstance(data, bytes): yield data - raise TypeError(f'Iterable contained non-bytes: {type(data).__name__!r}') # pragma: no cover + else: # pragma: no cover + raise TypeError(f'Iterable contained non-bytes: {type(data).__name__!r}') finally: self.seek(t) From 6accc4821d60d11a13e5b201bd2917355b99be0a Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 02:04:24 +0200 Subject: [PATCH 38/40] fixes --- httoop/header/security.py | 4 ++-- httoop/parser.py | 2 +- tests/api/test_statemachine.py | 6 +++--- tests/messaging/test_request_codecs.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/httoop/header/security.py b/httoop/header/security.py index 6598038..6357cbc 100644 --- a/httoop/header/security.py +++ b/httoop/header/security.py @@ -25,7 +25,7 @@ class ContentSecurityPolicy(HeaderElement, name='Content-Security-Policy'): is_response_header = True RE_SPLIT = re.compile(rb';') - RE_PARAMS = re.compile(rb'\\s+') + RE_PARAMS = re.compile(rb'\s+') def compose(self) -> bytes: return b'%s %s; ' % (self.value.encode('ISO8859-1'), b' '.join(self.params.keys())) @@ -82,7 +82,7 @@ class FrameOptions(HeaderElement, name='X-Frame-Options'): is_response_header = True - RE_PARAMS = re.compile(rb'\\s+') + RE_PARAMS = re.compile(rb'\s+') @property def deny(self) -> bool: diff --git a/httoop/parser.py b/httoop/parser.py index d7e089b..6ae5ad6 100644 --- a/httoop/parser.py +++ b/httoop/parser.py @@ -179,7 +179,7 @@ def _parse_single_headers(self) -> None: rest += self.buffer[-len(self.line_end):] else: headers, _, rest = self.buffer.rpartition(self.line_end) - if headers and _ and rest[:1] not in {b'', b'\t', b' '}: + if headers and _ and bytes(rest[:1]) not in {b'', b'\t', b' '}: self.buffer = rest self._parse_header(headers) diff --git a/tests/api/test_statemachine.py b/tests/api/test_statemachine.py index 8e41993..5b55f17 100644 --- a/tests/api/test_statemachine.py +++ b/tests/api/test_statemachine.py @@ -50,9 +50,9 @@ def test_max_uri_length(statemachine): def test_max_request_line_length(statemachine): statemachine.MAX_URI_LENGTH = 100 - with pytest.raises(URI_TOO_LONG) as exc: # noqa: PT012 - path = b'A' * statemachine.MAX_URI_LENGTH - statemachine.parse(b'GET /%s HTTP/' % (path,)) + path = b'A' * statemachine.MAX_URI_LENGTH + statemachine.parse(b'GET /%s HTTP/' % (path,)) + with pytest.raises(URI_TOO_LONG) as exc: statemachine.parse(b'1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n') assert 'The maximum length of the request is 100' in str(exc.value.description) diff --git a/tests/messaging/test_request_codecs.py b/tests/messaging/test_request_codecs.py index 2c25cb2..56eaf68 100644 --- a/tests/messaging/test_request_codecs.py +++ b/tests/messaging/test_request_codecs.py @@ -16,7 +16,7 @@ def test_json(body): def test_invalid_json(body): body.mimetype = 'application/json' - with pytest.raises(ValueError, match='Iterable contained non-bytes'): + with pytest.raises(ValueError, match=r"Expecting ':' delimiter: line 1 column 8 \(char 7\)"): body.decode('{"foo" "bar"}') @@ -159,8 +159,8 @@ def check_encoding_dict(body, data): def check_raises(body, chars, type_, exception): for chr_ in chars: - body.set(chr_) - with pytest.raises(exception): + with pytest.raises(exception): # noqa: PT012 + body.set(chr_) type_(body) From fa0e2a42782baa09098bd3e7cf85ad10c514f5f7 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 02:14:18 +0200 Subject: [PATCH 39/40] fixup! fixes --- httoop/__main__.py | 4 ++-- tests/api/test_statemachine.py | 4 ++-- tests/headers/test_accept_headers.py | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/httoop/__main__.py b/httoop/__main__.py index f5cf200..d8010d8 100644 --- a/httoop/__main__.py +++ b/httoop/__main__.py @@ -80,7 +80,7 @@ def get_file(cls, file: str) -> IO: def parse_request(self) -> None: server = ServerStateMachine(self.arguments.scheme, self.arguments.host, self.arguments.port) - for _request, response in server.parse(self.get_file(self.arguments.file)): + for _request, response in server.parse(self.get_file(self.arguments.file).read()): print(repr(response)) print(repr(response.headers)) print(repr(response.body)) @@ -88,7 +88,7 @@ def parse_request(self) -> None: def parse_response(self) -> None: client = ClientStateMachine() client.request = Request() - for response in client.parse(self.arguments.file.read()): + for response in client.parse(self.get_file(self.arguments.file).read()): print(repr(response)) print(repr(response.headers)) print(repr(response.body)) diff --git a/tests/api/test_statemachine.py b/tests/api/test_statemachine.py index 5b55f17..c0f8a6e 100644 --- a/tests/api/test_statemachine.py +++ b/tests/api/test_statemachine.py @@ -51,9 +51,9 @@ def test_max_uri_length(statemachine): def test_max_request_line_length(statemachine): statemachine.MAX_URI_LENGTH = 100 path = b'A' * statemachine.MAX_URI_LENGTH - statemachine.parse(b'GET /%s HTTP/' % (path,)) with pytest.raises(URI_TOO_LONG) as exc: - statemachine.parse(b'1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n') + statemachine.parse(b'GET /%s HTTP/' % (path,)) + # statemachine.parse(b'1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n') assert 'The maximum length of the request is 100' in str(exc.value.description) diff --git a/tests/headers/test_accept_headers.py b/tests/headers/test_accept_headers.py index aced76d..0996b93 100644 --- a/tests/headers/test_accept_headers.py +++ b/tests/headers/test_accept_headers.py @@ -8,8 +8,10 @@ def test_quality_parameter_in_accept_header(headers): def test_comparing_accept(headers): headers.parse(b'Accept: application/json; q=0.2, text/plain, text/html; q=0.5, *; q=0') - assert headers.elements('Accept')[0] < 'text/html' - assert headers.elements('Accept')[0] < '*' + assert headers.elements('Accept')[2] < 'text/html' + assert headers.elements('Accept')[2] < '*' + # FIXME: assert headers.elements('Accept')[1] <= 'text/html' + assert headers.elements('Accept')[1] < 'text/html' @pytest.mark.parametrize('header_name', ['Accept', 'Content-Type']) From e9b17a738b01f5f120c37655d57f6e38bc761a19 Mon Sep 17 00:00:00 2001 From: Space One Date: Fri, 24 Jul 2026 02:49:54 +0200 Subject: [PATCH 40/40] type checks --- httoop/authentication/basic.py | 4 +- httoop/client/__init__.py | 4 +- httoop/codecs/application/gzip.py | 4 +- httoop/codecs/application/hal_json.py | 6 +-- httoop/codecs/application/json.py | 6 +-- .../application/x_www_form_urlencoded.py | 3 +- httoop/codecs/codec.py | 8 ++-- httoop/date.py | 15 +++---- httoop/gateway/wsgi.py | 12 ++++-- httoop/header/conditional.py | 2 + httoop/header/element.py | 41 +++++++++++-------- httoop/semantic/message.py | 10 ++++- httoop/semantic/request.py | 1 + httoop/server/__init__.py | 8 ++++ httoop/uri/percent_encoding.py | 12 +++--- tests/api/test_body.py | 38 ++++++++--------- tests/api/test_codecs.py | 3 +- tests/api/test_date.py | 14 ++++++- tests/api/test_method.py | 6 ++- tests/api/test_protocol.py | 11 ++--- tests/api/test_status.py | 5 ++- tests/api/test_uri.py | 15 +++---- tests/api/test_wsgi.py | 1 + tests/conftest.py | 14 +++---- tests/messaging/test_body.py | 14 +++---- tests/messaging/test_request_codecs.py | 4 +- tests/messaging/test_semantic.py | 29 ++++++------- tests/uri/test_uri_query.py | 2 +- 28 files changed, 169 insertions(+), 123 deletions(-) diff --git a/httoop/authentication/basic.py b/httoop/authentication/basic.py index c3aecb0..5958394 100644 --- a/httoop/authentication/basic.py +++ b/httoop/authentication/basic.py @@ -42,9 +42,9 @@ def compose(authinfo: ByteUnicodeDict) -> bytes: class BasicAuthResponseScheme: @staticmethod - def parse(authinfo: bytes) -> dict[bytes, str | bytes]: + def parse(authinfo: bytes) -> dict[bytes, str]: params = HeaderElement.parseparams(b'X;%s' % authinfo)[1] - params.setdefault(b'realm', b'') + params.setdefault(b'realm', '') return params @staticmethod diff --git a/httoop/client/__init__.py b/httoop/client/__init__.py index 36caf55..6393b69 100644 --- a/httoop/client/__init__.py +++ b/httoop/client/__init__.py @@ -1,13 +1,15 @@ from __future__ import annotations from httoop.exceptions import InvalidLine -from httoop.messages import Response +from httoop.messages import Request, Response from httoop.parser import NOT_RECEIVED_YET, StateMachine class ClientStateMachine(StateMachine): Message = Response + request: Request + message: Response def __init__(self, *, strict: bool = True, max_status_line_length: float = 256) -> None: super().__init__(strict=strict) diff --git a/httoop/codecs/application/gzip.py b/httoop/codecs/application/gzip.py index 56eb335..e457ee3 100644 --- a/httoop/codecs/application/gzip.py +++ b/httoop/codecs/application/gzip.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import gzip import io import zlib @@ -23,7 +25,7 @@ def encode(cls, data: bytes, charset: None = None, mimetype: None = None) -> byt raise EncodeError(_('Invalid gzip data.')) from None @classmethod - def decode(cls, data: bytes, charset: None = None, mimetype: None = None) -> str: + def decode(cls, data: bytes, charset: str | None = None, mimetype: None = None) -> str: try: with gzip.GzipFile(fileobj=io.BytesIO(data)) as fd: data = fd.read() diff --git a/httoop/codecs/application/hal_json.py b/httoop/codecs/application/hal_json.py index 8064a32..c19fd44 100644 --- a/httoop/codecs/application/hal_json.py +++ b/httoop/codecs/application/hal_json.py @@ -125,10 +125,10 @@ class HAL(JSON): @classmethod def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | None = None) -> Resource: - data = super().decode(data) - if not isinstance(data, dict): + doc = super().decode(data) + if not isinstance(doc, dict): raise DecodeError('HAL documents must be JSON objects.') - return Resource(data) + return Resource(doc) @classmethod def encode(cls, data: dict[str, None] | Resource, charset: str | None = None, mimetype: ContentType | None = None) -> bytes: diff --git a/httoop/codecs/application/json.py b/httoop/codecs/application/json.py index 097ae83..8fcfc7e 100644 --- a/httoop/codecs/application/json.py +++ b/httoop/codecs/application/json.py @@ -15,10 +15,8 @@ class JSON(Codec): @classmethod def encode(cls, data: dict[str, str], charset: str | None = None, mimetype: ContentType | None = None) -> bytes: - data = json_encode(data) - if not isinstance(data, bytes): # python3 - data = data.encode(charset or 'UTF-8') - return data + doc = json_encode(data) + return doc.encode(charset or 'UTF-8') @classmethod def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | None = None) -> dict[str, Any]: diff --git a/httoop/codecs/application/x_www_form_urlencoded.py b/httoop/codecs/application/x_www_form_urlencoded.py index 70f3af0..8ecaca2 100644 --- a/httoop/codecs/application/x_www_form_urlencoded.py +++ b/httoop/codecs/application/x_www_form_urlencoded.py @@ -39,5 +39,4 @@ def unquote(cls, data: bytes, charset: str | None = None) -> str: @classmethod def quote(cls, data: str | list[int], charset: str | None = None) -> bytes: - data = data.encode(charset or 'ISO8859-1') - return Percent.quote(data, cls.UNQUOTED) + return Percent.quote(data.encode(charset or 'ISO8859-1'), cls.UNQUOTED) diff --git a/httoop/codecs/codec.py b/httoop/codecs/codec.py index cfaa38b..e2cb8b7 100644 --- a/httoop/codecs/codec.py +++ b/httoop/codecs/codec.py @@ -9,14 +9,16 @@ class Codec: + mimetype: str + @classmethod - def decode(cls, data: bytes, charset: str | None = None, mimetype: None = None) -> str: # pragma: no cover + def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType | None = None) -> str: # pragma: no cover if isinstance(data, bytes): data = data.decode(charset or 'ascii') return data @classmethod - def encode(cls, data: bytes, charset: None = None, mimetype: None = None) -> bytes: # pragma: no cover + def encode(cls, data: bytes, charset: None = None, mimetype: ContentType | None = None) -> bytes: # pragma: no cover if isinstance(data, str): data = data.encode(charset or 'ascii') return data @@ -26,5 +28,5 @@ def iterencode(cls, data: Any, charset: str | None = None, mimetype: ContentType yield cls.encode(data, charset, mimetype) @classmethod - def iterdecode(cls, data: Any, charset=None, mimetype=None): # pragma: no cover + def iterdecode(cls, data: Any, charset=None, mimetype: ContentType | None = None): # pragma: no cover yield cls.decode(data, charset, mimetype) diff --git a/httoop/date.py b/httoop/date.py index dc3ef11..9e27fc2 100644 --- a/httoop/date.py +++ b/httoop/date.py @@ -9,7 +9,7 @@ import time # import calendar -from datetime import datetime, timezone +from datetime import datetime as datetime_type, timezone from email.utils import parsedate_tz from typing import Any @@ -18,11 +18,6 @@ from httoop.util import _ -try: - from typing import Self -except ImportError: # < Py 3.11 - from typing_extensions import Self - __all__ = ['Date'] @@ -66,7 +61,7 @@ def __init__(self, timeval: Any | None = None) -> None: elif isinstance(timeval, (tuple, time.struct_time)): # self.__timestamp = calendar.timegm(timeval) self.__timestamp = time.mktime(timeval) - time.timezone - elif isinstance(timeval, datetime): + elif isinstance(timeval, datetime_type): self.__datetime = timeval # self.__timestamp = calendar.timegm(self.datetime.utctimetuple()) self.__timestamp = time.mktime(self.datetime.utctimetuple()) - time.timezone @@ -80,9 +75,9 @@ def __init__(self, timeval: Any | None = None) -> None: raise TypeError('Date(): got invalid argument') @property - def datetime(self) -> datetime: + def datetime(self) -> datetime_type: if self.__datetime is None: - self.__datetime = datetime.fromtimestamp(int(self), tz=timezone.utc) + self.__datetime = datetime_type.fromtimestamp(int(self), tz=timezone.utc) return self.__datetime @property @@ -154,7 +149,7 @@ def __lt__(self, other: Date | str | None) -> bool: return NotImplemented @staticmethod - def __other(other) -> Self: + def __other(other) -> Date: if other is None: return Date(0) # raise NotImplementedError() diff --git a/httoop/gateway/wsgi.py b/httoop/gateway/wsgi.py index e83be9b..3f5ecfa 100644 --- a/httoop/gateway/wsgi.py +++ b/httoop/gateway/wsgi.py @@ -9,7 +9,7 @@ from typing import Any, Callable, Iterator import httoop.header -from httoop.messages import Body +from httoop.messages import Body, Request, Response __all__ = ('WSGI',) @@ -32,6 +32,9 @@ def read(self, *size): class WSGI: """A mixin class which implements the WSGI interface.""" + request: Request + response: Response + def __init__(self, environ: dict[str, str] | None = None, *args, use_path_info: bool = False, **kwargs) -> None: self.use_path_info = use_path_info super().__init__() @@ -64,7 +67,7 @@ def __init__(self, environ: dict[str, str] | None = None, *args, use_path_info: def start_response(self) -> None: pass - def __call__(self, application: Callable) -> Iterator[Any]: + def __call__(self, application: Callable) -> Iterator[Any] | None: def write(data): if not self.headers_set: raise RuntimeError('write() before start_response()') @@ -116,8 +119,9 @@ def buffered(data): if data: yield data finally: - if hasattr(result, 'close'): - result.close() + close = getattr(result, 'close', None) + if callable(close): + close() self.response.body = buffered(data) return raw_result diff --git a/httoop/header/conditional.py b/httoop/header/conditional.py index 9658702..25a000a 100644 --- a/httoop/header/conditional.py +++ b/httoop/header/conditional.py @@ -6,6 +6,7 @@ class _DateComparable: # noqa: PLW1641 Date = Date + value: str def sanitize(self) -> None: super().sanitize() @@ -26,6 +27,7 @@ def __int__(self) -> int: class _MatchElement: # noqa: PLW1641 + value: str def __eq__(self, other: object) -> bool: return self.value in {other, '*'} diff --git a/httoop/header/element.py b/httoop/header/element.py index ba61a4a..bd1ee41 100644 --- a/httoop/header/element.py +++ b/httoop/header/element.py @@ -172,9 +172,9 @@ def _rfc2231_and_continuation_params(cls, params: Iterator[Any]) -> Iterator[tup yield b'%s*%d' % (key, k), v @classmethod - def parse(cls, elementstr: bytes) -> HeaderElement: + def parse(cls, element: bytes) -> HeaderElement: """Construct an instance from a string of the form 'token;key=val'.""" - elementstr, encoding = cls.decode_rfc2047_charset(elementstr) + elementstr, encoding = cls.decode_rfc2047_charset(element) ival, params = cls.parseparams(elementstr.encode(encoding)) return cls(ival.decode(encoding), params) @@ -202,18 +202,20 @@ def formatparam(cls, param: bytes, value: bytes | str | None = None, *, quote: b This will quote the value if needed or if quote is true. """ if value: - if not isinstance(value, bytes): + if isinstance(value, bytes): + val = value + else: try: - value = value.encode('ASCII') + val = value.encode('ASCII') except UnicodeEncodeError: param += b'*' - value = b"utf-8''%s" % (Percent.quote(value.encode('UTF-8')),) + val = b"utf-8''%s" % (Percent.quote(value.encode('UTF-8')),) quote = False - if quote or cls.RE_TSPECIALS.search(value): - value = value.replace(b'\\', b'\\\\').replace(b'"', rb'\"') - return b'%s="%s"' % (param, value) - return b'%s=%s' % (param, value) + if quote or cls.RE_TSPECIALS.search(val): + val = val.replace(b'\\', b'\\\\').replace(b'"', rb'\"') + return b'%s="%s"' % (param, val) + return b'%s=%s' % (param, val) return param @classmethod @@ -257,6 +259,9 @@ class MimeType: .. seealso:: rfc:`3023` """ + value: str + params: dict + @property def mimetype(self) -> str: return f'{self.type}/{self.subtype_wo_vendor}' @@ -317,14 +322,14 @@ class _AcceptElement(HeaderElement): # noqa: PLW1641 RE_Q_SEPARATOR = re.compile(rb';\s*q\s*=\s*') @property - def quality(self) -> float | None: + def quality(self) -> float: """The quality of this value.""" val = self.params.get('q', '1') if isinstance(val, HeaderElement): # pragma: no cover val = val.value if val: return float(val) - return None + return 0.0 def sanitize(self) -> None: super().sanitize() @@ -334,8 +339,8 @@ def sanitize(self) -> None: raise InvalidHeader(_('Quality value must be float.')) from None @classmethod - def parse(cls, elementstr: bytes) -> HeaderElement: - elementstr, encoding = cls.decode_rfc2047_charset(elementstr) + def parse(cls, element: bytes) -> HeaderElement: + elementstr, encoding = cls.decode_rfc2047_charset(element) qvalue = None # The first "q" parameter (if any) separates the initial # media-range parameter(s) (if any) from the accept-params. @@ -348,7 +353,7 @@ def parse(cls, elementstr: bytes) -> HeaderElement: media_type, params = cls.parseparams(media_range) if qvalue is not None: - params['q'] = bytes(qvalue) + params[b'q'] = str(qvalue) return cls(media_type.decode(encoding), params) @@ -356,12 +361,12 @@ def parse(cls, elementstr: bytes) -> HeaderElement: def sorted(cls, elements: list) -> list: return sorted(elements, reverse=True) - def __eq__(self, other: str) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, _AcceptElement): other = _AcceptElement(other) return other.value == self.value and other.quality == self.quality - def __lt__(self, other: str) -> bool: + def __lt__(self, other: object) -> bool: if not isinstance(other, _AcceptElement): other = _AcceptElement(other) if self.quality == other.quality: @@ -380,8 +385,8 @@ def __init__(self, cookie_name: str, cookie_value: str, params: dict[bytes, str] super().__init__(self.value, params) @classmethod - def parse(cls, elementstr: bytes) -> HeaderElement: - elementstr, encoding = cls.decode_rfc2047_charset(elementstr) + def parse(cls, element: bytes) -> HeaderElement: + elementstr, encoding = cls.decode_rfc2047_charset(element) value, params = cls.parseparams(elementstr.encode(encoding)) cookie_name, cookie_value, __ = cls.parseparam(value) return cls(cookie_name.decode(encoding), cookie_value.decode(encoding), params) diff --git a/httoop/semantic/message.py b/httoop/semantic/message.py index b58be17..c723ce6 100644 --- a/httoop/semantic/message.py +++ b/httoop/semantic/message.py @@ -1,5 +1,11 @@ +from __future__ import annotations + from contextlib import contextmanager -from typing import Iterator +from typing import TYPE_CHECKING, Iterator + + +if TYPE_CHECKING: + from httoop import Request, Response try: @@ -10,6 +16,8 @@ class ComposedMessage: + message: Request | Response + # FIXME: use it @property def close(self): # pragma: no cover diff --git a/httoop/semantic/request.py b/httoop/semantic/request.py index bc9a128..3d3b98f 100644 --- a/httoop/semantic/request.py +++ b/httoop/semantic/request.py @@ -11,6 +11,7 @@ class ComposedRequest(ComposedMessage): USER_AGENT = UserAgentHeader + message: Request def __init__(self, request: Request) -> None: self.message = request diff --git a/httoop/server/__init__.py b/httoop/server/__init__.py index 1e0ba12..40799a8 100644 --- a/httoop/server/__init__.py +++ b/httoop/server/__init__.py @@ -18,6 +18,7 @@ class ServerStateMachine(StateMachine): Message = Request + message: Request HTTP2 = None def __init__(self, scheme: str, host: str, port: int, *, strict: bool = True, max_uri_length: float = 8192) -> None: @@ -41,6 +42,8 @@ def on_message_started(self) -> None: def on_message_complete(self) -> tuple[httoop.messages.request.Request, httoop.messages.response.Response]: request = super().on_message_complete() response = self.response + assert request is not None # noqa: S101 + assert response is not None # noqa: S101 self.request = None self.response = None return (request, response) @@ -62,6 +65,7 @@ def on_startline_complete(self) -> None: def on_uri_complete(self) -> None: super().on_uri_complete() + assert self.request # noqa: S101 self._check_uri_max_length(bytes(self.request.uri)) self.sanitize_request_uri_path() self.validate_request_uri_scheme() @@ -91,6 +95,8 @@ def check_request_protocol(self) -> None: def set_response_protocol(self) -> None: # set appropriate response protocol version + assert self.message # noqa: S101 + assert self.response # noqa: S101 self.response.protocol = min(self.message.protocol, ServerProtocol) def _check_uri_max_length(self, uri: bytearray | bytes) -> None: @@ -114,6 +120,7 @@ def validate_request_uri_scheme(self) -> None: self.message.uri.port = self._default_port def set_server_response_header(self) -> None: + assert self.response # noqa: S101 self.response.headers.setdefault('Server', ServerHeader) def check_host_header_exists(self) -> None: @@ -150,6 +157,7 @@ def is_http2_upgrade(): if all(is_http2_upgrade()): if self.HTTP2 is None: return + assert self.response # noqa: S101 self.response.headers['Upgrade'] = 'h2c' self.response.headers['Connection'] = 'Upgrade' self.__class__ = self.HTTP2 diff --git a/httoop/uri/percent_encoding.py b/httoop/uri/percent_encoding.py index 0e066a2..7c12193 100644 --- a/httoop/uri/percent_encoding.py +++ b/httoop/uri/percent_encoding.py @@ -35,9 +35,9 @@ def unquote(cls, data: bytes) -> bytes: @classmethod def _decode_iter(cls, data: bytes) -> Iterator[bytes]: - data = data.split(b'%') - yield data.pop(0) - for item in data: + datas = data.split(b'%') + yield datas.pop(0) + for item in datas: mapped = cls.HEX_MAP.get(item[:2]) if mapped is not None: yield mapped @@ -48,6 +48,6 @@ def _decode_iter(cls, data: bytes) -> Iterator[bytes]: @classmethod def quote(cls, data: bytes, charset: bytes = UNRESERVED) -> bytes: - charset = {bytes((c,)) for c in iter(charset)} - {b'%'} - data = (bytes((d,)) for d in iter(data)) - return b''.join(b'%%%X' % (ord(d),) if d not in charset else d for d in data) + charset_s = {bytes((c,)) for c in iter(charset)} - {b'%'} + datas = (bytes((d,)) for d in iter(data)) + return b''.join(b'%%%X' % (ord(d),) if d not in charset_s else d for d in datas) diff --git a/tests/api/test_body.py b/tests/api/test_body.py index cb7b580..5bc8667 100644 --- a/tests/api/test_body.py +++ b/tests/api/test_body.py @@ -3,24 +3,24 @@ import pytest -from httoop import Body +from httoop import Body, Request -def test_body_set_unicode(request_): +def test_body_set_unicode(request_: Request): request_.body = 'foobar' assert bytes(request_.body) == b'foobar' assert str(request_.body) == 'foobar' assert request_.body.fileable -def test_body_set_bytes(request_): +def test_body_set_bytes(request_: Request): request_.body = b'foobar' assert bytes(request_.body) == b'foobar' assert str(request_.body) == 'foobar' assert request_.body.fileable -def test_body_set_bytesio(request_): +def test_body_set_bytesio(request_: Request): b = BytesIO(b'ThisIsABytesIOBody') request_.body = b assert bytes(request_.body) == b'ThisIsABytesIOBody' @@ -28,7 +28,7 @@ def test_body_set_bytesio(request_): assert request_.body.fileable -def test_body_set_stringio(request_): +def test_body_set_stringio(request_: Request): s = StringIO('ThisIsAStringIOBody') request_.body = s assert bytes(request_.body) == b'ThisIsAStringIOBody' @@ -36,7 +36,7 @@ def test_body_set_stringio(request_): assert request_.body.fileable -def test_body_set_tempfile(request_): +def test_body_set_tempfile(request_: Request): with NamedTemporaryFile() as tempfile: tempfile.write(b'ThisIsANamedTemporaryFile') tempfile.flush() @@ -46,25 +46,25 @@ def test_body_set_tempfile(request_): assert request_.body.fileable -def test_body_set_bytearray(request_): +def test_body_set_bytearray(request_: Request): a = bytearray(b''.join([b'We', b'', b'are ', b'just', b' ', b'testing\t', b'ByteArrays!'])) request_.body = a assert bytes(request_.body) == b'Weare just testing\tByteArrays!' -def test_body_set_list(request_): +def test_body_set_list(request_: Request): ls = ['This ', 'is', b'\nsome', None, 'list\t', 'content', ''] request_.body = ls assert bytes(request_.body) == b'This is\nsomelist\tcontent' -def test_body_set_tuple(request_): +def test_body_set_tuple(request_: Request): t = ('Testing', ' ', 'a', 'tuple') request_.body = t assert bytes(request_.body) == b'Testing atuple' -def test_body_set_generator(request_): +def test_body_set_generator(request_: Request): def g(): yield 'This ' yield 'is' @@ -82,7 +82,7 @@ def g(): assert len(request_.body) == 19 -def test_body_set_body(request_): +def test_body_set_body(request_: Request): b = Body() b.mimetype = 'application/json' b.set('{}') @@ -92,27 +92,27 @@ def test_body_set_body(request_): assert request_.body.fd is b.fd -def test_body_iterencode(request_): +def test_body_iterencode(request_: Request): request_.body.mimetype = 'application/json' request_.body.iterencode({}) assert bytes(request_.body) == b'{}' -def tets_body_set_none(request_): +def tets_body_set_none(request_: Request): request_.body = 'Foobar' request_.body = None assert not request_.body assert bytes(request_.body) == b'' -def test_closed_body(request_): +def test_closed_body(request_: Request): b = BytesIO() b.close() with pytest.raises(ValueError, match=r'I/O operation on closed file\.'): request_.body = b -def test_body_close_clear(request_): +def test_body_close_clear(request_: Request): request_.body = b'asfd asdf asdf asdf asdf asdf asdf' assert request_.body request_.body.close() @@ -121,7 +121,7 @@ def test_body_close_clear(request_): assert bytes(request_.body) == b'' -def test_empty_body_is_false(request_): +def test_empty_body_is_false(request_: Request): request_.body = 'foobar' assert request_.body assert len(request_.body) == 6 @@ -130,12 +130,12 @@ def test_empty_body_is_false(request_): assert len(request_.body) == 0 -def test_set_invalid_body(request_): +def test_set_invalid_body(request_: Request): with pytest.raises(TypeError): request_.body = 1 -def test_body_iter_list(request_): +def test_body_iter_list(request_: Request): request_.body = ['asdf', 'foo', 'Ba', 'Baz', None, 'blub'] assert next(request_.body) == b'asdf' assert next(request_.body) == b'foo' @@ -151,7 +151,7 @@ def test_body_iter_list(request_): assert next(request_.body) == b'blub' -def test_body_file_interface(request_): +def test_body_file_interface(request_: Request): assert request_.body.name is None request_.body.flush() request_.body = b'foo\nbar\nbaz\nblub' diff --git a/tests/api/test_codecs.py b/tests/api/test_codecs.py index 03a1f90..2a6729d 100644 --- a/tests/api/test_codecs.py +++ b/tests/api/test_codecs.py @@ -1,5 +1,6 @@ import pytest +from httoop import Body from httoop.codecs import Codec, lookup, register from httoop.exceptions import DecodeError, EncodeError @@ -54,7 +55,7 @@ def test_body_invalid_hal(): hal.decode(b'{"_embedded": {"foo": 1}}').get_resource('foo') -def test_invalid_multipart(body): +def test_invalid_multipart(body: Body): body.mimetype = 'multipart/x; boundary=asdf' codec = body.mimetype.codec '--asdf\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nfoo\r\n--asdf\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nbar\r\n--asdf\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nbaz\r\n--asdf--\r\n' diff --git a/tests/api/test_date.py b/tests/api/test_date.py index 53f4665..d372323 100644 --- a/tests/api/test_date.py +++ b/tests/api/test_date.py @@ -1,11 +1,23 @@ +from __future__ import annotations + import datetime +from typing import TypedDict import pytest from httoop import Date, InvalidDate -dates = [{ +class DateData(TypedDict): + datetime: datetime.datetime + timestamp: float + lt: Date + gt: Date + gmtime: tuple[int, int, int, int, int, int, int, int, int] + formats: set[str] + + +dates: list[DateData] = [{ 'datetime': datetime.datetime(1994, 11, 6, 8, 49, 37, tzinfo=datetime.timezone.utc), 'timestamp': 784111777.0, 'lt': Date(784111776.0), diff --git a/tests/api/test_method.py b/tests/api/test_method.py index 10716d3..9dd7bdb 100644 --- a/tests/api/test_method.py +++ b/tests/api/test_method.py @@ -2,8 +2,10 @@ import pytest +from httoop import Request -def test_safe_methods(request_): + +def test_safe_methods(request_: Request): all_methods = ( ('GET', True, True), ('HEAD', True, True), @@ -20,7 +22,7 @@ def test_safe_methods(request_): @pytest.mark.xfail(reason='hash changing + fixed references') -def test_hashable_methods(request_): +def test_hashable_methods(request_: Request): methods = {} request_.method = 'GET' methods[request_.method] = 1 diff --git a/tests/api/test_protocol.py b/tests/api/test_protocol.py index 1543113..c46539f 100644 --- a/tests/api/test_protocol.py +++ b/tests/api/test_protocol.py @@ -1,25 +1,26 @@ import pytest +from httoop import Request from httoop.exceptions import InvalidLine -def test_protocol_tuple(request_): +def test_protocol_tuple(request_: Request): request_.protocol.parse(b'HTTP/1.0') assert request_.protocol == (1, 0) -def test_set_protocol_tuple(request_): +def test_set_protocol_tuple(request_: Request): request_.protocol = (1, 0) assert bytes(request_.protocol) == b'HTTP/1.0' -def test_protocol_minor_mayor(request_): +def test_protocol_minor_mayor(request_: Request): request_.protocol = (1, 0) assert request_.protocol.major == 1 assert request_.protocol.minor == 0 -def test_protocol_compare_bytes(request_): +def test_protocol_compare_bytes(request_: Request): request_.protocol = (1, 0) assert request_.protocol == b'HTTP/1.0' @@ -42,7 +43,7 @@ def test_invalid_protocol(request_, invalid): request_.protocol.set(invalid) -def test_protocol_comparision(request_): +def test_protocol_comparision(request_: Request): request_.protocol = (1, 2) assert request_.protocol < (2, 0) assert request_.protocol < 2 diff --git a/tests/api/test_status.py b/tests/api/test_status.py index 350b758..f8bc9e2 100644 --- a/tests/api/test_status.py +++ b/tests/api/test_status.py @@ -1,6 +1,7 @@ import pytest from httoop import Status +from httoop.messages import Response from httoop.status import ( CREATED, INTERNAL_SERVER_ERROR, @@ -66,7 +67,7 @@ def test_stats_int(response): assert int(response.status) == 100 -def test_status_tuple(response): +def test_status_tuple(response: Response): response.status = 200 assert response.status.code == 200 assert response.status.reason == 'OK' @@ -122,7 +123,7 @@ def test_status_parse(response): assert not response.status.reason -def test_status_type(response): +def test_status_type(response: Response): response.status = 100 assert response.status.informational assert not response.status.successful diff --git a/tests/api/test_uri.py b/tests/api/test_uri.py index c859b6a..2574091 100644 --- a/tests/api/test_uri.py +++ b/tests/api/test_uri.py @@ -1,19 +1,20 @@ import pytest +from httoop import Request from httoop.exceptions import InvalidURI -def test_uri_set_string(request_): +def test_uri_set_string(request_: Request): request_.uri = '/foo' assert request_.uri == '/foo' -def test_uri_set_bytes(request_): +def test_uri_set_bytes(request_: Request): request_.uri = b'/foo' assert request_.uri == b'/foo' -def test_uri_set_dict(request_): +def test_uri_set_dict(request_: Request): uri = { 'scheme': 'http', 'username': 'username', @@ -30,7 +31,7 @@ def test_uri_set_dict(request_): assert bytes(request_.uri) == b'http://username:password@host:8090/path?query=string#fragment' -def test_set_invalid_uri_nonascii(request_): +def test_set_invalid_uri_nonascii(request_: Request): with pytest.raises(InvalidURI): request_.uri = '/fooäbar' with pytest.raises(InvalidURI): @@ -39,20 +40,20 @@ def test_set_invalid_uri_nonascii(request_): request_.uri = '/fooäbar'.encode() -def test_set_invalid_uri(request_): +def test_set_invalid_uri(request_: Request): with pytest.raises(TypeError): request_.uri = 1 with pytest.raises(TypeError): request_.uri.path = 1 -def test_set_latin1_bytes_uri_path(request_): # just for code coverage... behvaior is stupid +def test_set_latin1_bytes_uri_path(request_: Request): # just for code coverage... behvaior is stupid request_.uri.path = b'/foo\xffbar' assert bytes(request_.uri) == b'/foo%C3%BFbar' @pytest.mark.xfail -def test_uri_path_segments(request_): +def test_uri_path_segments(request_: Request): request_.uri.parse(b'/fo%2fbar/baz%2Fblub') assert request_.uri.path_segments == ['', 'fo/bar', 'baz/blub'] request_.uri.path_segments = ['', 'my/path', 'segments'] diff --git a/tests/api/test_wsgi.py b/tests/api/test_wsgi.py index 84c02d5..305970b 100644 --- a/tests/api/test_wsgi.py +++ b/tests/api/test_wsgi.py @@ -165,6 +165,7 @@ def application15(environ, start_response): def test_essential_parameters(): client = WSGIClient({'CONTENT_TYPE': 'text/html', 'CONTENT_LENGTH': '0', 'HTTP_HOST': 'foobar'}) client(application15) + assert client.exc_info is not None assert client.exc_info[1].args[0] is True diff --git a/tests/conftest.py b/tests/conftest.py index adb4c79..065422c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,37 +4,37 @@ @pytest.fixture -def request_(): +def request_() -> Request: return Request() @pytest.fixture -def response(): +def response() -> Response: return Response() @pytest.fixture -def headers(): +def headers() -> Headers: return Headers() @pytest.fixture -def body(): +def body() -> Body: return Body() @pytest.fixture -def statemachine(): +def statemachine() -> ServerStateMachine: return ServerStateMachine('http', 'localhost', 8090) @pytest.fixture -def clientstatemachine(): +def clientstatemachine() -> ClientStateMachine: c = ClientStateMachine() c.request = Request() return c @pytest.fixture -def uri(): +def uri() -> URI: return URI() diff --git a/tests/messaging/test_body.py b/tests/messaging/test_body.py index 6bd65be..a802227 100644 --- a/tests/messaging/test_body.py +++ b/tests/messaging/test_body.py @@ -2,13 +2,13 @@ import pytest -from httoop import BAD_REQUEST +from httoop import BAD_REQUEST, Request from httoop.codecs import lookup from httoop.semantic.request import ComposedRequest from httoop.semantic.response import ComposedResponse -def test_chunked_body_without_trailer(request_): +def test_chunked_body_without_trailer(request_: Request): def content(): yield 'Let us test this chunked' yield '\n' @@ -32,7 +32,7 @@ def test_parse_chunked_body_without_trailer(statemachine): assert bytes(request_.body) == request_body -def test_parse_chunked_body_without_trailer_2(request_): +def test_parse_chunked_body_without_trailer_2(request_: Request): request_.body = [ 'This is a chunked body with some lines', 'foo', 'bar', 'Baz', '\n', '', 'blah!', 'blub' @@ -54,7 +54,7 @@ def test_parse_transfer_encoding_deflate(statemachine): assert request.body == 'this is a test' -def test_body_parse_transfer_encoding_deflate(request_): +def test_body_parse_transfer_encoding_deflate(request_: Request): request_.body.transfer_encoding = 'deflate' request_.body.parse(b'x\x9c+\xc9\xc8,V\x00\xa2D\x85\x92\xd4\xe2\x12\x00&3\x05\x16') assert request_.body == 'this is a test' @@ -79,7 +79,7 @@ def test_parse_chunked_body_with_untold_trailer(statemachine): assert 'untold trailers: "Bar"' in str(exc.value) -def test_chunked_body_with_trailer(request_): +def test_chunked_body_with_trailer(request_: Request): def content(): yield 'Let us test this chunked' yield '\n' @@ -155,13 +155,13 @@ def test_parse_chunked_body_with_invalid_terminator(statemachine): assert 'Invalid chunk terminator' in str(exc.value.description) -def test_body_compress(request_): +def test_body_compress(request_: Request): request_.body = 'this is a test' request_.body.content_encoding = 'deflate' request_.body.compress() assert lookup('application/zlib').decode(bytes(request_.body)) == 'this is a test' -def test_body_encode_none(request_): +def test_body_encode_none(request_: Request): request_.body.mimetype = 'application/json' request_.body.encode() diff --git a/tests/messaging/test_request_codecs.py b/tests/messaging/test_request_codecs.py index 56eaf68..e0767bc 100644 --- a/tests/messaging/test_request_codecs.py +++ b/tests/messaging/test_request_codecs.py @@ -38,7 +38,7 @@ def test_percent_encoding(body): ) -def test_parse_multipart_form_data(body): +def test_parse_multipart_form_data(body: Body): body.mimetype = 'multipart/form-data' content_type = body.mimetype content_type.boundary = '------------------------409e1d7f8fe3763a' @@ -58,7 +58,7 @@ def test_parse_multipart_form_data(body): assert bar.headers.element('Content-Disposition').params['filename'] == 'test.txt' -def test_compose_multipart_form_data(body): +def test_compose_multipart_form_data(body: Body): body.mimetype = 'multipart/form-data' content_type = body.mimetype content_type.boundary = '------------------------409e1d7f8fe3763a' diff --git a/tests/messaging/test_semantic.py b/tests/messaging/test_semantic.py index a873793..3393671 100644 --- a/tests/messaging/test_semantic.py +++ b/tests/messaging/test_semantic.py @@ -1,21 +1,22 @@ import pytest +from httoop import Request, Response from httoop.semantic.request import ComposedRequest from httoop.semantic.response import ComposedResponse -def test_composing(request_, response): +def test_composing(request_: Request, response: Response): c = ComposedResponse(response, request_) c.prepare() -def test_request_close(request_): +def test_request_close(request_: Request): c = ComposedRequest(request_) c.close = True assert request_.headers['Connection'] == 'close' -def test_request_trace_header_removal(request_): +def test_request_trace_header_removal(request_: Request): request_.headers['Cookie'] = 'foo=bar' request_.headers['WWW-Authenticate'] = 'foo bar' request_.method = 'TRACE' @@ -25,14 +26,14 @@ def test_request_trace_header_removal(request_): assert 'WWW-Authenticate' not in request_.headers -def test_request_host_header_set(request_): +def test_request_host_header_set(request_: Request): request_.uri = 'http://www.example.com/foo' c = ComposedRequest(request_) c.prepare() assert request_.headers['Host'] == 'www.example.com' -def test_request_defaults(request_): +def test_request_defaults(request_: Request): request_.body = 'foo' c = ComposedRequest(request_) c.chunked = True @@ -53,7 +54,7 @@ def test_request_defaults_body(request_, method): assert request_.headers.get('Content-Length') == '3' -def test_chunked(request_): +def test_chunked(request_: Request): c = ComposedRequest(request_) c.transfer_encoding = None c.transfer_encoding = b'chunked' @@ -72,7 +73,7 @@ def test_response_no_body(request_, response, status): assert not response.body -def test_response_no_body_head(request_, response): +def test_response_no_body_head(request_: Request, response: Response): response.body = 'foo' request_.method = 'HEAD' c = ComposedResponse(response, request_) @@ -80,14 +81,14 @@ def test_response_no_body_head(request_, response): assert not response.body -def test_response_allow_default_header(request_, response): +def test_response_allow_default_header(request_: Request, response: Response): response.status = 405 c = ComposedResponse(response, request_) c.prepare() assert response.headers['Allow'] == 'GET, HEAD' -def test_byte_ranges(request_, response): +def test_byte_ranges(request_: Request, response: Response): request_.headers['Range'] = 'bytes=3-5' response.headers['ETag'] = 'foo' response.body = 'foobarbaz' @@ -98,7 +99,7 @@ def test_byte_ranges(request_, response): assert response.headers['Content-Range'] == 'bytes 3-5/9' -def test_invalid_byte_ranges(request_, response): +def test_invalid_byte_ranges(request_: Request, response: Response): request_.headers['Range'] = 'bytes=5-3' response.headers['ETag'] = 'foo' response.body = 'foobarbaz' @@ -110,7 +111,7 @@ def test_invalid_byte_ranges(request_, response): assert response.headers['Content-Range'] == 'bytes */9' -def test_multipart_byte_ranges(request_, response): +def test_multipart_byte_ranges(request_: Request, response: Response): request_.headers['Range'] = 'bytes=0-2, 6-' response.headers['ETag'] = 'foo' response.body = 'foobarbaz' @@ -121,7 +122,7 @@ def test_multipart_byte_ranges(request_, response): assert bytes(response.body) == b'--%(boundary)s\r\nContent-Range: bytes 0-2/9\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nfoo\r\n--%(boundary)s\r\nContent-Range: bytes 6-9/9\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nbaz\r\n--%(boundary)s--\r\n' % {b'boundary': response.headers.get_element('Content-Type').boundary.encode('ASCII')} -def test_content_range_is_set(request_, response): +def test_content_range_is_set(request_: Request, response: Response): response.status = 416 response.body = 'foobarbaz' c = ComposedResponse(response, request_) @@ -129,7 +130,7 @@ def test_content_range_is_set(request_, response): assert response.headers['Content-Range'] == 'bytes */9' -def test_response_trace_header_removal(request_, response): +def test_response_trace_header_removal(request_: Request, response: Response): response.headers['Set-Cookie'] = 'foo=bar' request_.method = 'TRACE' c = ComposedResponse(response, request_) @@ -137,7 +138,7 @@ def test_response_trace_header_removal(request_, response): assert 'Set-Cookie' not in response.headers -def test_response_close(request_, response): +def test_response_close(request_: Request, response: Response): c = ComposedResponse(response, request_) c.close = True assert response.headers['Connection'] == 'close' diff --git a/tests/uri/test_uri_query.py b/tests/uri/test_uri_query.py index 2d17f88..8514313 100644 --- a/tests/uri/test_uri_query.py +++ b/tests/uri/test_uri_query.py @@ -62,7 +62,7 @@ def test_parse_encodings(query_string, encoding, query): def test_urlencode_sequences(): u = URI() u.query = {'a': [1, 2], 'b': (3, 4, 5)} - assert set(u.query_string.split(b'&')) == {b'a=1', b'a=2', b'b=3', b'b=4', b'b=5'} + assert set(u.query_string.split('&')) == {b'a=1', b'a=2', b'b=3', b'b=4', b'b=5'} @pytest.mark.xfail(reason='API not yet implemented.')