Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/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

233 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 

327else: 

328 

329 @_checks_drafts( 

330 draft7="iri", 

331 draft201909="iri", 

332 draft202012="iri", 

333 raises=ValueError, 

334 ) 

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

336 if not isinstance(instance, str): 

337 return True 

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

339 

340 @_checks_drafts( 

341 draft7="iri-reference", 

342 draft201909="iri-reference", 

343 draft202012="iri-reference", 

344 raises=ValueError, 

345 ) 

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

347 if not isinstance(instance, str): 

348 return True 

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

350 

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

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

353 if not isinstance(instance, str): 

354 return True 

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

356 

357 @_checks_drafts( 

358 draft6="uri-reference", 

359 draft7="uri-reference", 

360 draft201909="uri-reference", 

361 draft202012="uri-reference", 

362 raises=ValueError, 

363 ) 

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

365 if not isinstance(instance, str): 

366 return True 

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

368 

369 

370with suppress(ImportError): 

371 from rfc3339_validator import validate_rfc3339 

372 

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

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

375 if not isinstance(instance, str): 

376 return True 

377 return validate_rfc3339(instance.upper()) 

378 

379 @_checks_drafts( 

380 draft7="time", 

381 draft201909="time", 

382 draft202012="time", 

383 ) 

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

385 if not isinstance(instance, str): 

386 return True 

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

388 

389 

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

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

392 if not isinstance(instance, str): 

393 return True 

394 return bool(re.compile(instance)) 

395 

396 

397@_checks_drafts( 

398 draft3="date", 

399 draft7="date", 

400 draft201909="date", 

401 draft202012="date", 

402 raises=ValueError, 

403) 

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

405 if not isinstance(instance, str): 

406 return True 

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

408 

409 

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

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

412 if not isinstance(instance, str): 

413 return True 

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

415 

416 

417with suppress(ImportError): 

418 import webcolors 

419 

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

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

422 if isinstance(instance, str): 

423 try: 

424 webcolors.name_to_hex(instance) 

425 except ValueError: 

426 webcolors.normalize_hex(instance.lower()) 

427 return True 

428 

429 

430with suppress(ImportError): 

431 import jsonpointer 

432 

433 @_checks_drafts( 

434 draft6="json-pointer", 

435 draft7="json-pointer", 

436 draft201909="json-pointer", 

437 draft202012="json-pointer", 

438 raises=jsonpointer.JsonPointerException, 

439 ) 

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

441 if not isinstance(instance, str): 

442 return True 

443 return bool(jsonpointer.JsonPointer(instance)) 

444 

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

446 # needs to go either into jsonpointer (pending 

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

448 # into a new external library. 

449 @_checks_drafts( 

450 draft7="relative-json-pointer", 

451 draft201909="relative-json-pointer", 

452 draft202012="relative-json-pointer", 

453 raises=jsonpointer.JsonPointerException, 

454 ) 

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

456 # Definition taken from: 

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

458 if not isinstance(instance, str): 

459 return True 

460 if not instance: 

461 return False 

462 

463 non_negative_integer, rest = [], "" 

464 for i, character in enumerate(instance): 

465 if character.isdigit(): 

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

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

468 return False 

469 

470 non_negative_integer.append(character) 

471 continue 

472 

473 if not non_negative_integer: 

474 return False 

475 

476 rest = instance[i:] 

477 break 

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

479 

480 

481with suppress(ImportError): 

482 import uri_template 

483 

484 @_checks_drafts( 

485 draft6="uri-template", 

486 draft7="uri-template", 

487 draft201909="uri-template", 

488 draft202012="uri-template", 

489 ) 

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

491 if not isinstance(instance, str): 

492 return True 

493 return uri_template.validate(instance) 

494 

495 

496with suppress(ImportError): 

497 import isoduration 

498 

499 @_checks_drafts( 

500 draft201909="duration", 

501 draft202012="duration", 

502 raises=isoduration.DurationParsingException, 

503 ) 

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

505 if not isinstance(instance, str): 

506 return True 

507 isoduration.parse_duration(instance) 

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

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

510 

511 

512@_checks_drafts( 

513 draft201909="uuid", 

514 draft202012="uuid", 

515 raises=ValueError, 

516) 

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

518 if not isinstance(instance, str): 

519 return True 

520 UUID(instance) 

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