Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/wtforms/validators.py: 45%

286 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:32 +0000

1import ipaddress 

2import math 

3import re 

4import uuid 

5 

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) 

37 

38 

39class ValidationError(ValueError): 

40 """ 

41 Raised when a validator fails to validate its input. 

42 """ 

43 

44 def __init__(self, message="", *args, **kwargs): 

45 ValueError.__init__(self, message, *args, **kwargs) 

46 

47 

48class StopValidation(Exception): 

49 """ 

50 Causes the validation chain to stop. 

51 

52 If StopValidation is raised, no more validators in the validation chain are 

53 called. If raised with a message, the message will be added to the errors 

54 list. 

55 """ 

56 

57 def __init__(self, message="", *args, **kwargs): 

58 Exception.__init__(self, message, *args, **kwargs) 

59 

60 

61class EqualTo: 

62 """ 

63 Compares the values of two fields. 

64 

65 :param fieldname: 

66 The name of the other field to compare to. 

67 :param message: 

68 Error message to raise in case of a validation error. Can be 

69 interpolated with `%(other_label)s` and `%(other_name)s` to provide a 

70 more helpful error. 

71 """ 

72 

73 def __init__(self, fieldname, message=None): 

74 self.fieldname = fieldname 

75 self.message = message 

76 

77 def __call__(self, form, field): 

78 try: 

79 other = form[self.fieldname] 

80 except KeyError as exc: 

81 raise ValidationError( 

82 field.gettext("Invalid field name '%s'.") % self.fieldname 

83 ) from exc 

84 if field.data == other.data: 

85 return 

86 

87 d = { 

88 "other_label": hasattr(other, "label") 

89 and other.label.text 

90 or self.fieldname, 

91 "other_name": self.fieldname, 

92 } 

93 message = self.message 

94 if message is None: 

95 message = field.gettext("Field must be equal to %(other_name)s.") 

96 

97 raise ValidationError(message % d) 

98 

99 

100class Length: 

101 """ 

102 Validates the length of a string. 

103 

104 :param min: 

105 The minimum required length of the string. If not provided, minimum 

106 length will not be checked. 

107 :param max: 

108 The maximum length of the string. If not provided, maximum length 

109 will not be checked. 

110 :param message: 

111 Error message to raise in case of a validation error. Can be 

112 interpolated using `%(min)d` and `%(max)d` if desired. Useful defaults 

113 are provided depending on the existence of min and max. 

114 

115 When supported, sets the `minlength` and `maxlength` attributes on widgets. 

116 """ 

117 

118 def __init__(self, min=-1, max=-1, message=None): 

119 assert ( 

120 min != -1 or max != -1 

121 ), "At least one of `min` or `max` must be specified." 

122 assert max == -1 or min <= max, "`min` cannot be more than `max`." 

123 self.min = min 

124 self.max = max 

125 self.message = message 

126 self.field_flags = {} 

127 if self.min != -1: 

128 self.field_flags["minlength"] = self.min 

129 if self.max != -1: 

130 self.field_flags["maxlength"] = self.max 

131 

132 def __call__(self, form, field): 

133 length = field.data and len(field.data) or 0 

134 if length >= self.min and (self.max == -1 or length <= self.max): 

135 return 

136 

137 if self.message is not None: 

138 message = self.message 

139 

140 elif self.max == -1: 

141 message = field.ngettext( 

142 "Field must be at least %(min)d character long.", 

143 "Field must be at least %(min)d characters long.", 

144 self.min, 

145 ) 

146 elif self.min == -1: 

147 message = field.ngettext( 

148 "Field cannot be longer than %(max)d character.", 

149 "Field cannot be longer than %(max)d characters.", 

150 self.max, 

151 ) 

152 elif self.min == self.max: 

153 message = field.ngettext( 

154 "Field must be exactly %(max)d character long.", 

155 "Field must be exactly %(max)d characters long.", 

156 self.max, 

157 ) 

158 else: 

159 message = field.gettext( 

160 "Field must be between %(min)d and %(max)d characters long." 

161 ) 

162 

163 raise ValidationError(message % dict(min=self.min, max=self.max, length=length)) 

164 

165 

166class NumberRange: 

167 """ 

168 Validates that a number is of a minimum and/or maximum value, inclusive. 

169 This will work with any comparable number type, such as floats and 

170 decimals, not just integers. 

171 

172 :param min: 

173 The minimum required value of the number. If not provided, minimum 

174 value will not be checked. 

175 :param max: 

176 The maximum value of the number. If not provided, maximum value 

177 will not be checked. 

178 :param message: 

179 Error message to raise in case of a validation error. Can be 

180 interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults 

181 are provided depending on the existence of min and max. 

182 

183 When supported, sets the `min` and `max` attributes on widgets. 

184 """ 

185 

186 def __init__(self, min=None, max=None, message=None): 

187 self.min = min 

188 self.max = max 

189 self.message = message 

190 self.field_flags = {} 

191 if self.min is not None: 

192 self.field_flags["min"] = self.min 

193 if self.max is not None: 

194 self.field_flags["max"] = self.max 

195 

196 def __call__(self, form, field): 

197 data = field.data 

198 if ( 

199 data is not None 

200 and not math.isnan(data) 

201 and (self.min is None or data >= self.min) 

202 and (self.max is None or data <= self.max) 

203 ): 

204 return 

205 

206 if self.message is not None: 

207 message = self.message 

208 

209 # we use %(min)s interpolation to support floats, None, and 

210 # Decimals without throwing a formatting exception. 

211 elif self.max is None: 

212 message = field.gettext("Number must be at least %(min)s.") 

213 

214 elif self.min is None: 

215 message = field.gettext("Number must be at most %(max)s.") 

216 

217 else: 

218 message = field.gettext("Number must be between %(min)s and %(max)s.") 

219 

220 raise ValidationError(message % dict(min=self.min, max=self.max)) 

221 

222 

223class Optional: 

224 """ 

225 Allows empty input and stops the validation chain from continuing. 

226 

227 If input is empty, also removes prior errors (such as processing errors) 

228 from the field. 

229 

230 :param strip_whitespace: 

231 If True (the default) also stop the validation chain on input which 

232 consists of only whitespace. 

233 

234 Sets the `optional` attribute on widgets. 

235 """ 

236 

237 def __init__(self, strip_whitespace=True): 

238 if strip_whitespace: 

239 self.string_check = lambda s: s.strip() 

240 else: 

241 self.string_check = lambda s: s 

242 

243 self.field_flags = {"optional": True} 

244 

245 def __call__(self, form, field): 

246 if ( 

247 not field.raw_data 

248 or isinstance(field.raw_data[0], str) 

249 and not self.string_check(field.raw_data[0]) 

250 ): 

251 field.errors[:] = [] 

252 raise StopValidation() 

253 

254 

255class DataRequired: 

256 """ 

257 Checks the field's data is 'truthy' otherwise stops the validation chain. 

258 

259 This validator checks that the ``data`` attribute on the field is a 'true' 

260 value (effectively, it does ``if field.data``.) Furthermore, if the data 

261 is a string type, a string containing only whitespace characters is 

262 considered false. 

263 

264 If the data is empty, also removes prior errors (such as processing errors) 

265 from the field. 

266 

267 **NOTE** this validator used to be called `Required` but the way it behaved 

268 (requiring coerced data, not input data) meant it functioned in a way 

269 which was not symmetric to the `Optional` validator and furthermore caused 

270 confusion with certain fields which coerced data to 'falsey' values like 

271 ``0``, ``Decimal(0)``, ``time(0)`` etc. Unless a very specific reason 

272 exists, we recommend using the :class:`InputRequired` instead. 

273 

274 :param message: 

275 Error message to raise in case of a validation error. 

276 

277 Sets the `required` attribute on widgets. 

278 """ 

279 

280 def __init__(self, message=None): 

281 self.message = message 

282 self.field_flags = {"required": True} 

283 

284 def __call__(self, form, field): 

285 if field.data and (not isinstance(field.data, str) or field.data.strip()): 

286 return 

287 

288 if self.message is None: 

289 message = field.gettext("This field is required.") 

290 else: 

291 message = self.message 

292 

293 field.errors[:] = [] 

294 raise StopValidation(message) 

295 

296 

297class InputRequired: 

298 """ 

299 Validates that input was provided for this field. 

300 

301 Note there is a distinction between this and DataRequired in that 

302 InputRequired looks that form-input data was provided, and DataRequired 

303 looks at the post-coercion data. This means that this validator only checks 

304 whether non-empty data was sent, not whether non-empty data was coerced 

305 from that data. Initially populated data is not considered sent. 

306 

307 Sets the `required` attribute on widgets. 

308 """ 

309 

310 def __init__(self, message=None): 

311 self.message = message 

312 self.field_flags = {"required": True} 

313 

314 def __call__(self, form, field): 

315 if field.raw_data and field.raw_data[0]: 

316 return 

317 

318 if self.message is None: 

319 message = field.gettext("This field is required.") 

320 else: 

321 message = self.message 

322 

323 field.errors[:] = [] 

324 raise StopValidation(message) 

325 

326 

327class Regexp: 

328 """ 

329 Validates the field against a user provided regexp. 

330 

331 :param regex: 

332 The regular expression string to use. Can also be a compiled regular 

333 expression pattern. 

334 :param flags: 

335 The regexp flags to use, for example re.IGNORECASE. Ignored if 

336 `regex` is not a string. 

337 :param message: 

338 Error message to raise in case of a validation error. 

339 """ 

340 

341 def __init__(self, regex, flags=0, message=None): 

342 if isinstance(regex, str): 

343 regex = re.compile(regex, flags) 

344 self.regex = regex 

345 self.message = message 

346 

347 def __call__(self, form, field, message=None): 

348 match = self.regex.match(field.data or "") 

349 if match: 

350 return match 

351 

352 if message is None: 

353 if self.message is None: 

354 message = field.gettext("Invalid input.") 

355 else: 

356 message = self.message 

357 

358 raise ValidationError(message) 

359 

360 

361class Email: 

362 """ 

363 Validates an email address. Requires email_validator package to be 

364 installed. For ex: pip install wtforms[email]. 

365 

366 :param message: 

367 Error message to raise in case of a validation error. 

368 :param granular_message: 

369 Use validation failed message from email_validator library 

370 (Default False). 

371 :param check_deliverability: 

372 Perform domain name resolution check (Default False). 

373 :param allow_smtputf8: 

374 Fail validation for addresses that would require SMTPUTF8 

375 (Default True). 

376 :param allow_empty_local: 

377 Allow an empty local part (i.e. @example.com), e.g. for validating 

378 Postfix aliases (Default False). 

379 """ 

380 

381 def __init__( 

382 self, 

383 message=None, 

384 granular_message=False, 

385 check_deliverability=False, 

386 allow_smtputf8=True, 

387 allow_empty_local=False, 

388 ): 

389 self.message = message 

390 self.granular_message = granular_message 

391 self.check_deliverability = check_deliverability 

392 self.allow_smtputf8 = allow_smtputf8 

393 self.allow_empty_local = allow_empty_local 

394 

395 def __call__(self, form, field): 

396 try: 

397 import email_validator 

398 except ImportError as exc: # pragma: no cover 

399 raise Exception( 

400 "Install 'email_validator' for email validation support." 

401 ) from exc 

402 

403 try: 

404 if field.data is None: 

405 raise email_validator.EmailNotValidError() 

406 email_validator.validate_email( 

407 field.data, 

408 check_deliverability=self.check_deliverability, 

409 allow_smtputf8=self.allow_smtputf8, 

410 allow_empty_local=self.allow_empty_local, 

411 ) 

412 except email_validator.EmailNotValidError as e: 

413 message = self.message 

414 if message is None: 

415 if self.granular_message: 

416 message = field.gettext(e) 

417 else: 

418 message = field.gettext("Invalid email address.") 

419 raise ValidationError(message) from e 

420 

421 

422class IPAddress: 

423 """ 

424 Validates an IP address. 

425 

426 :param ipv4: 

427 If True, accept IPv4 addresses as valid (default True) 

428 :param ipv6: 

429 If True, accept IPv6 addresses as valid (default False) 

430 :param message: 

431 Error message to raise in case of a validation error. 

432 """ 

433 

434 def __init__(self, ipv4=True, ipv6=False, message=None): 

435 if not ipv4 and not ipv6: 

436 raise ValueError( 

437 "IP Address Validator must have at least one of ipv4 or ipv6 enabled." 

438 ) 

439 self.ipv4 = ipv4 

440 self.ipv6 = ipv6 

441 self.message = message 

442 

443 def __call__(self, form, field): 

444 value = field.data 

445 valid = False 

446 if value: 

447 valid = (self.ipv4 and self.check_ipv4(value)) or ( 

448 self.ipv6 and self.check_ipv6(value) 

449 ) 

450 

451 if valid: 

452 return 

453 

454 message = self.message 

455 if message is None: 

456 message = field.gettext("Invalid IP address.") 

457 raise ValidationError(message) 

458 

459 @classmethod 

460 def check_ipv4(cls, value): 

461 try: 

462 address = ipaddress.ip_address(value) 

463 except ValueError: 

464 return False 

465 

466 if not isinstance(address, ipaddress.IPv4Address): 

467 return False 

468 

469 return True 

470 

471 @classmethod 

472 def check_ipv6(cls, value): 

473 try: 

474 address = ipaddress.ip_address(value) 

475 except ValueError: 

476 return False 

477 

478 if not isinstance(address, ipaddress.IPv6Address): 

479 return False 

480 

481 return True 

482 

483 

484class MacAddress(Regexp): 

485 """ 

486 Validates a MAC address. 

487 

488 :param message: 

489 Error message to raise in case of a validation error. 

490 """ 

491 

492 def __init__(self, message=None): 

493 pattern = r"^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$" 

494 super().__init__(pattern, message=message) 

495 

496 def __call__(self, form, field): 

497 message = self.message 

498 if message is None: 

499 message = field.gettext("Invalid Mac address.") 

500 

501 super().__call__(form, field, message) 

502 

503 

504class URL(Regexp): 

505 """ 

506 Simple regexp based url validation. Much like the email validator, you 

507 probably want to validate the url later by other means if the url must 

508 resolve. 

509 

510 :param require_tld: 

511 If true, then the domain-name portion of the URL must contain a .tld 

512 suffix. Set this to false if you want to allow domains like 

513 `localhost`. 

514 :param message: 

515 Error message to raise in case of a validation error. 

516 """ 

517 

518 def __init__(self, require_tld=True, message=None): 

519 regex = ( 

520 r"^[a-z]+://" 

521 r"(?P<host>[^\/\?:]+)" 

522 r"(?P<port>:[0-9]+)?" 

523 r"(?P<path>\/.*?)?" 

524 r"(?P<query>\?.*)?$" 

525 ) 

526 super().__init__(regex, re.IGNORECASE, message) 

527 self.validate_hostname = HostnameValidation( 

528 require_tld=require_tld, allow_ip=True 

529 ) 

530 

531 def __call__(self, form, field): 

532 message = self.message 

533 if message is None: 

534 message = field.gettext("Invalid URL.") 

535 

536 match = super().__call__(form, field, message) 

537 if not self.validate_hostname(match.group("host")): 

538 raise ValidationError(message) 

539 

540 

541class UUID: 

542 """ 

543 Validates a UUID. 

544 

545 :param message: 

546 Error message to raise in case of a validation error. 

547 """ 

548 

549 def __init__(self, message=None): 

550 self.message = message 

551 

552 def __call__(self, form, field): 

553 message = self.message 

554 if message is None: 

555 message = field.gettext("Invalid UUID.") 

556 try: 

557 uuid.UUID(field.data) 

558 except ValueError as exc: 

559 raise ValidationError(message) from exc 

560 

561 

562class AnyOf: 

563 """ 

564 Compares the incoming data to a sequence of valid inputs. 

565 

566 :param values: 

567 A sequence of valid inputs. 

568 :param message: 

569 Error message to raise in case of a validation error. `%(values)s` 

570 contains the list of values. 

571 :param values_formatter: 

572 Function used to format the list of values in the error message. 

573 """ 

574 

575 def __init__(self, values, message=None, values_formatter=None): 

576 self.values = values 

577 self.message = message 

578 if values_formatter is None: 

579 values_formatter = self.default_values_formatter 

580 self.values_formatter = values_formatter 

581 

582 def __call__(self, form, field): 

583 if field.data in self.values: 

584 return 

585 

586 message = self.message 

587 if message is None: 

588 message = field.gettext("Invalid value, must be one of: %(values)s.") 

589 

590 raise ValidationError(message % dict(values=self.values_formatter(self.values))) 

591 

592 @staticmethod 

593 def default_values_formatter(values): 

594 return ", ".join(str(x) for x in values) 

595 

596 

597class NoneOf: 

598 """ 

599 Compares the incoming data to a sequence of invalid inputs. 

600 

601 :param values: 

602 A sequence of invalid inputs. 

603 :param message: 

604 Error message to raise in case of a validation error. `%(values)s` 

605 contains the list of values. 

606 :param values_formatter: 

607 Function used to format the list of values in the error message. 

608 """ 

609 

610 def __init__(self, values, message=None, values_formatter=None): 

611 self.values = values 

612 self.message = message 

613 if values_formatter is None: 

614 values_formatter = self.default_values_formatter 

615 self.values_formatter = values_formatter 

616 

617 def __call__(self, form, field): 

618 if field.data not in self.values: 

619 return 

620 

621 message = self.message 

622 if message is None: 

623 message = field.gettext("Invalid value, can't be any of: %(values)s.") 

624 

625 raise ValidationError(message % dict(values=self.values_formatter(self.values))) 

626 

627 @staticmethod 

628 def default_values_formatter(v): 

629 return ", ".join(str(x) for x in v) 

630 

631 

632class HostnameValidation: 

633 """ 

634 Helper class for checking hostnames for validation. 

635 

636 This is not a validator in and of itself, and as such is not exported. 

637 """ 

638 

639 hostname_part = re.compile(r"^(xn-|[a-z0-9_]+)(-[a-z0-9_-]+)*$", re.IGNORECASE) 

640 tld_part = re.compile(r"^([a-z]{2,20}|xn--([a-z0-9]+-)*[a-z0-9]+)$", re.IGNORECASE) 

641 

642 def __init__(self, require_tld=True, allow_ip=False): 

643 self.require_tld = require_tld 

644 self.allow_ip = allow_ip 

645 

646 def __call__(self, hostname): 

647 if self.allow_ip and ( 

648 IPAddress.check_ipv4(hostname) or IPAddress.check_ipv6(hostname) 

649 ): 

650 return True 

651 

652 # Encode out IDNA hostnames. This makes further validation easier. 

653 try: 

654 hostname = hostname.encode("idna") 

655 except UnicodeError: 

656 pass 

657 

658 # Turn back into a string in Python 3x 

659 if not isinstance(hostname, str): 

660 hostname = hostname.decode("ascii") 

661 

662 if len(hostname) > 253: 

663 return False 

664 

665 # Check that all labels in the hostname are valid 

666 parts = hostname.split(".") 

667 for part in parts: 

668 if not part or len(part) > 63: 

669 return False 

670 if not self.hostname_part.match(part): 

671 return False 

672 

673 if self.require_tld and (len(parts) < 2 or not self.tld_part.match(parts[-1])): 

674 return False 

675 

676 return True 

677 

678 

679email = Email 

680equal_to = EqualTo 

681ip_address = IPAddress 

682mac_address = MacAddress 

683length = Length 

684number_range = NumberRange 

685optional = Optional 

686input_required = InputRequired 

687data_required = DataRequired 

688regexp = Regexp 

689url = URL 

690any_of = AnyOf 

691none_of = NoneOf