v0.166.0
  1from collections.abc import Callable, Sequence
  2from typing import TYPE_CHECKING, Any
  3
  4from plain.postgres.dialect import adapt_ipaddressfield_value
  5from plain.preflight import PreflightResult
  6from plain.utils.ipv6 import clean_ipv6_address
  7from plain.validators import ip_address_validators
  8
  9from plain import exceptions
 10
 11from .base import NOT_PROVIDED, DefaultableField
 12
 13if TYPE_CHECKING:
 14    from plain.postgres.connection import DatabaseConnection
 15
 16
 17class GenericIPAddressField[T: (str, str | None) = str](DefaultableField[T]):
 18    db_type_sql = "inet"
 19    empty_strings_allowed = False
 20
 21    def __init__(
 22        self,
 23        *,
 24        protocol: str = "both",
 25        unpack_ipv4: bool = False,
 26        required: bool = True,
 27        allow_null: bool = False,
 28        default: Any = NOT_PROVIDED,
 29        validators: Sequence[Callable[..., Any]] = (),
 30    ):
 31        self.unpack_ipv4 = unpack_ipv4
 32        self.protocol = protocol
 33        (
 34            self.default_validators,
 35            self.invalid_error_message,
 36        ) = ip_address_validators(protocol, unpack_ipv4)
 37        super().__init__(
 38            required=required,
 39            allow_null=allow_null,
 40            default=default,
 41            validators=validators,
 42        )
 43
 44    def preflight(self, **kwargs: Any) -> list[PreflightResult]:
 45        return [
 46            *super().preflight(**kwargs),
 47            *self._check_required_and_null_values(),
 48        ]
 49
 50    def _check_required_and_null_values(self) -> list[PreflightResult]:
 51        if not getattr(self, "allow_null", False) and not getattr(
 52            self, "required", True
 53        ):
 54            return [
 55                PreflightResult(
 56                    fix="GenericIPAddressFields cannot have required=False if allow_null=False, "
 57                    "as blank values are stored as nulls.",
 58                    obj=self,
 59                    id="fields.generic_ip_field_null_blank_config",
 60                )
 61            ]
 62        return []
 63
 64    def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
 65        name, path, args, kwargs = super().deconstruct()
 66        if self.unpack_ipv4 is not False:
 67            kwargs["unpack_ipv4"] = self.unpack_ipv4
 68        if self.protocol != "both":
 69            kwargs["protocol"] = self.protocol
 70        return name, path, args, kwargs
 71
 72    def to_python(self, value: Any) -> str | None:
 73        if value is None:
 74            return None
 75        if not isinstance(value, str):
 76            value = str(value)
 77        value = value.strip()
 78        if ":" in value:
 79            return clean_ipv6_address(
 80                value, self.unpack_ipv4, self.invalid_error_message
 81            )
 82        return value
 83
 84    def get_db_prep_value(
 85        self, value: Any, connection: DatabaseConnection, prepared: bool = False
 86    ) -> Any:
 87        if not prepared:
 88            value = self.get_prep_value(value)
 89        return adapt_ipaddressfield_value(value)
 90
 91    def get_prep_value(self, value: Any) -> Any:
 92        value = super().get_prep_value(value)
 93        if value is None:
 94            return None
 95        if value and ":" in value:
 96            try:
 97                return clean_ipv6_address(value, self.unpack_ipv4)
 98            except exceptions.ValidationError:
 99                pass
100        return str(value)