From d5e03707849e42d7cee6479d81f8f31b4e803791 Mon Sep 17 00:00:00 2001 From: Eirini Galati Date: Wed, 4 Nov 2026 10:45:49 +0100 Subject: [PATCH] feat: add Format constants for convert media types, release 1.8.0 Adds a Format namespace of media-type string constants so the types accepted by convert() are discoverable via autocomplete, matching the other Tinify client libraries and including JPEG XL (image/jxl), which the API already accepts. Each constant is annotated Final[Literal[...]] via type comments. Without that they infer as plain str and fail type checking against the ConvertTypes union, making the constants exactly what you could not pass to convert(). Type comments plus a TYPE_CHECKING-guarded import keep the module importable on the Python 2.7 the package still claims. Also adds the missing image/jpg to ConvertTypes: the API accepts it but the type hints did not, so passing it failed type checking. JPG documents image/jpg as the alternative spelling the API also accepts. It normalises to image/jpeg before conversion, so the two constants hold different strings but select the same format. Bumps to 1.8.0 and promotes the changelog entry out of Unreleased. The change is additive, so this is a minor release; nothing is published until the matching tag is pushed. --- CHANGES.md | 5 +++ test/unit/tinify_format_test.py | 60 +++++++++++++++++++++++++++++++++ tinify/__init__.py | 2 ++ tinify/_format.py | 58 +++++++++++++++++++++++++++++++ tinify/_typed.py | 2 +- tinify/version.py | 2 +- 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 test/unit/tinify_format_test.py create mode 100644 tinify/_format.py diff --git a/CHANGES.md b/CHANGES.md index 4092e15..14e60d9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +## 1.8.0 + +* Added `tinify.Format` constants for the media types `convert` accepts. +* Added the missing `image/jpg` media type to the `convert` type hints. + ## 1.7.2 * Add JXL to supported image types diff --git a/test/unit/tinify_format_test.py b/test/unit/tinify_format_test.py new file mode 100644 index 0000000..7bc693e --- /dev/null +++ b/test/unit/tinify_format_test.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +import json +import pytest + +import tinify +from tinify import Format, Source + + +class TestTinifyFormat: + def test_should_expose_supported_media_types(self): + assert Format.WEBP == "image/webp" + assert Format.PNG == "image/png" + assert Format.JPEG == "image/jpeg" + assert Format.JPG == "image/jpg" + assert Format.AVIF == "image/avif" + assert Format.JXL == "image/jxl" + assert Format.ANY == "*/*" + + def test_should_not_be_instantiable(self): + with pytest.raises(TypeError): + Format() + + +class TestTinifyFormatConvert: + @pytest.fixture(autouse=True) + def setup(self, mock_requests): + tinify.key = "valid" + mock_requests.post( + "https://api.tinify.com/shrink", + status_code=201, + headers={"Location": "https://api.tinify.com/some/location"}, + ) + self.mock_requests = mock_requests + yield + + def test_convert_with_format_should_serialize_media_type(self): + self.mock_requests.post( + "https://api.tinify.com/some/location", + status_code=200, + content=b"converted file", + ) + + Source.from_buffer("png file").convert(type=Format.JXL).to_buffer() + + body = json.loads(self.mock_requests.last_request.text) + assert body["convert"] == {"type": "image/jxl"} + + def test_convert_with_multiple_formats_should_serialize_media_types(self): + self.mock_requests.post( + "https://api.tinify.com/some/location", + status_code=200, + content=b"converted file", + ) + + Source.from_buffer("png file").convert( + type=[Format.JXL, Format.WEBP] + ).to_buffer() + + body = json.loads(self.mock_requests.last_request.text) + assert body["convert"] == {"type": ["image/jxl", "image/webp"]} diff --git a/tinify/__init__.py b/tinify/__init__.py index 9fcbd45..686a3cd 100644 --- a/tinify/__init__.py +++ b/tinify/__init__.py @@ -138,6 +138,7 @@ def from_url(url): # type: (str) -> Source from .version import __version__ +from ._format import Format from .client import Client from .result_meta import ResultMeta from .result import Result @@ -146,6 +147,7 @@ def from_url(url): # type: (str) -> Source __all__ = [ 'Client', + 'Format', 'Result', 'ResultMeta', 'Source', diff --git a/tinify/_format.py b/tinify/_format.py new file mode 100644 index 0000000..28144df --- /dev/null +++ b/tinify/_format.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals + +try: + from typing import TYPE_CHECKING +except ImportError: + TYPE_CHECKING = False # type: ignore + +if TYPE_CHECKING: + from typing import Final, Literal + + +class Format(object): + """Media types that images can be converted to. + + Use these constants with :meth:`Source.convert`:: + + tinify.from_file("input.png") \\ + .convert(type=tinify.Format.JXL) \\ + .to_file("output.jxl") + + Multiple types may be supplied, in which case the API returns the + smallest result:: + + .convert(type=[tinify.Format.JXL, tinify.Format.WEBP]) + + Each constant is typed as its own literal, so they satisfy the + :data:`~tinify._typed.ConvertTypes` hints that :meth:`Source.convert` + accepts. + """ + + #: WebP. + WEBP = "image/webp" # type: Final[Literal["image/webp"]] + + #: PNG. + PNG = "image/png" # type: Final[Literal["image/png"]] + + #: JPEG. + JPEG = "image/jpeg" # type: Final[Literal["image/jpeg"]] + + #: JPEG, under the alternative ``image/jpg`` media type the API also + #: accepts. The API normalises this to ``image/jpeg``, so the result is + #: identical to :attr:`JPEG`. The two constants hold different strings + #: but select the same format. + JPG = "image/jpg" # type: Final[Literal["image/jpg"]] + + #: AVIF. + AVIF = "image/avif" # type: Final[Literal["image/avif"]] + + #: JPEG XL. + JXL = "image/jxl" # type: Final[Literal["image/jxl"]] + + #: Wildcard, returns the smallest of the supported types. + ANY = "*/*" # type: Final[Literal["*/*"]] + + def __init__(self): + # type: () -> None + raise TypeError("Format is a namespace of constants and cannot be instantiated") diff --git a/tinify/_typed.py b/tinify/_typed.py index fc1a6da..06789e4 100644 --- a/tinify/_typed.py +++ b/tinify/_typed.py @@ -5,7 +5,7 @@ class ResizeOptions(TypedDict,total=False): width: Optional[int] height: Optional[int] -ConvertTypes = Literal['image/webp', 'image/jpeg', 'image/png', "image/avif", "image/jxl", "*/*"] +ConvertTypes = Literal['image/webp', 'image/jpeg', 'image/jpg', 'image/png', "image/avif", "image/jxl", "*/*"] class ConvertOptions(TypedDict, total=False): type: Union[ConvertTypes, List[ConvertTypes]] diff --git a/tinify/version.py b/tinify/version.py index 8adfee4..b280975 100644 --- a/tinify/version.py +++ b/tinify/version.py @@ -1 +1 @@ -__version__ = '1.7.2' +__version__ = '1.8.0'