v0.156.1
   1"""
   2Field classes.
   3"""
   4
   5from __future__ import annotations
   6
   7import copy
   8import datetime
   9import enum
  10import json
  11import math
  12import re
  13import uuid
  14from collections.abc import Callable, Iterable, Iterator, Sequence
  15from decimal import Decimal, DecimalException
  16from io import BytesIO
  17from typing import TYPE_CHECKING, Any, Self
  18from urllib.parse import urlsplit, urlunsplit
  19
  20from plain import validators as validators_
  21from plain.exceptions import ValidationError
  22from plain.utils import timezone
  23from plain.utils.dateparse import parse_datetime, parse_duration
  24from plain.utils.duration import duration_string
  25from plain.utils.regex_helper import _lazy_re_compile
  26from plain.utils.text import pluralize_lazy
  27
  28from .boundfield import BoundField
  29from .exceptions import FormFieldMissingError
  30
  31if TYPE_CHECKING:
  32    from .forms import BaseForm
  33
  34__all__ = [
  35    "Field",
  36    "TextField",
  37    "IntegerField",
  38    "DateField",
  39    "TimeField",
  40    "DateTimeField",
  41    "DurationField",
  42    "RegexField",
  43    "EmailField",
  44    "FileField",
  45    "ImageField",
  46    "URLField",
  47    "BooleanField",
  48    "NullBooleanField",
  49    "ChoiceField",
  50    "MultipleChoiceField",
  51    "FloatField",
  52    "DecimalField",
  53    "JSONField",
  54    "TypedChoiceField",
  55    "UUIDField",
  56]
  57
  58
  59_FILE_INPUT_CONTRADICTION = object()
  60
  61
  62class Field:
  63    default_validators: list[Callable[[Any], None]] = []  # Default set of validators
  64    # Add an 'invalid' entry to default_error_message if you want a specific
  65    # field error message not raised by the field validators.
  66    default_error_messages = {
  67        "required": "This field is required.",
  68    }
  69    empty_values = list(validators_.EMPTY_VALUES)
  70
  71    def __init__(
  72        self,
  73        *,
  74        required: bool = True,
  75        initial: Any = None,
  76        error_messages: dict[str, str] | None = None,
  77        validators: Sequence[Callable[[Any], None]] = (),
  78    ):
  79        # required -- Boolean that specifies whether the field is required.
  80        #             True by default.
  81        # initial -- A value to use in this Field's initial display. This value
  82        #            is *not* used as a fallback if data isn't given.
  83        # error_messages -- An optional dictionary to override the default
  84        #                   messages that the field will raise.
  85        # validators -- List of additional validators to use
  86        self.required = required
  87        self.initial = initial
  88
  89        messages = {}
  90        for c in reversed(self.__class__.__mro__):
  91            messages.update(getattr(c, "default_error_messages", {}))
  92        messages.update(error_messages or {})
  93        self.error_messages = messages
  94
  95        self.validators = [*self.default_validators, *validators]
  96
  97    def prepare_value(self, value: Any) -> Any:
  98        return value
  99
 100    def to_python(self, value: Any) -> Any:
 101        return value
 102
 103    def validate(self, value: Any) -> None:
 104        if value in self.empty_values and self.required:
 105            raise ValidationError(self.error_messages["required"], code="required")
 106
 107    def run_validators(self, value: Any) -> None:
 108        if value in self.empty_values:
 109            return None
 110        errors = []
 111        for v in self.validators:
 112            try:
 113                v(value)
 114            except ValidationError as e:
 115                if hasattr(e, "code") and e.code in self.error_messages:
 116                    e.message = self.error_messages[e.code]
 117                errors.extend(e.error_list)
 118        if errors:
 119            raise ValidationError(errors)
 120
 121    def clean(self, value: Any) -> Any:
 122        """
 123        Validate the given value and return its "cleaned" value as an
 124        appropriate Python object. Raise ValidationError for any errors.
 125        """
 126        value = self.to_python(value)
 127        self.validate(value)
 128        self.run_validators(value)
 129        return value
 130
 131    def bound_data(self, data: Any, initial: Any) -> Any:
 132        """
 133        Return the value that should be shown for this field on render of a
 134        bound form, given the submitted POST data for the field and the initial
 135        data, if any.
 136
 137        For most fields, this will simply be data; FileFields need to handle it
 138        a bit differently.
 139        """
 140        return data
 141
 142    def has_changed(self, initial: Any, data: Any) -> bool:
 143        """Return True if data differs from initial."""
 144        try:
 145            data = self.to_python(data)
 146            if hasattr(self, "_coerce"):
 147                return self._coerce(data) != self._coerce(initial)  # ty: ignore[call-non-callable]
 148        except ValidationError:
 149            return True
 150        # For purposes of seeing whether something has changed, None is
 151        # the same as an empty string, if the data or initial value we get
 152        # is None, replace it with ''.
 153        initial_value = initial if initial is not None else ""
 154        data_value = data if data is not None else ""
 155        return initial_value != data_value
 156
 157    def get_bound_field(self, form: BaseForm, field_name: str) -> BoundField:
 158        """
 159        Return a BoundField instance that will be used when accessing the form
 160        field in a template.
 161        """
 162        return BoundField(form, self, field_name)
 163
 164    def __deepcopy__(self: Self, memo: dict[int, Any]) -> Self:
 165        result = copy.copy(self)
 166        memo[id(self)] = result
 167        result.error_messages = self.error_messages.copy()
 168        result.validators = self.validators[:]
 169        return result
 170
 171    def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
 172        # By default, all fields are expected to be present in HTML form data.
 173        try:
 174            return data[html_name]
 175        except KeyError as e:
 176            raise FormFieldMissingError(html_name) from e
 177
 178    def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
 179        if self.required and html_name not in data:
 180            raise FormFieldMissingError(html_name)
 181
 182        return data.get(html_name, None)
 183
 184
 185class TextField(Field):
 186    def __init__(
 187        self,
 188        *,
 189        max_length: int | None = None,
 190        min_length: int | None = None,
 191        strip: bool = True,
 192        empty_value: str = "",
 193        required: bool = True,
 194        initial: Any = None,
 195        error_messages: dict[str, str] | None = None,
 196        validators: Sequence[Callable[[Any], None]] = (),
 197    ):
 198        self.max_length = max_length
 199        self.min_length = min_length
 200        self.strip = strip
 201        self.empty_value = empty_value
 202        super().__init__(
 203            required=required,
 204            initial=initial,
 205            error_messages=error_messages,
 206            validators=validators,
 207        )
 208        if min_length is not None:
 209            self.validators.append(validators_.MinLengthValidator(int(min_length)))
 210        if max_length is not None:
 211            self.validators.append(validators_.MaxLengthValidator(int(max_length)))
 212        self.validators.append(validators_.ProhibitNullCharactersValidator())
 213
 214    def to_python(self, value: Any) -> str:
 215        """Return a string."""
 216        if value not in self.empty_values:
 217            value = str(value)
 218            if self.strip:
 219                value = value.strip()
 220        if value in self.empty_values:
 221            return self.empty_value
 222        return value
 223
 224
 225class NumericField(Field):
 226    """Base class for numeric fields with min/max/step validation."""
 227
 228    def __init__(
 229        self,
 230        *,
 231        max_value: int | float | Decimal | None = None,
 232        min_value: int | float | Decimal | None = None,
 233        step_size: int | float | Decimal | None = None,
 234        required: bool = True,
 235        initial: Any = None,
 236        error_messages: dict[str, str] | None = None,
 237        validators: Sequence[Callable[[Any], None]] = (),
 238    ):
 239        self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
 240        super().__init__(
 241            required=required,
 242            initial=initial,
 243            error_messages=error_messages,
 244            validators=validators,
 245        )
 246
 247        if max_value is not None:
 248            self.validators.append(validators_.MaxValueValidator(max_value))
 249        if min_value is not None:
 250            self.validators.append(validators_.MinValueValidator(min_value))
 251        if step_size is not None:
 252            self.validators.append(validators_.StepValueValidator(step_size))
 253
 254
 255class IntegerField(NumericField):
 256    default_error_messages = {
 257        "invalid": "Enter a whole number.",
 258    }
 259    re_decimal = _lazy_re_compile(r"\.0*\s*$")
 260
 261    def to_python(self, value: Any) -> int | None:
 262        """
 263        Validate that int() can be called on the input. Return the result
 264        of int() or None for empty values.
 265        """
 266        value = super().to_python(value)
 267        if value in self.empty_values:
 268            return None
 269        # Strip trailing decimal and zeros.
 270        try:
 271            value = int(self.re_decimal.sub("", str(value)))
 272        except (ValueError, TypeError):
 273            raise ValidationError(self.error_messages["invalid"], code="invalid")
 274        return value
 275
 276
 277class FloatField(NumericField):
 278    default_error_messages = {
 279        "invalid": "Enter a number.",
 280    }
 281
 282    def to_python(self, value: Any) -> float | None:
 283        """
 284        Validate that float() can be called on the input. Return the result
 285        of float() or None for empty values.
 286        """
 287        value = super().to_python(value)
 288        if value in self.empty_values:
 289            return None
 290        try:
 291            value = float(value)
 292        except (ValueError, TypeError):
 293            raise ValidationError(self.error_messages["invalid"], code="invalid")
 294        return value
 295
 296    def validate(self, value: Any) -> None:
 297        super().validate(value)
 298        if value in self.empty_values:
 299            return None
 300        if not math.isfinite(value):
 301            raise ValidationError(self.error_messages["invalid"], code="invalid")
 302
 303
 304class DecimalField(NumericField):
 305    default_error_messages = {
 306        "invalid": "Enter a number.",
 307    }
 308
 309    def __init__(
 310        self,
 311        *,
 312        max_value: Decimal | int | None = None,
 313        min_value: Decimal | int | None = None,
 314        max_digits: int | None = None,
 315        decimal_places: int | None = None,
 316        required: bool = True,
 317        initial: Any = None,
 318        error_messages: dict[str, str] | None = None,
 319        validators: Sequence[Callable[[Any], None]] = (),
 320    ):
 321        self.max_digits, self.decimal_places = max_digits, decimal_places
 322        super().__init__(
 323            max_value=max_value,
 324            min_value=min_value,
 325            required=required,
 326            initial=initial,
 327            error_messages=error_messages,
 328            validators=validators,
 329        )
 330        self.validators.append(validators_.DecimalValidator(max_digits, decimal_places))
 331
 332    def to_python(self, value: Any) -> Decimal | None:
 333        """
 334        Validate that the input is a decimal number. Return a Decimal
 335        instance or None for empty values. Ensure that there are no more
 336        than max_digits in the number and no more than decimal_places digits
 337        after the decimal point.
 338        """
 339        if value in self.empty_values:
 340            return None
 341        try:
 342            value = Decimal(str(value))
 343        except DecimalException:
 344            raise ValidationError(self.error_messages["invalid"], code="invalid")
 345        return value
 346
 347    def validate(self, value: Any) -> None:
 348        super().validate(value)
 349        if value in self.empty_values:
 350            return None
 351        if not value.is_finite():
 352            raise ValidationError(
 353                self.error_messages["invalid"],
 354                code="invalid",
 355                params={"value": value},
 356            )
 357
 358
 359class BaseTemporalField(Field):
 360    # Default formats to be used when parsing dates from input boxes, in order
 361    # See all available format string here:
 362    # https://docs.python.org/library/datetime.html#strftime-behavior
 363    # * Note that these format strings are different from the ones to display dates
 364    DATE_INPUT_FORMATS = [
 365        "%Y-%m-%d",  # '2006-10-25'
 366        "%m/%d/%Y",  # '10/25/2006'
 367        "%m/%d/%y",  # '10/25/06'
 368        "%b %d %Y",  # 'Oct 25 2006'
 369        "%b %d, %Y",  # 'Oct 25, 2006'
 370        "%d %b %Y",  # '25 Oct 2006'
 371        "%d %b, %Y",  # '25 Oct, 2006'
 372        "%B %d %Y",  # 'October 25 2006'
 373        "%B %d, %Y",  # 'October 25, 2006'
 374        "%d %B %Y",  # '25 October 2006'
 375        "%d %B, %Y",  # '25 October, 2006'
 376    ]
 377
 378    # Default formats to be used when parsing times from input boxes, in order
 379    # See all available format string here:
 380    # https://docs.python.org/library/datetime.html#strftime-behavior
 381    # * Note that these format strings are different from the ones to display dates
 382    TIME_INPUT_FORMATS = [
 383        "%H:%M:%S",  # '14:30:59'
 384        "%H:%M:%S.%f",  # '14:30:59.000200'
 385        "%H:%M",  # '14:30'
 386    ]
 387
 388    # Default formats to be used when parsing dates and times from input boxes,
 389    # in order
 390    # See all available format string here:
 391    # https://docs.python.org/library/datetime.html#strftime-behavior
 392    # * Note that these format strings are different from the ones to display dates
 393    DATETIME_INPUT_FORMATS = [
 394        "%Y-%m-%d %H:%M:%S",  # '2006-10-25 14:30:59'
 395        "%Y-%m-%d %H:%M:%S.%f",  # '2006-10-25 14:30:59.000200'
 396        "%Y-%m-%d %H:%M",  # '2006-10-25 14:30'
 397        "%m/%d/%Y %H:%M:%S",  # '10/25/2006 14:30:59'
 398        "%m/%d/%Y %H:%M:%S.%f",  # '10/25/2006 14:30:59.000200'
 399        "%m/%d/%Y %H:%M",  # '10/25/2006 14:30'
 400        "%m/%d/%y %H:%M:%S",  # '10/25/06 14:30:59'
 401        "%m/%d/%y %H:%M:%S.%f",  # '10/25/06 14:30:59.000200'
 402        "%m/%d/%y %H:%M",  # '10/25/06 14:30'
 403    ]
 404
 405    def __init__(
 406        self,
 407        *,
 408        input_formats: list[str] | None = None,
 409        required: bool = True,
 410        initial: Any = None,
 411        error_messages: dict[str, str] | None = None,
 412        validators: Sequence[Callable[[Any], None]] = (),
 413    ):
 414        super().__init__(
 415            required=required,
 416            initial=initial,
 417            error_messages=error_messages,
 418            validators=validators,
 419        )
 420        if input_formats is not None:
 421            self.input_formats = input_formats
 422
 423    def to_python(self, value: Any) -> Any:
 424        value = value.strip()
 425        # Try to strptime against each input format.
 426        for format in self.input_formats:
 427            try:
 428                return self.strptime(value, format)
 429            except (ValueError, TypeError):
 430                continue
 431        raise ValidationError(self.error_messages["invalid"], code="invalid")
 432
 433    def strptime(self, value: str, format: str) -> Any:
 434        raise NotImplementedError("Subclasses must define this method.")
 435
 436
 437class DateField(BaseTemporalField):
 438    input_formats = BaseTemporalField.DATE_INPUT_FORMATS
 439    default_error_messages = {
 440        "invalid": "Enter a valid date.",
 441    }
 442
 443    def to_python(self, value: Any) -> datetime.date | None:
 444        """
 445        Validate that the input can be converted to a date. Return a Python
 446        datetime.date object.
 447        """
 448        if value in self.empty_values:
 449            return None
 450        if isinstance(value, datetime.datetime):
 451            return value.date()
 452        if isinstance(value, datetime.date):
 453            return value
 454        return super().to_python(value)
 455
 456    def strptime(self, value: str, format: str) -> datetime.date:
 457        return datetime.datetime.strptime(value, format).date()
 458
 459
 460class TimeField(BaseTemporalField):
 461    input_formats = BaseTemporalField.TIME_INPUT_FORMATS
 462    default_error_messages = {"invalid": "Enter a valid time."}
 463
 464    def to_python(self, value: Any) -> datetime.time | None:
 465        """
 466        Validate that the input can be converted to a time. Return a Python
 467        datetime.time object.
 468        """
 469        if value in self.empty_values:
 470            return None
 471        if isinstance(value, datetime.time):
 472            return value
 473        return super().to_python(value)
 474
 475    def strptime(self, value: str, format: str) -> datetime.time:
 476        return datetime.datetime.strptime(value, format).time()
 477
 478
 479class DateTimeFormatsIterator:
 480    def __iter__(self) -> Any:
 481        yield from BaseTemporalField.DATETIME_INPUT_FORMATS
 482        yield from BaseTemporalField.DATE_INPUT_FORMATS
 483
 484
 485class DateTimeField(BaseTemporalField):
 486    input_formats = DateTimeFormatsIterator()
 487    default_error_messages = {
 488        "invalid": "Enter a valid date/time.",
 489    }
 490
 491    def prepare_value(self, value: Any) -> Any:
 492        if isinstance(value, datetime.datetime):
 493            value = to_current_timezone(value)
 494        return value
 495
 496    def to_python(self, value: Any) -> datetime.datetime | None:
 497        """
 498        Validate that the input can be converted to a datetime. Return a
 499        Python datetime.datetime object.
 500        """
 501        if value in self.empty_values:
 502            return None
 503        if isinstance(value, datetime.datetime):
 504            return from_current_timezone(value)
 505        if isinstance(value, datetime.date):
 506            result = datetime.datetime(value.year, value.month, value.day)
 507            return from_current_timezone(result)
 508        try:
 509            result = parse_datetime(value.strip())
 510        except ValueError:
 511            raise ValidationError(self.error_messages["invalid"], code="invalid")
 512        if not result:
 513            result = super().to_python(value)
 514        return from_current_timezone(result)
 515
 516    def strptime(self, value: str, format: str) -> datetime.datetime:
 517        return datetime.datetime.strptime(value, format)
 518
 519
 520class DurationField(Field):
 521    default_error_messages = {
 522        "invalid": "Enter a valid duration.",
 523        "overflow": "The number of days must be between {min_days} and {max_days}.",
 524    }
 525
 526    def prepare_value(self, value: Any) -> Any:
 527        if isinstance(value, datetime.timedelta):
 528            return duration_string(value)
 529        return value
 530
 531    def to_python(self, value: Any) -> datetime.timedelta | None:
 532        if value in self.empty_values:
 533            return None
 534        if isinstance(value, datetime.timedelta):
 535            return value
 536        try:
 537            value = parse_duration(str(value))
 538        except OverflowError:
 539            raise ValidationError(
 540                self.error_messages["overflow"].format(
 541                    min_days=datetime.timedelta.min.days,
 542                    max_days=datetime.timedelta.max.days,
 543                ),
 544                code="overflow",
 545            )
 546        if value is None:
 547            raise ValidationError(self.error_messages["invalid"], code="invalid")
 548        return value
 549
 550
 551class RegexField(TextField):
 552    def __init__(
 553        self,
 554        regex: str | re.Pattern[str],
 555        *,
 556        max_length: int | None = None,
 557        min_length: int | None = None,
 558        strip: bool = False,
 559        empty_value: str = "",
 560        required: bool = True,
 561        initial: Any = None,
 562        error_messages: dict[str, str] | None = None,
 563        validators: Sequence[Callable[[Any], None]] = (),
 564    ) -> None:
 565        """
 566        regex can be either a string or a compiled regular expression object.
 567        """
 568        super().__init__(
 569            max_length=max_length,
 570            min_length=min_length,
 571            strip=strip,
 572            empty_value=empty_value,
 573            required=required,
 574            initial=initial,
 575            error_messages=error_messages,
 576            validators=validators,
 577        )
 578        self._set_regex(regex)
 579
 580    def _get_regex(self) -> re.Pattern[str]:
 581        return self._regex
 582
 583    def _set_regex(self, regex: str | re.Pattern[str]) -> None:
 584        if isinstance(regex, str):
 585            regex = re.compile(regex)
 586        self._regex = regex
 587        if (
 588            hasattr(self, "_regex_validator")
 589            and self._regex_validator in self.validators
 590        ):
 591            self.validators.remove(self._regex_validator)
 592        self._regex_validator = validators_.RegexValidator(regex=regex)
 593        self.validators.append(self._regex_validator)
 594
 595    regex = property(_get_regex, _set_regex)
 596
 597
 598class EmailField(TextField):
 599    default_validators = [validators_.validate_email]
 600
 601    def __init__(
 602        self,
 603        *,
 604        max_length: int | None = None,
 605        min_length: int | None = None,
 606        strip: bool = True,
 607        empty_value: str = "",
 608        required: bool = True,
 609        initial: Any = None,
 610        error_messages: dict[str, str] | None = None,
 611        validators: Sequence[Callable[[Any], None]] = (),
 612    ) -> None:
 613        super().__init__(
 614            max_length=max_length,
 615            min_length=min_length,
 616            strip=strip,
 617            empty_value=empty_value,
 618            required=required,
 619            initial=initial,
 620            error_messages=error_messages,
 621            validators=validators,
 622        )
 623
 624
 625class FileField(Field):
 626    default_error_messages = {
 627        "invalid": "No file was submitted. Check the encoding type on the form.",
 628        "missing": "No file was submitted.",
 629        "empty": "The submitted file is empty.",
 630        "text": pluralize_lazy(
 631            "Ensure this filename has at most %(max)d character (it has %(length)d).",
 632            "Ensure this filename has at most %(max)d characters (it has %(length)d).",
 633            "max",
 634        ),
 635        "contradiction": "Please either submit a file or check the clear checkbox, not both.",
 636    }
 637
 638    def __init__(
 639        self,
 640        *,
 641        max_length: int | None = None,
 642        allow_empty_file: bool = False,
 643        required: bool = True,
 644        initial: Any = None,
 645        error_messages: dict[str, str] | None = None,
 646        validators: Sequence[Callable[[Any], None]] = (),
 647    ) -> None:
 648        self.max_length = max_length
 649        self.allow_empty_file = allow_empty_file
 650        super().__init__(
 651            required=required,
 652            initial=initial,
 653            error_messages=error_messages,
 654            validators=validators,
 655        )
 656
 657    def to_python(self, value: Any) -> Any:
 658        if value in self.empty_values:
 659            return None
 660
 661        # UploadedFile objects should have name and size attributes.
 662        try:
 663            file_name = value.name
 664            file_size = value.size
 665        except AttributeError:
 666            raise ValidationError(self.error_messages["invalid"], code="invalid")
 667
 668        if self.max_length is not None and len(file_name) > self.max_length:
 669            params = {"max": self.max_length, "length": len(file_name)}
 670            raise ValidationError(
 671                self.error_messages["max_length"], code="max_length", params=params
 672            )
 673        if not file_name:
 674            raise ValidationError(self.error_messages["invalid"], code="invalid")
 675        if not self.allow_empty_file and not file_size:
 676            raise ValidationError(self.error_messages["empty"], code="empty")
 677
 678        return value
 679
 680    def clean(self, data: Any, initial: Any = None) -> Any:  # ty: ignore[invalid-method-override]
 681        # If the widget got contradictory inputs, we raise a validation error
 682        if data is _FILE_INPUT_CONTRADICTION:
 683            raise ValidationError(
 684                self.error_messages["contradiction"], code="contradiction"
 685            )
 686        # False means the field value should be cleared; further validation is
 687        # not needed.
 688        if data is False:
 689            if not self.required:
 690                return False
 691            # If the field is required, clearing is not possible (the widget
 692            # shouldn't return False data in that case anyway). False is not
 693            # in self.empty_value; if a False value makes it this far
 694            # it should be validated from here on out as None (so it will be
 695            # caught by the required check).
 696            data = None
 697        if not data and initial:
 698            return initial
 699        return super().clean(data)
 700
 701    def bound_data(self, data: Any, initial: Any) -> Any:
 702        return initial
 703
 704    def has_changed(self, initial: Any, data: Any) -> bool:
 705        return data is not None
 706
 707    def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
 708        return files.get(html_name)
 709
 710    def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
 711        return files.get(html_name)
 712
 713
 714class ImageField(FileField):
 715    default_validators = [validators_.validate_image_file_extension]
 716    default_error_messages = {
 717        "invalid_image": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
 718    }
 719
 720    def to_python(self, value: Any) -> Any:
 721        """
 722        Check that the file-upload field data contains a valid image (GIF, JPG,
 723        PNG, etc. -- whatever Pillow supports).
 724        """
 725        f = super().to_python(value)
 726        if f is None:
 727            return None
 728
 729        from PIL import Image  # ty: ignore[unresolved-import]
 730
 731        # We need to get a file object for Pillow. We might have a path or we might
 732        # have to read the data into memory.
 733        if hasattr(value, "temporary_file_path"):
 734            file = value.temporary_file_path()
 735        else:
 736            if hasattr(value, "read"):
 737                file = BytesIO(value.read())
 738            else:
 739                file = BytesIO(value["content"])
 740
 741        try:
 742            # load() could spot a truncated JPEG, but it loads the entire
 743            # image in memory, which is a DoS vector. See #3848 and #18520.
 744            image = Image.open(file)
 745            # verify() must be called immediately after the constructor.
 746            image.verify()
 747
 748            # Annotating so subclasses can reuse it for their own validation
 749            f.image = image
 750            # Pillow doesn't detect the MIME type of all formats. In those
 751            # cases, content_type will be None.
 752            f.content_type = Image.MIME.get(image.format)
 753        except Exception as exc:
 754            # Pillow doesn't recognize it as an image.
 755            raise ValidationError(
 756                self.error_messages["invalid_image"],
 757                code="invalid_image",
 758            ) from exc
 759        if hasattr(f, "seek") and callable(f.seek):
 760            f.seek(0)
 761        return f
 762
 763
 764class URLField(TextField):
 765    default_error_messages = {
 766        "invalid": "Enter a valid URL.",
 767    }
 768    default_validators = [validators_.URLValidator()]
 769
 770    def __init__(
 771        self,
 772        *,
 773        max_length: int | None = None,
 774        min_length: int | None = None,
 775        strip: bool = True,
 776        empty_value: str = "",
 777        required: bool = True,
 778        initial: Any = None,
 779        error_messages: dict[str, str] | None = None,
 780        validators: Sequence[Callable[[Any], None]] = (),
 781    ) -> None:
 782        super().__init__(
 783            max_length=max_length,
 784            min_length=min_length,
 785            strip=strip,
 786            empty_value=empty_value,
 787            required=required,
 788            initial=initial,
 789            error_messages=error_messages,
 790            validators=validators,
 791        )
 792
 793    def to_python(self, value: Any) -> str:
 794        def split_url(url: str | bytes) -> list[str]:
 795            """
 796            Return a list of url parts via urlparse.urlsplit(), or raise
 797            ValidationError for some malformed URLs.
 798            """
 799            try:
 800                # Ensure url is a string for consistent typing
 801                if isinstance(url, bytes):
 802                    url = url.decode("utf-8")
 803                return list(urlsplit(url))
 804            except ValueError:
 805                # urlparse.urlsplit can raise a ValueError with some
 806                # misformatted URLs.
 807                raise ValidationError(self.error_messages["invalid"], code="invalid")
 808
 809        value = super().to_python(value)
 810        if value:
 811            url_fields = split_url(value)
 812            if not url_fields[0]:
 813                # If no URL scheme given, assume http://
 814                url_fields[0] = "http"
 815            if not url_fields[1]:
 816                # Assume that if no domain is provided, that the path segment
 817                # contains the domain.
 818                url_fields[1] = url_fields[2]
 819                url_fields[2] = ""
 820                # Rebuild the url_fields list, since the domain segment may now
 821                # contain the path too.
 822                url_result = urlunsplit(url_fields)
 823                url_fields = split_url(
 824                    str(url_result) if isinstance(url_result, bytes) else url_result
 825                )
 826            value = str(urlunsplit(url_fields))
 827        return value
 828
 829
 830class BooleanField(Field):
 831    def to_python(self, value: Any) -> bool:
 832        """Return a Python boolean object."""
 833        # Explicitly check for the string 'False', which is what a hidden field
 834        # will submit for False. Also check for '0', since this is what
 835        # RadioSelect will provide. Because bool("True") == bool('1') == True,
 836        # we don't need to handle that explicitly.
 837        if isinstance(value, str) and value.lower() in ("false", "0"):
 838            value = False
 839        else:
 840            value = bool(value)
 841        return super().to_python(value)
 842
 843    def validate(self, value: Any) -> None:
 844        if not value and self.required:
 845            raise ValidationError(self.error_messages["required"], code="required")
 846
 847    def has_changed(self, initial: Any, data: Any) -> bool:
 848        # Sometimes data or initial may be a string equivalent of a boolean
 849        # so we should run it through to_python first to get a boolean value
 850        return self.to_python(initial) != self.to_python(data)
 851
 852    def value_from_form_data(
 853        self, data: Any, files: Any, html_name: str
 854    ) -> bool | None:
 855        if html_name not in data:
 856            # Unselected checkboxes aren't in HTML form data, so return False
 857            return False
 858
 859        value = data.get(html_name)
 860        # Translate true and false strings to boolean values.
 861        return {
 862            True: True,
 863            "True": True,
 864            "False": False,
 865            False: False,
 866            "true": True,
 867            "false": False,
 868            "on": True,
 869        }.get(value)
 870
 871    def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
 872        # Boolean fields must be present in the JSON data
 873        try:
 874            return data[html_name]
 875        except KeyError as e:
 876            raise FormFieldMissingError(html_name) from e
 877
 878
 879class NullBooleanField(BooleanField):
 880    """
 881    A field whose valid values are None, True, and False. Clean invalid values
 882    to None.
 883    """
 884
 885    def to_python(self, value: Any) -> bool | None:  # ty: ignore[invalid-method-override]
 886        """
 887        Explicitly check for the string 'True' and 'False', which is what a
 888        hidden field will submit for True and False, for 'true' and 'false',
 889        which are likely to be returned by JavaScript serializations of forms,
 890        and for '1' and '0', which is what a RadioField will submit. Unlike
 891        the Booleanfield, this field must check for True because it doesn't
 892        use the bool() function.
 893        """
 894        if value in (True, "True", "true", "1"):
 895            return True
 896        elif value in (False, "False", "false", "0"):
 897            return False
 898        else:
 899            return None
 900
 901    def validate(self, value: Any) -> None:
 902        pass
 903
 904
 905class CallableChoiceIterator:
 906    def __init__(self, choices_func: Callable[[], Any]) -> None:
 907        self.choices_func = choices_func
 908
 909    def __iter__(self) -> Iterator[Any]:
 910        yield from self.choices_func()
 911
 912
 913class ChoiceField(Field):
 914    default_error_messages = {
 915        "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
 916    }
 917
 918    _choices: CallableChoiceIterator | list[Any]  # Set by choices property setter
 919
 920    def __init__(
 921        self,
 922        *,
 923        choices: Any = (),
 924        required: bool = True,
 925        initial: Any = None,
 926        error_messages: dict[str, str] | None = None,
 927        validators: Sequence[Callable[[Any], None]] = (),
 928    ) -> None:
 929        super().__init__(
 930            required=required,
 931            initial=initial,
 932            error_messages=error_messages,
 933            validators=validators,
 934        )
 935        if hasattr(choices, "choices"):
 936            choices = choices.choices
 937        elif isinstance(choices, enum.EnumMeta):
 938            choices = [(member.value, member.name) for member in choices]
 939        self.choices = choices
 940
 941    def __deepcopy__(self, memo: dict[int, Any]) -> ChoiceField:
 942        result = super().__deepcopy__(memo)
 943        result._choices = copy.deepcopy(self._choices, memo)
 944        return result
 945
 946    def _get_choices(self) -> Iterable[Any]:
 947        return self._choices
 948
 949    def _set_choices(self, value: Any) -> None:
 950        # Setting choices also sets the choices on the widget.
 951        # choices can be any iterable, but we call list() on it because
 952        # it will be consumed more than once.
 953        if callable(value):
 954            value = CallableChoiceIterator(value)
 955        else:
 956            value = list(value)
 957
 958        self._choices = value
 959
 960    choices = property(_get_choices, _set_choices)
 961
 962    def to_python(self, value: Any) -> str:
 963        """Return a string."""
 964        if value in self.empty_values:
 965            return ""
 966        return str(value)
 967
 968    def validate(self, value: Any) -> None:
 969        """Validate that the input is in self.choices."""
 970        super().validate(value)
 971        if value and not self.valid_value(value):
 972            raise ValidationError(
 973                self.error_messages["invalid_choice"],
 974                code="invalid_choice",
 975                params={"value": value},
 976            )
 977
 978    def valid_value(self, value: Any) -> bool:
 979        """Check to see if the provided value is a valid choice."""
 980        text_value = str(value)
 981        for k, v in self.choices:
 982            if isinstance(v, list | tuple):
 983                # This is an optgroup, so look inside the group for options
 984                for k2, _ in v:
 985                    if value == k2 or text_value == str(k2):
 986                        return True
 987            else:
 988                if value == k or text_value == str(k):
 989                    return True
 990        return False
 991
 992
 993class TypedChoiceField(ChoiceField):
 994    def __init__(
 995        self,
 996        *,
 997        coerce: Callable[[Any], Any] = lambda val: val,
 998        empty_value: Any = "",
 999        choices: Any = (),
1000        required: bool = True,
1001        initial: Any = None,
1002        error_messages: dict[str, str] | None = None,
1003        validators: Sequence[Callable[[Any], None]] = (),
1004    ) -> None:
1005        self.coerce = coerce
1006        self.empty_value = empty_value
1007        super().__init__(
1008            choices=choices,
1009            required=required,
1010            initial=initial,
1011            error_messages=error_messages,
1012            validators=validators,
1013        )
1014
1015    def _coerce(self, value: Any) -> Any:
1016        """
1017        Validate that the value can be coerced to the right type (if not empty).
1018        """
1019        if value == self.empty_value or value in self.empty_values:
1020            return self.empty_value
1021        try:
1022            value = self.coerce(value)
1023        except (ValueError, TypeError, ValidationError):
1024            raise ValidationError(
1025                self.error_messages["invalid_choice"],
1026                code="invalid_choice",
1027                params={"value": value},
1028            )
1029        return value
1030
1031    def clean(self, value: Any) -> Any:
1032        value = super().clean(value)
1033        return self._coerce(value)
1034
1035
1036class MultipleChoiceField(ChoiceField):
1037    default_error_messages = {
1038        "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
1039        "invalid_list": "Enter a list of values.",
1040    }
1041
1042    def to_python(self, value: Any) -> list[str]:  # ty: ignore[invalid-method-override]
1043        if not value:
1044            return []
1045        elif not isinstance(value, list | tuple):
1046            raise ValidationError(
1047                self.error_messages["invalid_list"], code="invalid_list"
1048            )
1049        return [str(val) for val in value]
1050
1051    def validate(self, value: Any) -> None:
1052        """Validate that the input is a list or tuple."""
1053        if self.required and not value:
1054            raise ValidationError(self.error_messages["required"], code="required")
1055        # Validate that each value in the value list is in self.choices.
1056        for val in value:
1057            if not self.valid_value(val):
1058                raise ValidationError(
1059                    self.error_messages["invalid_choice"],
1060                    code="invalid_choice",
1061                    params={"value": val},
1062                )
1063
1064    def has_changed(self, initial: Any, data: Any) -> bool:
1065        if initial is None:
1066            initial = []
1067        if data is None:
1068            data = []
1069        if len(initial) != len(data):
1070            return True
1071        initial_set = {str(value) for value in initial}
1072        data_set = {str(value) for value in data}
1073        return data_set != initial_set
1074
1075    def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
1076        return data.getlist(html_name)
1077
1078
1079class UUIDField(TextField):
1080    default_error_messages = {
1081        "invalid": "Enter a valid UUID.",
1082    }
1083
1084    def prepare_value(self, value: Any) -> Any:
1085        if isinstance(value, uuid.UUID):
1086            return str(value)
1087        return value
1088
1089    def to_python(self, value: Any) -> uuid.UUID | None:  # ty: ignore[invalid-method-override]
1090        value = super().to_python(value)
1091        if value in self.empty_values:
1092            return None
1093        if not isinstance(value, uuid.UUID):
1094            try:
1095                value = uuid.UUID(value)
1096            except ValueError:
1097                raise ValidationError(self.error_messages["invalid"], code="invalid")
1098        return value
1099
1100
1101class InvalidJSONInput(str):
1102    pass
1103
1104
1105class JSONString(str):
1106    pass
1107
1108
1109class JSONField(TextField):
1110    default_error_messages = {
1111        "invalid": "Enter a valid JSON.",
1112    }
1113
1114    def __init__(
1115        self,
1116        encoder: Any = None,
1117        decoder: Any = None,
1118        indent: int | None = None,
1119        sort_keys: bool = False,
1120        *,
1121        max_length: int | None = None,
1122        min_length: int | None = None,
1123        strip: bool = True,
1124        empty_value: str = "",
1125        required: bool = True,
1126        initial: Any = None,
1127        error_messages: dict[str, str] | None = None,
1128        validators: Sequence[Callable[[Any], None]] = (),
1129    ) -> None:
1130        self.encoder = encoder
1131        self.decoder = decoder
1132        self.indent = indent
1133        self.sort_keys = sort_keys
1134        super().__init__(
1135            max_length=max_length,
1136            min_length=min_length,
1137            strip=strip,
1138            empty_value=empty_value,
1139            required=required,
1140            initial=initial,
1141            error_messages=error_messages,
1142            validators=validators,
1143        )
1144
1145    def to_python(self, value: Any) -> Any:
1146        if value in self.empty_values:
1147            return None
1148        elif isinstance(value, list | dict | int | float | JSONString):
1149            return value
1150        try:
1151            converted = json.loads(value, cls=self.decoder)
1152        except json.JSONDecodeError:
1153            raise ValidationError(
1154                self.error_messages["invalid"],
1155                code="invalid",
1156                params={"value": value},
1157            )
1158        if isinstance(converted, str):
1159            return JSONString(converted)
1160        else:
1161            return converted
1162
1163    def bound_data(self, data: Any, initial: Any) -> Any:
1164        if data is None:
1165            return None
1166        try:
1167            return json.loads(data, cls=self.decoder)
1168        except json.JSONDecodeError:
1169            return InvalidJSONInput(data)
1170
1171    def prepare_value(self, value: Any) -> Any:
1172        if isinstance(value, InvalidJSONInput):
1173            return value
1174        return json.dumps(
1175            value,
1176            indent=self.indent,
1177            sort_keys=self.sort_keys,
1178            ensure_ascii=False,
1179            cls=self.encoder,
1180        )
1181
1182    def has_changed(self, initial: Any, data: Any) -> bool:
1183        if super().has_changed(initial, data):
1184            return True
1185        # For purposes of seeing whether something has changed, True isn't the
1186        # same as 1 and the order of keys doesn't matter.
1187        return json.dumps(initial, sort_keys=True, cls=self.encoder) != json.dumps(
1188            self.to_python(data), sort_keys=True, cls=self.encoder
1189        )
1190
1191
1192def from_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
1193    """
1194    When time zone support is enabled, convert naive datetimes
1195    entered in the current time zone to aware datetimes.
1196    """
1197    if value is not None and timezone.is_naive(value):
1198        current_timezone = timezone.get_current_timezone()
1199        try:
1200            if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
1201                raise ValueError("Ambiguous or non-existent time.")
1202            return timezone.make_aware(value, current_timezone)
1203        except Exception as exc:
1204            raise ValidationError(
1205                (
1206                    "%(datetime)s couldn't be interpreted "
1207                    "in time zone %(current_timezone)s; it "
1208                    "may be ambiguous or it may not exist."
1209                ),
1210                code="ambiguous_timezone",
1211                params={"datetime": value, "current_timezone": current_timezone},
1212            ) from exc
1213    return value
1214
1215
1216def to_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
1217    """
1218    When time zone support is enabled, convert aware datetimes
1219    to naive datetimes in the current time zone for display.
1220    """
1221    if value is not None and timezone.is_aware(value):
1222        return timezone.make_naive(value)
1223    return value