codekingpro/portable-devtools
115k
1import ipaddress2import math3import re4import uuid5 6__all__ = (7 "DataRequired",8 "data_required",9 "Email",10 "email",11 "EqualTo",12 "equal_to",13 "IPAddress",14 "ip_address",15 "InputRequired",16 "input_required",17 "Length",18 "length",19 "NumberRange",20 "number_range",21 "Optional",22 "optional",23 "Regexp",24 "regexp",25 "URL",26 "url",27 "AnyOf",28 "any_of",29 "NoneOf",30 "none_of",31 "MacAddress",32 "mac_address",33 "UUID",34 "ValidationError",35 "StopValidation",36 "readonly",37 "ReadOnly",38 "disabled",39 "Disabled",40)41 42 43class ValidationError(ValueError):44 """45 Raised when a validator fails to validate its input.46 """47 48 def __init__(self, message="", *args, **kwargs):49 ValueError.__init__(self, message, *args, **kwargs)50 51 52class StopValidation(Exception):53 """54 Causes the validation chain to stop.55 56 If StopValidation is raised, no more validators in the validation chain are57 called. If raised with a message, the message will be added to the errors58 list.59 """60 61 def __init__(self, message="", *args, **kwargs):62 Exception.__init__(self, message, *args, **kwargs)63 64 65class EqualTo:66 """67 Compares the values of two fields.68 69 :param fieldname:70 The name of the other field to compare to.71 :param message:72 Error message to raise in case of a validation error. Can be73 interpolated with `%(other_label)s` and `%(other_name)s` to provide a74 more helpful error.75 """76 77 def __init__(self, fieldname, message=None):78 self.fieldname = fieldname79 self.message = message80 81 def __call__(self, form, field):82 try:83 other = form[self.fieldname]84 except KeyError as exc:85 raise ValidationError(86 field.gettext("Invalid field name '%s'.") % self.fieldname87 ) from exc88 if field.data == other.data:89 return90 91 d = {92 "other_label": hasattr(other, "label")93 and other.label.text94 or self.fieldname,95 "other_name": self.fieldname,96 }97 message = self.message98 if message is None:99 message = field.gettext("Field must be equal to %(other_name)s.")100 101 raise ValidationError(message % d)102 103 104class Length:105 """106 Validates the length of a string.107 108 :param min:109 The minimum required length of the string. If not provided, minimum110 length will not be checked.111 :param max:112 The maximum length of the string. If not provided, maximum length113 will not be checked.114 :param message:115 Error message to raise in case of a validation error. Can be116 interpolated using `%(min)d` and `%(max)d` if desired. Useful defaults117 are provided depending on the existence of min and max.118 119 When supported, sets the `minlength` and `maxlength` attributes on widgets.120 """121 122 def __init__(self, min=-1, max=-1, message=None):123 assert (124 min != -1 or max != -1125 ), "At least one of `min` or `max` must be specified."126 assert max == -1 or min <= max, "`min` cannot be more than `max`."127 self.min = min128 self.max = max129 self.message = message130 self.field_flags = {}131 if self.min != -1:132 self.field_flags["minlength"] = self.min133 if self.max != -1:134 self.field_flags["maxlength"] = self.max135 136 def __call__(self, form, field):137 length = field.data and len(field.data) or 0138 if length >= self.min and (self.max == -1 or length <= self.max):139 return140 141 if self.message is not None:142 message = self.message143 144 elif self.max == -1:145 message = field.ngettext(146 "Field must be at least %(min)d character long.",147 "Field must be at least %(min)d characters long.",148 self.min,149 )150 elif self.min == -1:151 message = field.ngettext(152 "Field cannot be longer than %(max)d character.",153 "Field cannot be longer than %(max)d characters.",154 self.max,155 )156 elif self.min == self.max:157 message = field.ngettext(158 "Field must be exactly %(max)d character long.",159 "Field must be exactly %(max)d characters long.",160 self.max,161 )162 else:163 message = field.gettext(164 "Field must be between %(min)d and %(max)d characters long."165 )166 167 raise ValidationError(message % dict(min=self.min, max=self.max, length=length))168 169 170class NumberRange:171 """172 Validates that a number is of a minimum and/or maximum value, inclusive.173 This will work with any comparable number type, such as floats and174 decimals, not just integers.175 176 :param min:177 The minimum required value of the number. If not provided, minimum178 value will not be checked.179 :param max:180 The maximum value of the number. If not provided, maximum value181 will not be checked.182 :param message:183 Error message to raise in case of a validation error. Can be184 interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults185 are provided depending on the existence of min and max.186 187 When supported, sets the `min` and `max` attributes on widgets.188 """189 190 def __init__(self, min=None, max=None, message=None):191 self.min = min192 self.max = max193 self.message = message194 self.field_flags = {}195 if self.min is not None:196 self.field_flags["min"] = self.min197 if self.max is not None:198 self.field_flags["max"] = self.max199 200 def __call__(self, form, field):201 data = field.data202 if (203 data is not None204 and not math.isnan(data)205 and (self.min is None or data >= self.min)206 and (self.max is None or data <= self.max)207 ):208 return209 210 if self.message is not None:211 message = self.message212 213 # we use %(min)s interpolation to support floats, None, and214 # Decimals without throwing a formatting exception.215 elif self.max is None:216 message = field.gettext("Number must be at least %(min)s.")217 218 elif self.min is None:219 message = field.gettext("Number must be at most %(max)s.")220 221 else:222 message = field.gettext("Number must be between %(min)s and %(max)s.")223 224 raise ValidationError(message % dict(min=self.min, max=self.max))225 226 227class Optional:228 """229 Allows empty input and stops the validation chain from continuing.230 231 If input is empty, also removes prior errors (such as processing errors)232 from the field.233 234 :param strip_whitespace:235 If True (the default) also stop the validation chain on input which236 consists of only whitespace.237 238 Sets the `optional` attribute on widgets.239 """240 241 def __init__(self, strip_whitespace=True):242 if strip_whitespace:243 self.string_check = lambda s: s.strip()244 else:245 self.string_check = lambda s: s246 247 self.field_flags = {"optional": True}248 249 def __call__(self, form, field):250 if (251 not field.raw_data252 or isinstance(field.raw_data[0], str)253 and not self.string_check(field.raw_data[0])254 ):255 field.errors[:] = []256 raise StopValidation()257 258 259class DataRequired:260 """261 Checks the field's data is 'truthy' otherwise stops the validation chain.262 263 This validator checks that the ``data`` attribute on the field is a 'true'264 value (effectively, it does ``if field.data``.) Furthermore, if the data265 is a string type, a string containing only whitespace characters is266 considered false.267 268 If the data is empty, also removes prior errors (such as processing errors)269 from the field.270 271 **NOTE** this validator used to be called `Required` but the way it behaved272 (requiring coerced data, not input data) meant it functioned in a way273 which was not symmetric to the `Optional` validator and furthermore caused274 confusion with certain fields which coerced data to 'falsey' values like275 ``0``, ``Decimal(0)``, ``time(0)`` etc. Unless a very specific reason276 exists, we recommend using the :class:`InputRequired` instead.277 278 :param message:279 Error message to raise in case of a validation error.280 281 Sets the `required` attribute on widgets.282 """283 284 def __init__(self, message=None):285 self.message = message286 self.field_flags = {"required": True}287 288 def __call__(self, form, field):289 if field.data and (not isinstance(field.data, str) or field.data.strip()):290 return291 292 if self.message is None:293 message = field.gettext("This field is required.")294 else:295 message = self.message296 297 field.errors[:] = []298 raise StopValidation(message)299 300 301class InputRequired:302 """303 Validates that input was provided for this field.304 305 Note there is a distinction between this and DataRequired in that306 InputRequired looks that form-input data was provided, and DataRequired307 looks at the post-coercion data. This means that this validator only checks308 whether non-empty data was sent, not whether non-empty data was coerced309 from that data. Initially populated data is not considered sent.310 311 Sets the `required` attribute on widgets.312 """313 314 def __init__(self, message=None):315 self.message = message316 self.field_flags = {"required": True}317 318 def __call__(self, form, field):319 if field.raw_data and field.raw_data[0]:320 return321 322 if self.message is None:323 message = field.gettext("This field is required.")324 else:325 message = self.message326 327 field.errors[:] = []328 raise StopValidation(message)329 330 331class Regexp:332 """333 Validates the field against a user provided regexp.334 335 :param regex:336 The regular expression string to use. Can also be a compiled regular337 expression pattern.338 :param flags:339 The regexp flags to use, for example re.IGNORECASE. Ignored if340 `regex` is not a string.341 :param message:342 Error message to raise in case of a validation error.343 """344 345 def __init__(self, regex, flags=0, message=None):346 if isinstance(regex, str):347 regex = re.compile(regex, flags)348 self.regex = regex349 self.message = message350 351 def __call__(self, form, field, message=None):352 match = self.regex.match(field.data or "")353 if match:354 return match355 356 if message is None:357 if self.message is None:358 message = field.gettext("Invalid input.")359 else:360 message = self.message361 362 raise ValidationError(message)363 364 365class Email:366 """367 Validates an email address. Requires email_validator package to be368 installed. For ex: pip install wtforms[email].369 370 :param message:371 Error message to raise in case of a validation error.372 :param granular_message:373 Use validation failed message from email_validator library374 (Default False).375 :param check_deliverability:376 Perform domain name resolution check (Default False).377 :param allow_smtputf8:378 Fail validation for addresses that would require SMTPUTF8379 (Default True).380 :param allow_empty_local:381 Allow an empty local part (i.e. @example.com), e.g. for validating382 Postfix aliases (Default False).383 """384 385 def __init__(386 self,387 message=None,388 granular_message=False,389 check_deliverability=False,390 allow_smtputf8=True,391 allow_empty_local=False,392 ):393 self.message = message394 self.granular_message = granular_message395 self.check_deliverability = check_deliverability396 self.allow_smtputf8 = allow_smtputf8397 self.allow_empty_local = allow_empty_local398 399 def __call__(self, form, field):400 try:401 import email_validator402 except ImportError as exc: # pragma: no cover403 raise Exception(404 "Install 'email_validator' for email validation support."405 ) from exc406 407 try:408 if field.data is None:409 raise email_validator.EmailNotValidError()410 email_validator.validate_email(411 field.data,412 check_deliverability=self.check_deliverability,413 allow_smtputf8=self.allow_smtputf8,414 allow_empty_local=self.allow_empty_local,415 )416 except email_validator.EmailNotValidError as e:417 message = self.message418 if message is None:419 if self.granular_message:420 message = field.gettext(e)421 else:422 message = field.gettext("Invalid email address.")423 raise ValidationError(message) from e424 425 426class IPAddress:427 """428 Validates an IP address.429 430 :param ipv4:431 If True, accept IPv4 addresses as valid (default True)432 :param ipv6:433 If True, accept IPv6 addresses as valid (default False)434 :param message:435 Error message to raise in case of a validation error.436 """437 438 def __init__(self, ipv4=True, ipv6=False, message=None):439 if not ipv4 and not ipv6:440 raise ValueError(441 "IP Address Validator must have at least one of ipv4 or ipv6 enabled."442 )443 self.ipv4 = ipv4444 self.ipv6 = ipv6445 self.message = message446 447 def __call__(self, form, field):448 value = field.data449 valid = False450 if value:451 valid = (self.ipv4 and self.check_ipv4(value)) or (452 self.ipv6 and self.check_ipv6(value)453 )454 455 if valid:456 return457 458 message = self.message459 if message is None:460 message = field.gettext("Invalid IP address.")461 raise ValidationError(message)462 463 @classmethod464 def check_ipv4(cls, value):465 try:466 address = ipaddress.ip_address(value)467 except ValueError:468 return False469 470 if not isinstance(address, ipaddress.IPv4Address):471 return False472 473 return True474 475 @classmethod476 def check_ipv6(cls, value):477 try:478 address = ipaddress.ip_address(value)479 except ValueError:480 return False481 482 if not isinstance(address, ipaddress.IPv6Address):483 return False484 485 return True486 487 488class MacAddress(Regexp):489 """490 Validates a MAC address.491 492 :param message:493 Error message to raise in case of a validation error.494 """495 496 def __init__(self, message=None):497 pattern = r"^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"498 super().__init__(pattern, message=message)499 500 def __call__(self, form, field):501 message = self.message502 if message is None:503 message = field.gettext("Invalid Mac address.")504 505 super().__call__(form, field, message)506 507 508class URL(Regexp):509 """510 Simple regexp based url validation. Much like the email validator, you511 probably want to validate the url later by other means if the url must512 resolve.513 514 :param require_tld:515 If true, then the domain-name portion of the URL must contain a .tld516 suffix. Set this to false if you want to allow domains like517 `localhost`.518 :param allow_ip:519 If false, then give ip as host will fail validation520 :param message:521 Error message to raise in case of a validation error.522 """523 524 def __init__(self, require_tld=True, allow_ip=True, message=None):525 regex = (526 r"^[a-z]+://"527 r"(?P<host>[^\/\?:]+)"528 r"(?P<port>:[0-9]+)?"529 r"(?P<path>\/.*?)?"530 r"(?P<query>\?.*)?$"531 )532 super().__init__(regex, re.IGNORECASE, message)533 self.validate_hostname = HostnameValidation(534 require_tld=require_tld, allow_ip=allow_ip535 )536 537 def __call__(self, form, field):538 message = self.message539 if message is None:540 message = field.gettext("Invalid URL.")541 542 match = super().__call__(form, field, message)543 if not self.validate_hostname(match.group("host")):544 raise ValidationError(message)545 546 547class UUID:548 """549 Validates a UUID.550 551 :param message:552 Error message to raise in case of a validation error.553 """554 555 def __init__(self, message=None):556 self.message = message557 558 def __call__(self, form, field):559 message = self.message560 if message is None:561 message = field.gettext("Invalid UUID.")562 try:563 uuid.UUID(field.data)564 except ValueError as exc:565 raise ValidationError(message) from exc566 567 568class AnyOf:569 """570 Compares the incoming data to a sequence of valid inputs.571 572 :param values:573 A sequence of valid inputs.574 :param message:575 Error message to raise in case of a validation error. `%(values)s`576 contains the list of values.577 :param values_formatter:578 Function used to format the list of values in the error message.579 """580 581 def __init__(self, values, message=None, values_formatter=None):582 self.values = values583 self.message = message584 if values_formatter is None:585 values_formatter = self.default_values_formatter586 self.values_formatter = values_formatter587 588 def __call__(self, form, field):589 if field.data in self.values:590 return591 592 message = self.message593 if message is None:594 message = field.gettext("Invalid value, must be one of: %(values)s.")595 596 raise ValidationError(message % dict(values=self.values_formatter(self.values)))597 598 @staticmethod599 def default_values_formatter(values):600 return ", ".join(str(x) for x in values)601 602 603class NoneOf:604 """605 Compares the incoming data to a sequence of invalid inputs.606 607 :param values:608 A sequence of invalid inputs.609 :param message:610 Error message to raise in case of a validation error. `%(values)s`611 contains the list of values.612 :param values_formatter:613 Function used to format the list of values in the error message.614 """615 616 def __init__(self, values, message=None, values_formatter=None):617 self.values = values618 self.message = message619 if values_formatter is None:620 values_formatter = self.default_values_formatter621 self.values_formatter = values_formatter622 623 def __call__(self, form, field):624 if field.data not in self.values:625 return626 627 message = self.message628 if message is None:629 message = field.gettext("Invalid value, can't be any of: %(values)s.")630 631 raise ValidationError(message % dict(values=self.values_formatter(self.values)))632 633 @staticmethod634 def default_values_formatter(v):635 return ", ".join(str(x) for x in v)636 637 638class HostnameValidation:639 """640 Helper class for checking hostnames for validation.641 642 This is not a validator in and of itself, and as such is not exported.643 """644 645 hostname_part = re.compile(r"^(xn-|[a-z0-9_]+)(-[a-z0-9_-]+)*$", re.IGNORECASE)646 tld_part = re.compile(r"^([a-z]{2,20}|xn--([a-z0-9]+-)*[a-z0-9]+)$", re.IGNORECASE)647 648 def __init__(self, require_tld=True, allow_ip=False):649 self.require_tld = require_tld650 self.allow_ip = allow_ip651 652 def __call__(self, hostname):653 if self.allow_ip and (654 IPAddress.check_ipv4(hostname) or IPAddress.check_ipv6(hostname)655 ):656 return True657 658 # Encode out IDNA hostnames. This makes further validation easier.659 try:660 hostname = hostname.encode("idna")661 except UnicodeError:662 pass663 664 # Turn back into a string in Python 3x665 if not isinstance(hostname, str):666 hostname = hostname.decode("ascii")667 668 if len(hostname) > 253:669 return False670 671 # Check that all labels in the hostname are valid672 parts = hostname.split(".")673 for part in parts:674 if not part or len(part) > 63:675 return False676 if not self.hostname_part.match(part):677 return False678 679 if self.require_tld and (len(parts) < 2 or not self.tld_part.match(parts[-1])):680 return False681 682 return True683 684 685class ReadOnly:686 """687 Set a field readonly.688 689 Validation fails if the form data is different than the690 field object data, or if unset, from the field default data.691 """692 693 def __init__(self):694 self.field_flags = {"readonly": True}695 696 def __call__(self, form, field):697 if field.data != field.object_data:698 raise ValidationError(field.gettext("This field cannot be edited"))699 700 701class Disabled:702 """703 Set a field disabled.704 705 Validation fails if the form data has any value.706 """707 708 def __init__(self):709 self.field_flags = {"disabled": True}710 711 def __call__(self, form, field):712 if field.raw_data is not None:713 raise ValidationError(714 field.gettext("This field is disabled and cannot have a value")715 )716 717 718email = Email719equal_to = EqualTo720ip_address = IPAddress721mac_address = MacAddress722length = Length723number_range = NumberRange724optional = Optional725input_required = InputRequired726data_required = DataRequired727regexp = Regexp728url = URL729any_of = AnyOf730none_of = NoneOf731readonly = ReadOnly732disabled = Disabled733 