Coverage for /pythoncovmergedfiles/medio/medio/src/jsonschema/jsonschema/_format.py: 42%

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

240 statements  

1from __future__ import annotations 

2 

3from contextlib import suppress 

4from datetime import date, datetime 

5from uuid import UUID 

6import ipaddress 

7import re 

8import string 

9import typing 

10import warnings 

11 

12from jsonschema.exceptions import FormatError 

13 

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

15#: A format checker callable. 

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

17_RaisesType = type[Exception] | tuple[type[Exception], ...] 

18 

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

20 

21 

22class FormatChecker: 

23 """ 

24 A ``format`` property checker. 

25 

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

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

28 be hooked into validators to enable format validation. 

29 

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

31 formats that they do not know how to validate. 

32 

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

34 decorator. 

35 

36 Arguments: 

37 

38 formats: 

39 

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

41 limit which formats will be used during validation. 

42 

43 """ 

44 

45 checkers: dict[ 

46 str, 

47 tuple[_FormatCheckCallable, _RaisesType], 

48 ] = {} # noqa: RUF012 

49 

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

51 if formats is None: 

52 formats = self.checkers.keys() 

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

54 

55 def __repr__(self): 

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

57 

58 def checks( 

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

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

61 """ 

62 Register a decorated function as validating a new format. 

63 

64 Arguments: 

65 

66 format: 

67 

68 The format that the decorated function will check. 

69 

70 raises: 

71 

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

73 invalid instance is found. 

74 

75 The exception object will be accessible as the 

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

77 resulting validation error. 

78 

79 """ 

80 

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

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

83 return func 

84 

85 return _checks 

86 

87 @classmethod 

88 def cls_checks( 

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

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

91 warnings.warn( 

92 ( 

93 "FormatChecker.cls_checks is deprecated. Call " 

94 "FormatChecker.checks on a specific FormatChecker instance " 

95 "instead." 

96 ), 

97 DeprecationWarning, 

98 stacklevel=2, 

99 ) 

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

101 

102 @classmethod 

103 def _cls_checks( 

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

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

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

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

108 return func 

109 

110 return _checks 

111 

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

113 """ 

114 Check whether the instance conforms to the given format. 

115 

116 Arguments: 

117 

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

119 

120 The instance to check 

121 

122 format: 

123 

124 The format that instance should conform to 

125 

126 Raises: 

127 

128 FormatError: 

129 

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

131 

132 """ 

133 if format not in self.checkers: 

134 return 

135 

136 func, raises = self.checkers[format] 

137 result, cause = None, None 

138 try: 

139 result = func(instance) 

140 except raises as e: 

141 cause = e 

142 if not result: 

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

144 

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

146 """ 

147 Check whether the instance conforms to the given format. 

148 

149 Arguments: 

150 

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

152 

153 The instance to check 

154 

155 format: 

156 

157 The format that instance should conform to 

158 

159 Returns: 

160 

161 bool: whether it conformed 

162 

163 """ 

164 try: 

165 self.check(instance, format) 

166 except FormatError: 

167 return False 

168 else: 

169 return True 

170 

171 

172draft3_format_checker = FormatChecker() 

173draft4_format_checker = FormatChecker() 

174draft6_format_checker = FormatChecker() 

175draft7_format_checker = FormatChecker() 

176draft201909_format_checker = FormatChecker() 

177draft202012_format_checker = FormatChecker() 

178 

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

180 draft3=draft3_format_checker, 

181 draft4=draft4_format_checker, 

182 draft6=draft6_format_checker, 

183 draft7=draft7_format_checker, 

184 draft201909=draft201909_format_checker, 

185 draft202012=draft202012_format_checker, 

186) 

187 

188 

189def _checks_drafts( 

190 name=None, 

191 draft3=None, 

192 draft4=None, 

193 draft6=None, 

194 draft7=None, 

195 draft201909=None, 

196 draft202012=None, 

197 raises=(), 

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

199 draft3 = draft3 or name 

200 draft4 = draft4 or name 

201 draft6 = draft6 or name 

202 draft7 = draft7 or name 

203 draft201909 = draft201909 or name 

204 draft202012 = draft202012 or name 

205 

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

207 if draft3: 

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

209 if draft4: 

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

211 if draft6: 

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

213 if draft7: 

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

215 if draft201909: 

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

217 func, 

218 ) 

219 if draft202012: 

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

221 func, 

222 ) 

223 

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

225 # deprecation. See #519 and test_format_checkers_come_with_defaults 

226 FormatChecker._cls_checks( 

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

228 raises, 

229 )(func) 

230 return func 

231 

232 return wrap 

233 

234 

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

236@_checks_drafts(name="email") 

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

238 if not isinstance(instance, str): 

239 return True 

240 return "@" in instance 

241 

242 

243@_checks_drafts( 

244 draft3="ip-address", 

245 draft4="ipv4", 

246 draft6="ipv4", 

247 draft7="ipv4", 

248 draft201909="ipv4", 

249 draft202012="ipv4", 

250 raises=ipaddress.AddressValueError, 

251) 

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

253 if not isinstance(instance, str): 

254 return True 

255 return bool(ipaddress.IPv4Address(instance)) 

256 

257 

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

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

260 if not isinstance(instance, str): 

261 return True 

262 address = ipaddress.IPv6Address(instance) 

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

264 

265 

266with suppress(ImportError): 

267 from fqdn import FQDN 

268 

269 @_checks_drafts( 

270 draft3="host-name", 

271 draft4="hostname", 

272 draft6="hostname", 

273 draft7="hostname", 

274 draft201909="hostname", 

275 draft202012="hostname", 

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

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

278 # here for now 

279 raises=ValueError, 

280 ) 

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

282 if not isinstance(instance, str): 

283 return True 

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

285 

286 

287with suppress(ImportError): 

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

289 import idna 

290 

291 @_checks_drafts( 

292 draft7="idn-hostname", 

293 draft201909="idn-hostname", 

294 draft202012="idn-hostname", 

295 raises=(idna.IDNAError, UnicodeError), 

296 ) 

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

298 if not isinstance(instance, str): 

299 return True 

300 idna.encode(instance) 

301 return True 

302 

303 

304try: 

305 import rfc3987 

306except ImportError: 

307 with suppress(ImportError): 

308 from rfc3986_validator import validate_rfc3986 

309 

310 @_checks_drafts(name="uri") 

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

312 if not isinstance(instance, str): 

313 return True 

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

315 

316 @_checks_drafts( 

317 draft6="uri-reference", 

318 draft7="uri-reference", 

319 draft201909="uri-reference", 

320 draft202012="uri-reference", 

321 raises=ValueError, 

322 ) 

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

324 if not isinstance(instance, str): 

325 return True 

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

327 

328 with suppress(ImportError): 

329 from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax 

330 

331 @_checks_drafts( 

332 draft7="iri", 

333 draft201909="iri", 

334 draft202012="iri", 

335 raises=ValueError, 

336 ) 

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

338 if not isinstance(instance, str): 

339 return True 

340 return _rfc3987_is_valid_syntax("iri", instance) 

341 

342 @_checks_drafts( 

343 draft7="iri-reference", 

344 draft201909="iri-reference", 

345 draft202012="iri-reference", 

346 raises=ValueError, 

347 ) 

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

349 if not isinstance(instance, str): 

350 return True 

351 return _rfc3987_is_valid_syntax("iri_reference", instance) 

352 

353else: 

354 

355 @_checks_drafts( 

356 draft7="iri", 

357 draft201909="iri", 

358 draft202012="iri", 

359 raises=ValueError, 

360 ) 

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

362 if not isinstance(instance, str): 

363 return True 

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

365 

366 @_checks_drafts( 

367 draft7="iri-reference", 

368 draft201909="iri-reference", 

369 draft202012="iri-reference", 

370 raises=ValueError, 

371 ) 

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

373 if not isinstance(instance, str): 

374 return True 

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

376 

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

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

379 if not isinstance(instance, str): 

380 return True 

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

382 

383 @_checks_drafts( 

384 draft6="uri-reference", 

385 draft7="uri-reference", 

386 draft201909="uri-reference", 

387 draft202012="uri-reference", 

388 raises=ValueError, 

389 ) 

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

391 if not isinstance(instance, str): 

392 return True 

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

394 

395 

396with suppress(ImportError): 

397 from rfc3339_validator import validate_rfc3339 

398 

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

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

401 if not isinstance(instance, str): 

402 return True 

403 return validate_rfc3339(instance.upper()) 

404 

405 @_checks_drafts( 

406 draft7="time", 

407 draft201909="time", 

408 draft202012="time", 

409 ) 

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

411 if not isinstance(instance, str): 

412 return True 

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

414 

415 

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

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

418 if not isinstance(instance, str): 

419 return True 

420 return bool(re.compile(instance)) 

421 

422 

423@_checks_drafts( 

424 draft3="date", 

425 draft7="date", 

426 draft201909="date", 

427 draft202012="date", 

428 raises=ValueError, 

429) 

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

431 if not isinstance(instance, str): 

432 return True 

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

434 

435 

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

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

438 if not isinstance(instance, str): 

439 return True 

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

441 

442 

443with suppress(ImportError): 

444 import webcolors 

445 

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

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

448 if isinstance(instance, str): 

449 try: 

450 webcolors.name_to_hex(instance) 

451 except ValueError: 

452 webcolors.normalize_hex(instance.lower()) 

453 return True 

454 

455 

456with suppress(ImportError): 

457 import jsonpointer 

458 

459 @_checks_drafts( 

460 draft6="json-pointer", 

461 draft7="json-pointer", 

462 draft201909="json-pointer", 

463 draft202012="json-pointer", 

464 raises=jsonpointer.JsonPointerException, 

465 ) 

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

467 if not isinstance(instance, str): 

468 return True 

469 return bool(jsonpointer.JsonPointer(instance)) 

470 

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

472 # needs to go either into jsonpointer (pending 

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

474 # into a new external library. 

475 @_checks_drafts( 

476 draft7="relative-json-pointer", 

477 draft201909="relative-json-pointer", 

478 draft202012="relative-json-pointer", 

479 raises=jsonpointer.JsonPointerException, 

480 ) 

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

482 # Definition taken from: 

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

484 if not isinstance(instance, str): 

485 return True 

486 if not instance: 

487 return False 

488 

489 # A non-negative-integer prefix of ASCII digits (%x30-39), which is 

490 # either "0" or has no leading "0". 

491 rest = instance.lstrip(string.digits) 

492 prefix = instance[:len(instance) - len(rest)] 

493 if not prefix: 

494 return False 

495 if len(prefix) > 1 and prefix[0] == "0": 

496 return False 

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

498 

499 

500with suppress(ImportError): 

501 import uri_template 

502 

503 @_checks_drafts( 

504 draft6="uri-template", 

505 draft7="uri-template", 

506 draft201909="uri-template", 

507 draft202012="uri-template", 

508 ) 

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

510 if not isinstance(instance, str): 

511 return True 

512 return uri_template.validate(instance) 

513 

514 

515with suppress(ImportError): 

516 import isoduration 

517 

518 @_checks_drafts( 

519 draft201909="duration", 

520 draft202012="duration", 

521 raises=isoduration.DurationParsingException, 

522 ) 

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

524 if not isinstance(instance, str): 

525 return True 

526 isoduration.parse_duration(instance) 

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

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

529 

530 

531@_checks_drafts( 

532 draft201909="uuid", 

533 draft202012="uuid", 

534 raises=ValueError, 

535) 

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

537 if not isinstance(instance, str): 

538 return True 

539 return str(UUID(instance)) == instance.lower()