Source code for imgproxykit.options.base

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, ClassVar, override

from imgproxykit.types import Argument, OptionType


[docs] class Option(ABC): """ Base class for typed imgproxy processing options. Concrete options provide a fixed name and positional arguments. Pass their instances to :class:`~imgproxykit.Transform`. This abstract class cannot be instantiated directly. Arguments retain ``None`` values until serialization: internal gaps keep their positions, while trailing empty arguments are omitted. See `imgproxy processing options <https://docs.imgproxy.net/usage/processing#processing-options>`_. """ __slots__ = () # Public API: name: ClassVar[OptionType] """ All subtypes must provide a supported imgproxy processing option name. The name is shared by all instances and is not a dataclass field or a constructor argument. """ def __post_init__(self) -> None: """Validate option arguments at construction time.""" self.validate()
[docs] def validate(self) -> None: # noqa: B027 """ Validate option arguments. Override this method to enforce server-independent constraints. The default implementation does nothing. Raises: ValueError: If option arguments are invalid. """
# TODO: Add per option validation @property @abstractmethod def arguments(self) -> tuple[Argument, ...]: """Positional arguments in imgproxy protocol order."""
type _ScalarRegistry = dict[str, type[ScalarOption[Any]]] @dataclass(frozen=True, slots=True) class ScalarOption[ValueT: Argument](Option): """ Base class for processing options accepting one scalar value. Subclasses are registered automatically using their processing option name, with hyphens replaced by underscores. This allows passing scalar values directly to :class:`~imgproxykit.Transform` as keyword arguments. Concrete subclasses specify the value type and processing option name. Override :attr:`arguments` when the option requires custom encoding. Attributes: value: Scalar processing option value. """ value: ValueT _registry: ClassVar[_ScalarRegistry] = {} @override def __init_subclass__(cls) -> None: """Register a concrete scalar option for keyword construction.""" # Explicit super is required because slots=True replaces this class. super(ScalarOption, cls).__init_subclass__() # noqa: WPS608 # Slotted dataclasses create a replacement class. Register it again # so keyword construction uses the final class, including its slots. ScalarOption._registry[cls._get_kwarg_name()] = cls @property @override def arguments(self) -> tuple[Argument, ...]: """Positional arguments in imgproxy protocol order.""" return (self.value,) @classmethod def from_kwarg(cls, name: str, value: Argument) -> 'ScalarOption[Any]': """ Construct a registered scalar option from a Transform keyword. The concrete option constructor handles value validation and protocol-specific encoding, just as with positional options. Args: name: Python keyword name of a registered scalar option. value: Value passed to the concrete option constructor. Raises: TypeError: If the keyword does not identify a scalar option. ValueError: If the concrete option rejects the supplied value. """ option_cls = cls._registry.get(name) if option_cls is None: raise TypeError( f'Unknown scalar processing option {name!r}. ' 'Transform keyword arguments must name a registered scalar ' 'option. ' 'Pass complex processing options as positional Option ' 'instances.', ) return option_cls(value) @classmethod def _get_kwarg_name(cls) -> str: """Convert the processing option name to its Python keyword name.""" return cls.name.replace('-', '_')