From 49d565dfc58eaaf57fb2a2b65f613cd7a1597c0e Mon Sep 17 00:00:00 2001 From: Vijay Sirohi <191354616+Vijaysir0hi@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:54:30 +0530 Subject: [PATCH] Add validation for Quality and Rotate options Signed-off-by: Vijay Sirohi <191354616+Vijaysir0hi@users.noreply.github.com> --- imgproxykit/options/quality.py | 25 ++++++++-- imgproxykit/options/rotate.py | 20 +++++++- tests/test_unit/test_options/test_integer.py | 2 - tests/test_unit/test_options/test_quality.py | 48 ++++++++++++++++++++ tests/test_unit/test_options/test_rotate.py | 48 ++++++++++++++++++++ 5 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 tests/test_unit/test_options/test_quality.py create mode 100644 tests/test_unit/test_options/test_rotate.py diff --git a/imgproxykit/options/quality.py b/imgproxykit/options/quality.py index 40504c4..bec04ff 100644 --- a/imgproxykit/options/quality.py +++ b/imgproxykit/options/quality.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import ClassVar +from typing import ClassVar, override from imgproxykit.options.base import ScalarOption @@ -12,6 +12,9 @@ class Quality(ScalarOption[int]): Attributes: value: Quality percentage. Zero delegates to server quality settings. + Raises: + ValueError: If ``value`` is not an integer between 0 and 100. + Usage: >>> option = Quality(85) @@ -23,8 +26,24 @@ class Quality(ScalarOption[int]): """ - # Public API: - # Public API: name: ClassVar[str] = 'quality' short_name: ClassVar[str | None] = 'q' + + @override + def validate(self) -> None: + """ + Validate that the quality percentage is between 0 and 100. + + Raises: + ValueError: If quality is outside 0..100 or is a boolean. + + """ + if isinstance(self.value, bool) or not ( + isinstance(self.value, int) and 0 <= self.value <= 100 + ): + msg = ( + f'Quality must be an integer between 0 and 100, ' + f'got {self.value}.' + ) + raise ValueError(msg) diff --git a/imgproxykit/options/rotate.py b/imgproxykit/options/rotate.py index fdb3f36..fddf0f0 100644 --- a/imgproxykit/options/rotate.py +++ b/imgproxykit/options/rotate.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import ClassVar +from typing import ClassVar, override from imgproxykit.options.base import ScalarOption @@ -14,6 +14,9 @@ class Rotate(ScalarOption[int]): Attributes: value: Clockwise angle in degrees; must be a multiple of 90. + Raises: + ValueError: If ``value`` is not an integer multiple of 90. + Usage: >>> option = Rotate(90) @@ -27,3 +30,18 @@ class Rotate(ScalarOption[int]): name: ClassVar[str] = 'rotate' short_name: ClassVar[str | None] = 'rot' + + @override + def validate(self) -> None: + """ + Validate that the angle is an integer multiple of 90 degrees. + + Raises: + ValueError: If angle is not a multiple of 90 or is a boolean. + + """ + if isinstance(self.value, bool) or not ( + isinstance(self.value, int) and self.value % 90 == 0 + ): + msg = f'Angle must be an integer multiple of 90, got {self.value}.' + raise ValueError(msg) diff --git a/tests/test_unit/test_options/test_integer.py b/tests/test_unit/test_options/test_integer.py index c6594c1..f6684fe 100644 --- a/tests/test_unit/test_options/test_integer.py +++ b/tests/test_unit/test_options/test_integer.py @@ -13,9 +13,7 @@ (options.Height, 'height'), (options.MinWidth, 'min-width'), (options.MinHeight, 'min-height'), - (options.Quality, 'quality'), (options.Pixelate, 'pixelate'), - (options.Rotate, 'rotate'), (options.Expires, 'expires'), (options.MaxBytes, 'max_bytes'), (options.MaxResultDimension, 'max_result_dimension'), diff --git a/tests/test_unit/test_options/test_quality.py b/tests/test_unit/test_options/test_quality.py new file mode 100644 index 0000000..a472ad3 --- /dev/null +++ b/tests/test_unit/test_options/test_quality.py @@ -0,0 +1,48 @@ +from typing import Any + +import pytest + +from imgproxykit import Transform +from imgproxykit.options import Quality +from imgproxykit.serialization import PathSerializer + + +@pytest.mark.parametrize('value', [0, 1, 50, 99, 100]) +def test_quality_valid(path_serializer: PathSerializer, *, value: int) -> None: + """Ensure that valid quality values serialize correctly.""" + option = Quality(value) + expected = f'quality:{value}' + assert ( + path_serializer._serialize_segment(option.name, option.arguments) + == expected + ) + + +@pytest.mark.parametrize('value', [0, 75, 100]) +def test_quality_transform_valid(value: int) -> None: + """Ensure that valid quality works with Transform kwargs.""" + transform = Transform(quality=value) + assert transform.options == (Quality(value),) + + +@pytest.mark.parametrize( + 'invalid_value', + [-10, -1, 101, 200, True, False, 85.5, '85'], +) +def test_quality_invalid(invalid_value: Any) -> None: + """Ensure that invalid quality values raise ValueError.""" + with pytest.raises( + ValueError, + match='Quality must be an integer between 0 and 100', + ): + Quality(invalid_value) + + +@pytest.mark.parametrize('invalid_value', [-1, 101, True]) +def test_quality_transform_invalid(invalid_value: Any) -> None: + """Ensure that invalid quality in Transform kwargs raises ValueError.""" + with pytest.raises( + ValueError, + match='Quality must be an integer between 0 and 100', + ): + Transform(quality=invalid_value) # type: ignore[arg-type] diff --git a/tests/test_unit/test_options/test_rotate.py b/tests/test_unit/test_options/test_rotate.py new file mode 100644 index 0000000..787d413 --- /dev/null +++ b/tests/test_unit/test_options/test_rotate.py @@ -0,0 +1,48 @@ +from typing import Any + +import pytest + +from imgproxykit import Transform +from imgproxykit.options import Rotate +from imgproxykit.serialization import PathSerializer + + +@pytest.mark.parametrize('angle', [0, 90, 180, 270, 360, -90, -180]) +def test_rotate_valid(path_serializer: PathSerializer, *, angle: int) -> None: + """Ensure that valid rotation angles serialize correctly.""" + option = Rotate(angle) + expected = f'rotate:{angle}' + assert ( + path_serializer._serialize_segment(option.name, option.arguments) + == expected + ) + + +@pytest.mark.parametrize('angle', [0, 90, 180, 270]) +def test_rotate_transform_valid(angle: int) -> None: + """Ensure that valid rotation angle works with Transform kwargs.""" + transform = Transform(rotate=angle) + assert transform.options == (Rotate(angle),) + + +@pytest.mark.parametrize( + 'invalid_angle', + [1, 45, 89, 91, 100, -45, True, False, 90.0, '90'], +) +def test_rotate_invalid(invalid_angle: Any) -> None: + """Ensure that invalid rotation angles raise ValueError.""" + with pytest.raises( + ValueError, + match='Angle must be an integer multiple of 90', + ): + Rotate(invalid_angle) + + +@pytest.mark.parametrize('invalid_angle', [45, 100, True]) +def test_rotate_transform_invalid(invalid_angle: Any) -> None: + """Ensure that invalid rotation in Transform kwargs raises ValueError.""" + with pytest.raises( + ValueError, + match='Angle must be an integer multiple of 90', + ): + Transform(rotate=invalid_angle) # type: ignore[arg-type]