From 0b910c03961935cfd2f23a91565ac3f44107c6e3 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:23:37 -0800 Subject: [PATCH] Breaking: Drop support for Python 3.8 (#43) --- .github/workflows/ci.yml | 3 +- .pre-commit-config.yaml | 6 ++-- CHANGELOG.md | 10 ++++-- pyproject.toml | 18 ++++------ src/gotenberg_client/_base.py | 13 +++---- src/gotenberg_client/_client.py | 8 ++--- src/gotenberg_client/_convert/chromium.py | 8 ++--- src/gotenberg_client/_convert/common.py | 12 +++---- src/gotenberg_client/_convert/libre_office.py | 3 +- src/gotenberg_client/_convert/pdfa.py | 3 +- src/gotenberg_client/_merge.py | 3 +- src/gotenberg_client/_utils.py | 5 ++- src/gotenberg_client/options.py | 17 +++++----- tests/conftest.py | 2 +- tests/test_convert_chromium_html.py | 18 +++++----- tests/test_convert_chromium_url.py | 34 ++++++------------- tests/test_metadata.py | 3 +- tests/utils.py | 23 +++++-------- 18 files changed, 76 insertions(+), 113 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9e9f3e..062bb58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,8 +58,7 @@ jobs: strategy: fail-fast: false matrix: - # No pikepdf wheels for pypy3.8 - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.9', 'pypy3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.9', 'pypy3.10'] steps: - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11d1ffb..17624aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: detect-private-key # See https://github.com/prettier/prettier/issues/15742 for the fork reason - repo: https://github.com/rbubley/mirrors-prettier - rev: "v3.3.3" + rev: "v3.4.2" hooks: - id: prettier types_or: @@ -45,13 +45,13 @@ repos: - id: codespell # Python hooks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.6.9' + rev: 'v0.8.2' hooks: # Run the linter. - id: ruff # Run the formatter. - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt - rev: "2.2.4" + rev: "v2.5.0" hooks: - id: pyproject-fmt diff --git a/CHANGELOG.md b/CHANGELOG.md index d627065..f1b9f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Breaking Change + +- Dopped support for Python 3.8 ([#43](https://github.com/stumpylog/gotenberg-client/pull/43)) + ### Added -- Official support and testing for Python 3.13 ([#25](https://github.com/stumpylog/tika-client/pull/25)) -- Support for setting PDF metadata ([#42](https://github.com/stumpylog/tika-client/pull/42)) - - Initial work by @spechtx in ([#40](https://github.com/stumpylog/tika-client/pull/40)) +- Official support and testing for Python 3.13 ([#25](https://github.com/stumpylog/gotenberg-client/pull/25)) +- Support for setting PDF metadata ([#42](https://github.com/stumpylog/gotenberg-client/pull/42)) + - Initial work by @spechtx in ([#40](https://github.com/stumpylog/gotenberg-client/pull/40)) ### Changed diff --git a/pyproject.toml b/pyproject.toml index e6fa83e..97bd434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ license = "MPL-2.0" authors = [ { name = "Trenton H", email = "rda0128ou@mozmail.com" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", @@ -25,7 +25,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -36,8 +35,7 @@ classifiers = [ ] dynamic = [ "version" ] dependencies = [ - "httpx[http2]~=0.24; python_version<'3.9'", - "httpx[http2]~=0.27; python_version>='3.9'", + "httpx[http2]~=0.28", "typing-extensions; python_version<'3.11'", ] @@ -75,7 +73,6 @@ randomize = true dependencies = [ "coverage-enable-subprocess == 1.0", "coverage[toml] ~= 7.6", - "pytest < 8.0; python_version < '3.9'", "pytest ~= 8.3; python_version >= '3.9'", "pytest-mock ~= 3.14", "pytest-randomly ~= 3.15", @@ -84,8 +81,7 @@ dependencies = [ ] extra-dependencies = [ "pytest-sugar", - "pytest-httpx == 0.30.0; python_version >= '3.9'", - "pytest-httpx ~= 0.22; python_version < '3.9'", + "pytest-httpx ~= 0.35", "pikepdf", "python-magic", "pytest-docker ~= 3.1", @@ -111,7 +107,7 @@ cov-report = [ ] [[tool.hatch.envs.hatch-test.matrix]] -python = [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9", "pypy3.10" ] +python = [ "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9", "pypy3.10" ] # # Custom Environments @@ -119,11 +115,11 @@ python = [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9", "pypy3.10" ] [tool.hatch.envs.typing] detached = true dependencies = [ - "mypy ~= 1.11", + "mypy ~= 1.13", "httpx", "pytest", "pikepdf", - "pytest-httpx == 0.30.0", + "pytest-httpx ~= 0.35", ] [tool.hatch.envs.typing.scripts] @@ -170,7 +166,7 @@ deploy = [ # [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 120 # https://docs.astral.sh/ruff/settings/ diff --git a/src/gotenberg_client/_base.py b/src/gotenberg_client/_base.py index 0603dee..9c93062 100644 --- a/src/gotenberg_client/_base.py +++ b/src/gotenberg_client/_base.py @@ -6,10 +6,7 @@ from pathlib import Path from time import sleep from types import TracebackType -from typing import Dict from typing import Optional -from typing import Tuple -from typing import Type from httpx import Client from httpx import HTTPStatusError @@ -71,13 +68,13 @@ def __init__(self, client: Client, api_route: str) -> None: self._route = api_route self._stack = ExitStack() # These are the options that will be set to Gotenberg. Things like PDF/A - self._form_data: Dict[str, str] = {} + self._form_data: dict[str, str] = {} # These are the names of files, mapping to their Path - self._file_map: Dict[str, Path] = {} + self._file_map: dict[str, Path] = {} # Additional in memory resources, mapping the referenced name to the content and an optional mimetype - self._in_memory_resources: Dict[str, Tuple[str, Optional[str]]] = {} + self._in_memory_resources: dict[str, tuple[str, Optional[str]]] = {} # Any header that will also be sent - self._headers: Dict[str, str] = {} + self._headers: dict[str, str] = {} def __enter__(self) -> Self: self.reset() @@ -85,7 +82,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: diff --git a/src/gotenberg_client/_client.py b/src/gotenberg_client/_client.py index 1c53259..c1fe470 100644 --- a/src/gotenberg_client/_client.py +++ b/src/gotenberg_client/_client.py @@ -3,9 +3,7 @@ # SPDX-License-Identifier: MPL-2.0 import logging from types import TracebackType -from typing import Dict from typing import Optional -from typing import Type from httpx import Client @@ -65,7 +63,7 @@ def __init__( self.merge = MergeApi(self._client) self.health = HealthCheckApi(self._client) - def add_headers(self, header: Dict[str, str]) -> None: + def add_headers(self, header: dict[str, str]) -> None: """ Update the httpx Client headers with the given values. @@ -110,7 +108,7 @@ def set_error_webhook_http_method(self, method: HttpMethodsType = "PUT") -> None """ self.add_headers({"Gotenberg-Webhook-Error-Method": method}) - def set_webhook_extra_headers(self, extra_headers: Dict[str, str]) -> None: + def set_webhook_extra_headers(self, extra_headers: dict[str, str]) -> None: """ Set additional HTTP headers for Gotenberg to use when calling webhooks. @@ -138,7 +136,7 @@ def close(self) -> None: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: diff --git a/src/gotenberg_client/_convert/chromium.py b/src/gotenberg_client/_convert/chromium.py index 930a94d..b0de523 100644 --- a/src/gotenberg_client/_convert/chromium.py +++ b/src/gotenberg_client/_convert/chromium.py @@ -3,10 +3,8 @@ # SPDX-License-Identifier: MPL-2.0 import logging from pathlib import Path -from typing import List from typing import Literal from typing import Optional -from typing import Tuple from httpx import Client @@ -83,7 +81,7 @@ def string_resource(self, resource: str, name: str, mime_type: Optional[str] = N self._add_in_memory_file(resource, name=name, mime_type=mime_type) return self - def resources(self, resources: List[Path]) -> Self: + def resources(self, resources: list[Path]) -> Self: """ Adds multiple resource files for the index HTML file to reference. @@ -95,7 +93,7 @@ def resources(self, resources: List[Path]) -> Self: def string_resources( self, - resources: List[Tuple[str, str, Optional[str]]], + resources: list[tuple[str, str, Optional[str]]], ) -> Self: """ Process string resources. @@ -215,7 +213,7 @@ def markdown_file(self, markdown_file: Path) -> Self: return self - def markdown_files(self, markdown_files: List[Path]) -> Self: + def markdown_files(self, markdown_files: list[Path]) -> Self: """ Adds multiple Markdown files to be converted. diff --git a/src/gotenberg_client/_convert/common.py b/src/gotenberg_client/_convert/common.py index 89757cb..27671ff 100644 --- a/src/gotenberg_client/_convert/common.py +++ b/src/gotenberg_client/_convert/common.py @@ -3,12 +3,10 @@ # SPDX-License-Identifier: MPL-2.0 import json import logging +from collections.abc import Iterable from datetime import datetime from pathlib import Path -from typing import Dict from typing import Final -from typing import Iterable -from typing import List from typing import Optional from typing import Union from warnings import warn @@ -195,7 +193,7 @@ def user_agent(self, agent: str) -> Self: self._form_data.update({"userAgent": agent}) # type: ignore[attr-defined,misc] return self - def headers(self, headers: Dict[str, str]) -> Self: + def headers(self, headers: dict[str, str]) -> Self: json_str = json.dumps(headers) self._form_data.update({"extraHttpHeaders": json_str}) # type: ignore[attr-defined,misc] return self @@ -294,7 +292,7 @@ def metadata( pdf_copyright: Optional[str] = None, creation_date: Optional[datetime] = None, creator: Optional[str] = None, - keywords: Optional[List[str]] = None, + keywords: Optional[list[str]] = None, marked: Optional[bool] = None, modification_date: Optional[datetime] = None, pdf_version: Optional[float] = None, @@ -344,12 +342,12 @@ def metadata( raise InvalidKeywordError("Keywords cannot contain commas") # noqa: EM101, TRY003 # Get existing metadata if any - existing_metadata: Dict[str, Union[str, bool, float]] = {} + existing_metadata: dict[str, Union[str, bool, float]] = {} if "metadata" in self._form_data: # type: ignore[attr-defined,misc] existing_metadata = json.loads(self._form_data["metadata"]) # type: ignore[attr-defined,misc] # Convert validated metadata to dictionary - metadata: Dict[str, Union[str, bool, float]] = {} + metadata: dict[str, Union[str, bool, float]] = {} if author: metadata["Author"] = author diff --git a/src/gotenberg_client/_convert/libre_office.py b/src/gotenberg_client/_convert/libre_office.py index 1af7e5d..aaf9560 100644 --- a/src/gotenberg_client/_convert/libre_office.py +++ b/src/gotenberg_client/_convert/libre_office.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: MPL-2.0 from pathlib import Path -from typing import List from typing import Union from httpx import Client @@ -54,7 +53,7 @@ def convert(self, input_file_path: Path) -> Self: self._result_is_zip = True return self - def convert_files(self, file_paths: List[Path]) -> Self: + def convert_files(self, file_paths: list[Path]) -> Self: """ Adds all provided files for conversion to individual PDFs. diff --git a/src/gotenberg_client/_convert/pdfa.py b/src/gotenberg_client/_convert/pdfa.py index e799fa4..1c383d7 100644 --- a/src/gotenberg_client/_convert/pdfa.py +++ b/src/gotenberg_client/_convert/pdfa.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: MPL-2.0 from pathlib import Path -from typing import List from gotenberg_client._base import BaseApi from gotenberg_client._base import BaseSingleFileResponseRoute @@ -35,7 +34,7 @@ def convert(self, file_path: Path) -> Self: self._add_file_map(file_path) return self - def convert_files(self, file_paths: List[Path]) -> Self: + def convert_files(self, file_paths: list[Path]) -> Self: """ Converts multiple PDF files to the provided PDF/A format. diff --git a/src/gotenberg_client/_merge.py b/src/gotenberg_client/_merge.py index d08ddf5..aee43cd 100644 --- a/src/gotenberg_client/_merge.py +++ b/src/gotenberg_client/_merge.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: MPL-2.0 from pathlib import Path from typing import Final -from typing import List from httpx import Client @@ -38,7 +37,7 @@ def __init__(self, client: Client, api_route: str) -> None: super().__init__(client, api_route) self._next = 1 - def merge(self, files: List[Path]) -> Self: + def merge(self, files: list[Path]) -> Self: """ Add the given files to the merge operation. diff --git a/src/gotenberg_client/_utils.py b/src/gotenberg_client/_utils.py index 97cfb2e..e671c0f 100644 --- a/src/gotenberg_client/_utils.py +++ b/src/gotenberg_client/_utils.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: MPL-2.0 from importlib.util import find_spec from pathlib import Path -from typing import Dict from typing import Final from typing import Optional from typing import Union @@ -12,12 +11,12 @@ # See https://github.com/psf/requests/issues/1081#issuecomment-428504128 -class ForceMultipartDict(Dict): +class ForceMultipartDict(dict): def __bool__(self) -> bool: return True -def optional_to_form(value: Optional[FormFieldType], name: str) -> Dict[str, str]: +def optional_to_form(value: Optional[FormFieldType], name: str) -> dict[str, str]: """ Converts an optional value to a form data field with the given name, handling None values gracefully. diff --git a/src/gotenberg_client/options.py b/src/gotenberg_client/options.py index dc67a23..1fa4f2f 100644 --- a/src/gotenberg_client/options.py +++ b/src/gotenberg_client/options.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: MPL-2.0 import dataclasses import enum -from typing import Dict from typing import Final from typing import Optional @@ -28,7 +27,7 @@ class PdfAFormat(enum.Enum): A2b = enum.auto() A3b = enum.auto() - def to_form(self) -> Dict[str, str]: + def to_form(self) -> dict[str, str]: """ Converts this PdfAFormat enum value to a dictionary suitable for form data. @@ -38,7 +37,7 @@ def to_form(self) -> Dict[str, str]: If the format is not supported (e.g., A1a), raises an Exception. """ - format_mapping: Final[Dict[PdfAFormat, str]] = { + format_mapping: Final[dict[PdfAFormat, str]] = { PdfAFormat.A1a: "PDF/A-1a", # Include deprecated format with warning PdfAFormat.A2b: "PDF/A-2b", PdfAFormat.A3b: "PDF/A-3b", @@ -67,7 +66,7 @@ class PageOrientation(enum.Enum): Landscape = enum.auto() Portrait = enum.auto() - def to_form(self) -> Dict[str, str]: + def to_form(self) -> dict[str, str]: """ Converts this PageOrientation enum value to a dictionary suitable for form data. @@ -76,7 +75,7 @@ def to_form(self) -> Dict[str, str]: and the corresponding Gotenberg value ("landscape" or "portrait") as the value. """ - orientation_mapping: Final[Dict[PageOrientation, Dict[str, str]]] = { + orientation_mapping: Final[dict[PageOrientation, dict[str, str]]] = { PageOrientation.Landscape: {"landscape": "true"}, PageOrientation.Portrait: {"landscape": "false"}, } @@ -97,7 +96,7 @@ class PageSize: width: Optional[PageSizeType] = None height: Optional[PageSizeType] = None - def to_form(self) -> Dict[str, str]: + def to_form(self) -> dict[str, str]: """ Converts this PageSize object to a dictionary suitable for form data. @@ -160,7 +159,7 @@ class MarginType: value: MarginSizeType unit: MarginUnitType = MarginUnitType.Undefined - def to_form(self, name: str) -> Dict[str, str]: + def to_form(self, name: str) -> dict[str, str]: """ Converts this MarginType object to a dictionary suitable for form data. @@ -193,7 +192,7 @@ class PageMarginsType: left: Optional[MarginType] = None right: Optional[MarginType] = None - def to_form(self) -> Dict[str, str]: + def to_form(self) -> dict[str, str]: """ Converts this PageMarginsType object to a dictionary suitable for form data. @@ -225,7 +224,7 @@ class EmulatedMediaType(str, enum.Enum): Print = enum.auto() Screen = enum.auto() - def to_form(self) -> Dict[str, str]: + def to_form(self) -> dict[str, str]: """ Converts this EmulatedMediaType enum value to a dictionary suitable for form data. diff --git a/tests/conftest.py b/tests/conftest.py index e970407..e143876 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,8 +4,8 @@ import logging import os import shutil +from collections.abc import Generator from pathlib import Path -from typing import Generator from typing import Union import httpx diff --git a/tests/test_convert_chromium_html.py b/tests/test_convert_chromium_html.py index ed3d1d9..07d1a6a 100644 --- a/tests/test_convert_chromium_html.py +++ b/tests/test_convert_chromium_html.py @@ -127,8 +127,8 @@ def test_convert_page_size(self, client: GotenbergClient, sample_directory: Path _ = route.index(test_file).size(A4).run() request = httpx_mock.get_request() - verify_stream_contains("paperWidth", "8.27", request.stream) - verify_stream_contains("paperHeight", "11.7", request.stream) + verify_stream_contains(request, "paperWidth", "8.27") + verify_stream_contains(request, "paperHeight", "11.7") def test_convert_margin(self, client: GotenbergClient, sample_directory: Path, httpx_mock: HTTPXMock): httpx_mock.add_response(method="POST") @@ -149,10 +149,10 @@ def test_convert_margin(self, client: GotenbergClient, sample_directory: Path, h ) request = httpx_mock.get_request() - verify_stream_contains("marginTop", "1cm", request.stream) - verify_stream_contains("marginBottom", "2pc", request.stream) - verify_stream_contains("marginLeft", "3mm", request.stream) - verify_stream_contains("marginRight", "4", request.stream) + verify_stream_contains(request, "marginTop", "1cm") + verify_stream_contains(request, "marginBottom", "2pc") + verify_stream_contains(request, "marginLeft", "3mm") + verify_stream_contains(request, "marginRight", "4") def test_convert_render_control(self, client: GotenbergClient, sample_directory: Path, httpx_mock: HTTPXMock): httpx_mock.add_response(method="POST") @@ -161,8 +161,7 @@ def test_convert_render_control(self, client: GotenbergClient, sample_directory: with client.chromium.html_to_pdf() as route: _ = route.index(test_file).render_wait(500.0).run() - request = httpx_mock.get_request() - verify_stream_contains("waitDelay", "500.0", request.stream) + verify_stream_contains(httpx_mock.get_request(), "waitDelay", "500.0") @pytest.mark.parametrize( ("orientation"), @@ -181,9 +180,8 @@ def test_convert_orientation( with client.chromium.html_to_pdf() as route: _ = route.index(test_file).orient(orientation).run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "landscape", "true" if orientation == PageOrientation.Landscape else "false", - request.stream, ) diff --git a/tests/test_convert_chromium_url.py b/tests/test_convert_chromium_url.py index e0f5e7d..1204ed6 100644 --- a/tests/test_convert_chromium_url.py +++ b/tests/test_convert_chromium_url.py @@ -41,11 +41,10 @@ def test_convert_orientation( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).media_type(emulation).run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "emulatedMediaType", "screen" if emulation == EmulatedMediaType.Screen else "print", - request.stream, ) @pytest.mark.parametrize( @@ -66,11 +65,10 @@ def test_convert_css_or_not_size( getattr(route, method)() _ = route.run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "preferCssPageSize", "true" if method == "prefer_css_page_size" else "false", - request.stream, ) @pytest.mark.parametrize( @@ -91,11 +89,10 @@ def test_convert_background_graphics_or_not( getattr(route, method)() _ = route.run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "printBackground", "true" if method == "background_graphics" else "false", - request.stream, ) @pytest.mark.parametrize( @@ -116,11 +113,10 @@ def test_convert_hide_background_or_not( getattr(route, method)() _ = route.run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "omitBackground", "true" if method == "hide_background" else "false", - request.stream, ) @pytest.mark.parametrize( @@ -141,11 +137,10 @@ def test_convert_fail_exceptions( getattr(route, method)() _ = route.run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "failOnConsoleExceptions", "true" if method == "fail_on_exceptions" else "false", - request.stream, ) def test_convert_scale( @@ -159,11 +154,10 @@ def test_convert_scale( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).scale(1.5).run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "scale", "1.5", - request.stream, ) def test_convert_page_ranges( @@ -177,11 +171,10 @@ def test_convert_page_ranges( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).page_ranges("1-5").run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "nativePageRanges", "1-5", - request.stream, ) def test_convert_url_render_wait( @@ -195,11 +188,10 @@ def test_convert_url_render_wait( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).render_wait(500).run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "waitDelay", "500", - request.stream, ) def test_convert_url_render_expression( @@ -213,11 +205,10 @@ def test_convert_url_render_expression( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).render_expr("wait while false;").run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "waitForExpression", "wait while false;", - request.stream, ) @pytest.mark.filterwarnings("ignore::DeprecationWarning") @@ -232,11 +223,10 @@ def test_convert_url_user_agent( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).user_agent("Firefox").run() - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "userAgent", "Firefox", - request.stream, ) def test_convert_url_headers( @@ -251,10 +241,8 @@ def test_convert_url_headers( with client.chromium.url_to_pdf() as route: _ = route.url(webserver_docker_internal_url).headers(headers).run() - - request = httpx_mock.get_request() verify_stream_contains( + httpx_mock.get_request(), "extraHttpHeaders", json.dumps(headers), - request.stream, ) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index aefd454..af79ebc 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -2,7 +2,6 @@ from datetime import timedelta from datetime import timezone from pathlib import Path -from typing import List import pikepdf import pytest @@ -172,7 +171,7 @@ def test_metadata_invalid_pdf_keyword( self, client: GotenbergClient, webserver_docker_internal_url: str, - keywords: List[str], + keywords: list[str], ): with client.chromium.url_to_pdf() as route, pytest.raises(InvalidKeywordError): _ = ( diff --git a/tests/utils.py b/tests/utils.py index 1da4577..54bb19a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,24 +6,17 @@ import tempfile from pathlib import Path -from httpx._multipart import DataField -from httpx._multipart import FileField -from httpx._multipart import MultipartStream +def verify_stream_contains(request, key: str, value: str) -> None: + content_type = request.headers["Content-Type"] + assert "multipart/form-data" in content_type -def verify_stream_contains(key: str, value: str, stream: MultipartStream) -> None: - for item in stream.fields: - if isinstance(item, FileField): - continue - elif isinstance(item, DataField) and item.name == key: - actual_value = item.value - if isinstance(actual_value, bytes): - actual_value = actual_value.decode("utf-8") - assert actual_value == value, f"Key '{actual_value}' /= {value}" - return + boundary = content_type.split("boundary=")[1] - msg = f'Key "{key}" with value "{value}" not found in stream' - raise AssertionError(msg) + parts = request.content.split(f"--{boundary}".encode()) + + form_field_found = any(f'name="{key}"'.encode() in part and value.encode() in part for part in parts) + assert form_field_found, f'Key "{key}" with value "{value}" not found in stream' def extract_text(pdf_path: Path) -> str: