Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jsonschema/_format.py: 41%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

245 statements  

1from __future__ import annotations 

2 

3from contextlib import suppress 

4from datetime import date, datetime 

5from uuid import UUID 

6import ipaddress 

7import re 

8import typing 

9import warnings 

10 

11from jsonschema.exceptions import FormatError 

12 

13_FormatCheckCallable = typing.Callable[[object], bool] 

14#: A format checker callable. 

15_F = typing.TypeVar("_F", bound=_FormatCheckCallable) 

16_RaisesType = typing.Union[type[Exception], tuple[type[Exception], ...]] 

17 

18_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII) 

19 

20 

21class FormatChecker: 

22 """ 

23 A ``format`` property checker. 

24 

25 JSON Schema does not mandate that the ``format`` property actually do any 

26 validation. If validation is desired however, instances of this class can 

27 be hooked into validators to enable format validation. 

28 

29 `FormatChecker` objects always return ``True`` when asked about 

30 formats that they do not know how to validate. 

31 

32 To add a check for a custom format use the `FormatChecker.checks` 

33 decorator. 

34 

35 Arguments: 

36 

37 formats: 

38 

39 The known formats to validate. This argument can be used to 

40 limit which formats will be used during validation. 

41 

42 """ 

43 

44 checkers: dict[ 

45 str, 

46 tuple[_FormatCheckCallable, _RaisesType], 

47 ] = {} # noqa: RUF012 

48 

49 def __init__(self, formats: typing.Iterable[str] | None = None): 

50 if formats is None: 

51 formats = self.checkers.keys() 

52 self.checkers = {k: self.checkers[k] for k in formats} 

53 

54 def __repr__(self): 

55 return f"<FormatChecker checkers={sorted(self.checkers)}>" 

56 

57 def checks( 

58 self, format: str, raises: _RaisesType = (), 

59 ) -> typing.Callable[[_F], _F]: 

60 """ 

61 Register a decorated function as validating a new format. 

62 

63 Arguments: 

64 

65 format: 

66 

67 The format that the decorated function will check. 

68 

69 raises: 

70 

71 The exception(s) raised by the decorated function when an 

72 invalid instance is found. 

73 

74 The exception object will be accessible as the 

75 `jsonschema.exceptions.ValidationError.cause` attribute of the 

76 resulting validation error. 

77 

78 """ 

79 

80 def _checks(func: _F) -> _F: 

81 self.checkers[format] = (func, raises) 

82 return func 

83 

84 return _checks 

85 

86 @classmethod 

87 def cls_checks( 

88 cls, format: str, raises: _RaisesType = (), 

89 ) -> typing.Callable[[_F], _F]: 

90 warnings.warn( 

91 ( 

92 "FormatChecker.cls_checks is deprecated. Call " 

93 "FormatChecker.checks on a specific FormatChecker instance " 

94 "instead." 

95 ), 

96 DeprecationWarning, 

97 stacklevel=2, 

98 ) 

99 return cls._cls_checks(format=format, raises=raises) 

100 

101 @classmethod 

102 def _cls_checks( 

103 cls, format: str, raises: _RaisesType = (), 

104 ) -> typing.Callable[[_F], _F]: 

105 def _checks(func: _F) -> _F: 

106 cls.checkers[format] = (func, raises) 

107 return func 

108 

109 return _checks 

110 

111 def check(self, instance: object, format: str) -> None: 

112 """ 

113 Check whether the instance conforms to the given format. 

114 

115 Arguments: 

116 

117 instance (*any primitive type*, i.e. str, number, bool): 

118 

119 The instance to check 

120 

121 format: 

122 

123 The format that instance should conform to 

124 

125 Raises: 

126 

127 FormatError: 

128 

129 if the instance does not conform to ``format`` 

130 

131 """ 

132 if format not in self.checkers: 

133 return 

134 

135 func, raises = self.checkers[format] 

136 result, cause = None, None 

137 try: 

138 result = func(instance) 

139 except raises as e: 

140 cause = e 

141 if not result: 

142 raise FormatError(f"{instance!r} is not a {format!r}", cause=cause) 

143 

144 def conforms(self, instance: object, format: str) -> bool: 

145 """ 

146 Check whether the instance conforms to the given format. 

147 

148 Arguments: 

149 

150 instance (*any primitive type*, i.e. str, number, bool): 

151 

152 The instance to check 

153 

154 format: 

155 

156 The format that instance should conform to 

157 

158 Returns: 

159 

160 bool: whether it conformed 

161 

162 """ 

163 try: 

164 self.check(instance, format) 

165 except FormatError: 

166 return False 

167 else: 

168 return True 

169 

170 

171draft3_format_checker = FormatChecker() 

172draft4_format_checker = FormatChecker() 

173draft6_format_checker = FormatChecker() 

174draft7_format_checker = FormatChecker() 

175draft201909_format_checker = FormatChecker() 

176draft202012_format_checker = FormatChecker() 

177 

178_draft_checkers: dict[str, FormatChecker] = dict( 

179 draft3=draft3_format_checker, 

180 draft4=draft4_format_checker, 

181 draft6=draft6_format_checker, 

182 draft7=draft7_format_checker, 

183 draft201909=draft201909_format_checker, 

184 draft202012=draft202012_format_checker, 

185) 

186 

187 

188def _checks_drafts( 

189 name=None, 

190 draft3=None, 

191 draft4=None, 

192 draft6=None, 

193 draft7=None, 

194 draft201909=None, 

195 draft202012=None, 

196 raises=(), 

197) -> typing.Callable[[_F], _F]: 

198 draft3 = draft3 or name 

199 draft4 = draft4 or name 

200 draft6 = draft6 or name 

201 draft7 = draft7 or name 

202 draft201909 = draft201909 or name 

203 draft202012 = draft202012 or name 

204 

205 def wrap(func: _F) -> _F: 

206 if draft3: 

207 func = _draft_checkers["draft3"].checks(draft3, raises)(func) 

208 if draft4: 

209 func = _draft_checkers["draft4"].checks(draft4, raises)(func) 

210 if draft6: 

211 func = _draft_checkers["draft6"].checks(draft6, raises)(func) 

212 if draft7: 

213 func = _draft_checkers["draft7"].checks(draft7, raises)(func) 

214 if draft201909: 

215 func = _draft_checkers["draft201909"].checks(draft201909, raises)( 

216 func, 

217 ) 

218 if draft202012: 

219 func = _draft_checkers["draft202012"].checks(draft202012, raises)( 

220 func, 

221 ) 

222 

223 # Oy. This is bad global state, but relied upon for now, until 

224 # deprecation. See #519 and test_format_checkers_come_with_defaults 

225 FormatChecker._cls_checks( 

226 draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3, 

227 raises, 

228 )(func) 

229 return func 

230 

231 return wrap 

232 

233 

234@_checks_drafts(name="idn-email") 

235@_checks_drafts(name="email") 

236def is_email(instance: object) -> bool: 

237 if not isinstance(instance, str): 

238 return True 

239 return "@" in instance 

240 

241 

242@_checks_drafts( 

243 draft3="ip-address", 

244 draft4="ipv4", 

245 draft6="ipv4", 

246 draft7="ipv4", 

247 draft201909="ipv4", 

248 draft202012="ipv4", 

249 raises=ipaddress.AddressValueError, 

250) 

251def is_ipv4(instance: object) -> bool: 

252 if not isinstance(instance, str): 

253 return True 

254 return bool(ipaddress.IPv4Address(instance)) 

255 

256 

257@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError) 

258def is_ipv6(instance: object) -> bool: 

259 if not isinstance(instance, str): 

260 return True 

261 address = ipaddress.IPv6Address(instance) 

262 return not getattr(address, "scope_id", "") 

263 

264 

265with suppress(ImportError): 

266 from fqdn import FQDN 

267 

268 @_checks_drafts( 

269 draft3="host-name", 

270 draft4="hostname", 

271 draft6="hostname", 

272 draft7="hostname", 

273 draft201909="hostname", 

274 draft202012="hostname", 

275 # fqdn.FQDN("") raises a ValueError due to a bug 

276 # however, it's not clear when or if that will be fixed, so catch it 

277 # here for now 

278 raises=ValueError, 

279 ) 

280 def is_host_name(instance: object) -> bool: 

281 if not isinstance(instance, str): 

282 return True 

283 return FQDN(instance, min_labels=1).is_valid 

284 

285 

286with suppress(ImportError): 

287 # The built-in `idna` codec only implements RFC 3890, so we go elsewhere. 

288 import idna 

289 

290 @_checks_drafts( 

291 draft7="idn-hostname", 

292 draft201909="idn-hostname", 

293 draft202012="idn-hostname", 

294 raises=(idna.IDNAError, UnicodeError), 

295 ) 

296 def is_idn_host_name(instance: object) -> bool: 

297 if not isinstance(instance, str): 

298 return True 

299 idna.encode(instance) 

300 return True 

301 

302 

303try: 

304 import rfc3987 

305except ImportError: 

306 with suppress(ImportError): 

307 from rfc3986_validator import validate_rfc3986 

308 

309 @_checks_drafts(name="uri") 

310 def is_uri(instance: object) -> bool: 

311 if not isinstance(instance, str): 

312 return True 

313 return validate_rfc3986(instance, rule="URI") 

314 

315 @_checks_drafts( 

316 draft6="uri-reference", 

317 draft7="uri-reference", 

318 draft201909="uri-reference", 

319 draft202012="uri-reference", 

320 raises=ValueError, 

321 ) 

322 def is_uri_reference(instance: object) -> bool: 

323 if not isinstance(instance, str): 

324 return True 

325 return validate_rfc3986(instance, rule="URI_reference") 

326 

327 with suppress(ImportError): 

328 from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax 

329 

330 @_checks_drafts( 

331 draft7="iri", 

332 draft201909="iri", 

333 draft202012="iri", 

334 raises=ValueError, 

335 ) 

336 def is_iri(instance: object) -> bool: 

337 if not isinstance(instance, str): 

338 return True 

339 return _rfc3987_is_valid_syntax("iri", instance) 

340 

341 @_checks_drafts( 

342 draft7="iri-reference", 

343 draft201909="iri-reference", 

344 draft202012="iri-reference", 

345 raises=ValueError, 

346 ) 

347 def is_iri_reference(instance: object) -> bool: 

348 if not isinstance(instance, str): 

349 return True 

350 return _rfc3987_is_valid_syntax("iri_reference", instance) 

351 

352else: 

353 

354 @_checks_drafts( 

355 draft7="iri", 

356 draft201909="iri", 

357 draft202012="iri", 

358 raises=ValueError, 

359 ) 

360 def is_iri(instance: object) -> bool: 

361 if not isinstance(instance, str): 

362 return True 

363 return rfc3987.parse(instance, rule="IRI") 

364 

365 @_checks_drafts( 

366 draft7="iri-reference", 

367 draft201909="iri-reference", 

368 draft202012="iri-reference", 

369 raises=ValueError, 

370 ) 

371 def is_iri_reference(instance: object) -> bool: 

372 if not isinstance(instance, str): 

373 return True 

374 return rfc3987.parse(instance, rule="IRI_reference") 

375 

376 @_checks_drafts(name="uri", raises=ValueError) 

377 def is_uri(instance: object) -> bool: 

378 if not isinstance(instance, str): 

379 return True 

380 return rfc3987.parse(instance, rule="URI") 

381 

382 @_checks_drafts( 

383 draft6="uri-reference", 

384 draft7="uri-reference", 

385 draft201909="uri-reference", 

386 draft202012="uri-reference", 

387 raises=ValueError, 

388 ) 

389 def is_uri_reference(instance: object) -> bool: 

390 if not isinstance(instance, str): 

391 return True 

392 return rfc3987.parse(instance, rule="URI_reference") 

393 

394 

395with suppress(ImportError): 

396 from rfc3339_validator import validate_rfc3339 

397 

398 @_checks_drafts(name="date-time") 

399 def is_datetime(instance: object) -> bool: 

400 if not isinstance(instance, str): 

401 return True 

402 return validate_rfc3339(instance.upper()) 

403 

404 @_checks_drafts( 

405 draft7="time", 

406 draft201909="time", 

407 draft202012="time", 

408 ) 

409 def is_time(instance: object) -> bool: 

410 if not isinstance(instance, str): 

411 return True 

412 return is_datetime("1970-01-01T" + instance) 

413 

414 

415@_checks_drafts(name="regex", raises=re.error) 

416def is_regex(instance: object) -> bool: 

417 if not isinstance(instance, str): 

418 return True 

419 return bool(re.compile(instance)) 

420 

421 

422@_checks_drafts( 

423 draft3="date", 

424 draft7="date", 

425 draft201909="date", 

426 draft202012="date", 

427 raises=ValueError, 

428) 

429def is_date(instance: object) -> bool: 

430 if not isinstance(instance, str): 

431 return True 

432 return bool(_RE_DATE.fullmatch(instance) and date.fromisoformat(instance)) 

433 

434 

435@_checks_drafts(draft3="time", raises=ValueError) 

436def is_draft3_time(instance: object) -> bool: 

437 if not isinstance(instance, str): 

438 return True 

439 return bool(datetime.strptime(instance, "%H:%M:%S")) # noqa: DTZ007 

440 

441 

442with suppress(ImportError): 

443 import webcolors 

444 

445 @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) 

446 def is_css21_color(instance: object) -> bool: 

447 if isinstance(instance, str): 

448 try: 

449 webcolors.name_to_hex(instance) 

450 except ValueError: 

451 webcolors.normalize_hex(instance.lower()) 

452 return True 

453 

454 

455with suppress(ImportError): 

456 import jsonpointer 

457 

458 @_checks_drafts( 

459 draft6="json-pointer", 

460 draft7="json-pointer", 

461 draft201909="json-pointer", 

462 draft202012="json-pointer", 

463 raises=jsonpointer.JsonPointerException, 

464 ) 

465 def is_json_pointer(instance: object) -> bool: 

466 if not isinstance(instance, str): 

467 return True 

468 return bool(jsonpointer.JsonPointer(instance)) 

469 

470 # TODO: I don't want to maintain this, so it 

471 # needs to go either into jsonpointer (pending 

472 # https://github.com/stefankoegl/python-json-pointer/issues/34) or 

473 # into a new external library. 

474 @_checks_drafts( 

475 draft7="relative-json-pointer", 

476 draft201909="relative-json-pointer", 

477 draft202012="relative-json-pointer", 

478 raises=jsonpointer.JsonPointerException, 

479 ) 

480 def is_relative_json_pointer(instance: object) -> bool: 

481 # Definition taken from: 

482 # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3 

483 if not isinstance(instance, str): 

484 return True 

485 if not instance: 

486 return False 

487 

488 non_negative_integer, rest = [], "" 

489 for i, character in enumerate(instance): 

490 if character.isdigit(): 

491 # digits with a leading "0" are not allowed 

492 if i > 0 and int(instance[i - 1]) == 0: 

493 return False 

494 

495 non_negative_integer.append(character) 

496 continue 

497 

498 if not non_negative_integer: 

499 return False 

500 

501 rest = instance[i:] 

502 break 

503 return (rest == "#") or bool(jsonpointer.JsonPointer(rest)) 

504 

505 

506with suppress(ImportError): 

507 import uri_template 

508 

509 @_checks_drafts( 

510 draft6="uri-template", 

511 draft7="uri-template", 

512 draft201909="uri-template", 

513 draft202012="uri-template", 

514 ) 

515 def is_uri_template(instance: object) -> bool: 

516 if not isinstance(instance, str): 

517 return True 

518 return uri_template.validate(instance) 

519 

520 

521with suppress(ImportError): 

522 import isoduration 

523 

524 @_checks_drafts( 

525 draft201909="duration", 

526 draft202012="duration", 

527 raises=isoduration.DurationParsingException, 

528 ) 

529 def is_duration(instance: object) -> bool: 

530 if not isinstance(instance, str): 

531 return True 

532 isoduration.parse_duration(instance) 

533 # FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21 

534 return instance.endswith(tuple("DMYWHMS")) 

535 

536 

537@_checks_drafts( 

538 draft201909="uuid", 

539 draft202012="uuid", 

540 raises=ValueError, 

541) 

542def is_uuid(instance: object) -> bool: 

543 if not isinstance(instance, str): 

544 return True 

545 UUID(instance) 

546 return all(instance[position] == "-" for position in (8, 13, 18, 23))