Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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[ 

17 typing.Type[Exception], typing.Tuple[typing.Type[Exception], ...], 

18] 

19 

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

21 

22 

23class FormatChecker: 

24 """ 

25 A ``format`` property checker. 

26 

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

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

29 be hooked into validators to enable format validation. 

30 

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

32 formats that they do not know how to validate. 

33 

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

35 decorator. 

36 

37 Arguments: 

38 

39 formats: 

40 

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

42 limit which formats will be used during validation. 

43 

44 """ 

45 

46 checkers: dict[ 

47 str, 

48 tuple[_FormatCheckCallable, _RaisesType], 

49 ] = {} # noqa: RUF012 

50 

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

52 if formats is None: 

53 formats = self.checkers.keys() 

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

55 

56 def __repr__(self): 

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

58 

59 def checks( 

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

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

62 """ 

63 Register a decorated function as validating a new format. 

64 

65 Arguments: 

66 

67 format: 

68 

69 The format that the decorated function will check. 

70 

71 raises: 

72 

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

74 invalid instance is found. 

75 

76 The exception object will be accessible as the 

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

78 resulting validation error. 

79 

80 """ 

81 

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

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

84 return func 

85 

86 return _checks 

87 

88 @classmethod 

89 def cls_checks( 

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

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

92 warnings.warn( 

93 ( 

94 "FormatChecker.cls_checks is deprecated. Call " 

95 "FormatChecker.checks on a specific FormatChecker instance " 

96 "instead." 

97 ), 

98 DeprecationWarning, 

99 stacklevel=2, 

100 ) 

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

102 

103 @classmethod 

104 def _cls_checks( 

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

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

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

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

109 return func 

110 

111 return _checks 

112 

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

114 """ 

115 Check whether the instance conforms to the given format. 

116 

117 Arguments: 

118 

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

120 

121 The instance to check 

122 

123 format: 

124 

125 The format that instance should conform to 

126 

127 Raises: 

128 

129 FormatError: 

130 

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

132 

133 """ 

134 if format not in self.checkers: 

135 return 

136 

137 func, raises = self.checkers[format] 

138 result, cause = None, None 

139 try: 

140 result = func(instance) 

141 except raises as e: 

142 cause = e 

143 if not result: 

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

145 

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

147 """ 

148 Check whether the instance conforms to the given format. 

149 

150 Arguments: 

151 

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

153 

154 The instance to check 

155 

156 format: 

157 

158 The format that instance should conform to 

159 

160 Returns: 

161 

162 bool: whether it conformed 

163 

164 """ 

165 try: 

166 self.check(instance, format) 

167 except FormatError: 

168 return False 

169 else: 

170 return True 

171 

172 

173draft3_format_checker = FormatChecker() 

174draft4_format_checker = FormatChecker() 

175draft6_format_checker = FormatChecker() 

176draft7_format_checker = FormatChecker() 

177draft201909_format_checker = FormatChecker() 

178draft202012_format_checker = FormatChecker() 

179 

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

181 draft3=draft3_format_checker, 

182 draft4=draft4_format_checker, 

183 draft6=draft6_format_checker, 

184 draft7=draft7_format_checker, 

185 draft201909=draft201909_format_checker, 

186 draft202012=draft202012_format_checker, 

187) 

188 

189 

190def _checks_drafts( 

191 name=None, 

192 draft3=None, 

193 draft4=None, 

194 draft6=None, 

195 draft7=None, 

196 draft201909=None, 

197 draft202012=None, 

198 raises=(), 

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

200 draft3 = draft3 or name 

201 draft4 = draft4 or name 

202 draft6 = draft6 or name 

203 draft7 = draft7 or name 

204 draft201909 = draft201909 or name 

205 draft202012 = draft202012 or name 

206 

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

208 if draft3: 

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

210 if draft4: 

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

212 if draft6: 

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

214 if draft7: 

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

216 if draft201909: 

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

218 func, 

219 ) 

220 if draft202012: 

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

222 func, 

223 ) 

224 

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

226 # deprecation. See #519 and test_format_checkers_come_with_defaults 

227 FormatChecker._cls_checks( 

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

229 raises, 

230 )(func) 

231 return func 

232 

233 return wrap 

234 

235 

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

237@_checks_drafts(name="email") 

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

239 if not isinstance(instance, str): 

240 return True 

241 return "@" in instance 

242 

243 

244@_checks_drafts( 

245 draft3="ip-address", 

246 draft4="ipv4", 

247 draft6="ipv4", 

248 draft7="ipv4", 

249 draft201909="ipv4", 

250 draft202012="ipv4", 

251 raises=ipaddress.AddressValueError, 

252) 

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

254 if not isinstance(instance, str): 

255 return True 

256 return bool(ipaddress.IPv4Address(instance)) 

257 

258 

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

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

261 if not isinstance(instance, str): 

262 return True 

263 address = ipaddress.IPv6Address(instance) 

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

265 

266 

267with suppress(ImportError): 

268 from fqdn import FQDN 

269 

270 @_checks_drafts( 

271 draft3="host-name", 

272 draft4="hostname", 

273 draft6="hostname", 

274 draft7="hostname", 

275 draft201909="hostname", 

276 draft202012="hostname", 

277 ) 

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

279 if not isinstance(instance, str): 

280 return True 

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

282 

283 

284with suppress(ImportError): 

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

286 import idna 

287 

288 @_checks_drafts( 

289 draft7="idn-hostname", 

290 draft201909="idn-hostname", 

291 draft202012="idn-hostname", 

292 raises=(idna.IDNAError, UnicodeError), 

293 ) 

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

295 if not isinstance(instance, str): 

296 return True 

297 idna.encode(instance) 

298 return True 

299 

300 

301try: 

302 import rfc3987 

303except ImportError: 

304 with suppress(ImportError): 

305 from rfc3986_validator import validate_rfc3986 

306 

307 @_checks_drafts(name="uri") 

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

309 if not isinstance(instance, str): 

310 return True 

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

312 

313 @_checks_drafts( 

314 draft6="uri-reference", 

315 draft7="uri-reference", 

316 draft201909="uri-reference", 

317 draft202012="uri-reference", 

318 raises=ValueError, 

319 ) 

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

321 if not isinstance(instance, str): 

322 return True 

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

324 

325else: 

326 

327 @_checks_drafts( 

328 draft7="iri", 

329 draft201909="iri", 

330 draft202012="iri", 

331 raises=ValueError, 

332 ) 

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

334 if not isinstance(instance, str): 

335 return True 

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

337 

338 @_checks_drafts( 

339 draft7="iri-reference", 

340 draft201909="iri-reference", 

341 draft202012="iri-reference", 

342 raises=ValueError, 

343 ) 

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

345 if not isinstance(instance, str): 

346 return True 

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

348 

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

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

351 if not isinstance(instance, str): 

352 return True 

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

354 

355 @_checks_drafts( 

356 draft6="uri-reference", 

357 draft7="uri-reference", 

358 draft201909="uri-reference", 

359 draft202012="uri-reference", 

360 raises=ValueError, 

361 ) 

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

363 if not isinstance(instance, str): 

364 return True 

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

366 

367 

368with suppress(ImportError): 

369 from rfc3339_validator import validate_rfc3339 

370 

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

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

373 if not isinstance(instance, str): 

374 return True 

375 return validate_rfc3339(instance.upper()) 

376 

377 @_checks_drafts( 

378 draft7="time", 

379 draft201909="time", 

380 draft202012="time", 

381 ) 

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

383 if not isinstance(instance, str): 

384 return True 

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

386 

387 

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

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

390 if not isinstance(instance, str): 

391 return True 

392 return bool(re.compile(instance)) 

393 

394 

395@_checks_drafts( 

396 draft3="date", 

397 draft7="date", 

398 draft201909="date", 

399 draft202012="date", 

400 raises=ValueError, 

401) 

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

403 if not isinstance(instance, str): 

404 return True 

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

406 

407 

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

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

410 if not isinstance(instance, str): 

411 return True 

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

413 

414 

415with suppress(ImportError): 

416 import webcolors 

417 

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

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

420 if isinstance(instance, str): 

421 try: 

422 webcolors.name_to_hex(instance) 

423 except ValueError: 

424 webcolors.normalize_hex(instance.lower()) 

425 return True 

426 

427 

428with suppress(ImportError): 

429 import jsonpointer 

430 

431 @_checks_drafts( 

432 draft6="json-pointer", 

433 draft7="json-pointer", 

434 draft201909="json-pointer", 

435 draft202012="json-pointer", 

436 raises=jsonpointer.JsonPointerException, 

437 ) 

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

439 if not isinstance(instance, str): 

440 return True 

441 return bool(jsonpointer.JsonPointer(instance)) 

442 

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

444 # needs to go either into jsonpointer (pending 

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

446 # into a new external library. 

447 @_checks_drafts( 

448 draft7="relative-json-pointer", 

449 draft201909="relative-json-pointer", 

450 draft202012="relative-json-pointer", 

451 raises=jsonpointer.JsonPointerException, 

452 ) 

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

454 # Definition taken from: 

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

456 if not isinstance(instance, str): 

457 return True 

458 if not instance: 

459 return False 

460 

461 non_negative_integer, rest = [], "" 

462 for i, character in enumerate(instance): 

463 if character.isdigit(): 

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

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

466 return False 

467 

468 non_negative_integer.append(character) 

469 continue 

470 

471 if not non_negative_integer: 

472 return False 

473 

474 rest = instance[i:] 

475 break 

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

477 

478 

479with suppress(ImportError): 

480 import uri_template 

481 

482 @_checks_drafts( 

483 draft6="uri-template", 

484 draft7="uri-template", 

485 draft201909="uri-template", 

486 draft202012="uri-template", 

487 ) 

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

489 if not isinstance(instance, str): 

490 return True 

491 return uri_template.validate(instance) 

492 

493 

494with suppress(ImportError): 

495 import isoduration 

496 

497 @_checks_drafts( 

498 draft201909="duration", 

499 draft202012="duration", 

500 raises=isoduration.DurationParsingException, 

501 ) 

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

503 if not isinstance(instance, str): 

504 return True 

505 isoduration.parse_duration(instance) 

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

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

508 

509 

510@_checks_drafts( 

511 draft201909="uuid", 

512 draft202012="uuid", 

513 raises=ValueError, 

514) 

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

516 if not isinstance(instance, str): 

517 return True 

518 UUID(instance) 

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