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

233 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

1from __future__ import annotations 

2 

3from contextlib import suppress 

4from uuid import UUID 

5import datetime 

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 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( # noqa: D417 

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 """ # noqa: D214,D405 (charliermarsh/ruff#3547) 

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 if format not in self.checkers: 

132 return 

133 

134 func, raises = self.checkers[format] 

135 result, cause = None, None 

136 try: 

137 result = func(instance) 

138 except raises as e: 

139 cause = e 

140 if not result: 

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

142 

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

144 """ 

145 Check whether the instance conforms to the given format. 

146 

147 Arguments: 

148 

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

150 

151 The instance to check 

152 

153 format: 

154 

155 The format that instance should conform to 

156 

157 Returns: 

158 

159 bool: whether it conformed 

160 """ 

161 try: 

162 self.check(instance, format) 

163 except FormatError: 

164 return False 

165 else: 

166 return True 

167 

168 

169draft3_format_checker = FormatChecker() 

170draft4_format_checker = FormatChecker() 

171draft6_format_checker = FormatChecker() 

172draft7_format_checker = FormatChecker() 

173draft201909_format_checker = FormatChecker() 

174draft202012_format_checker = FormatChecker() 

175 

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

177 draft3=draft3_format_checker, 

178 draft4=draft4_format_checker, 

179 draft6=draft6_format_checker, 

180 draft7=draft7_format_checker, 

181 draft201909=draft201909_format_checker, 

182 draft202012=draft202012_format_checker, 

183) 

184 

185 

186def _checks_drafts( 

187 name=None, 

188 draft3=None, 

189 draft4=None, 

190 draft6=None, 

191 draft7=None, 

192 draft201909=None, 

193 draft202012=None, 

194 raises=(), 

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

196 draft3 = draft3 or name 

197 draft4 = draft4 or name 

198 draft6 = draft6 or name 

199 draft7 = draft7 or name 

200 draft201909 = draft201909 or name 

201 draft202012 = draft202012 or name 

202 

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

204 if draft3: 

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

206 if draft4: 

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

208 if draft6: 

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

210 if draft7: 

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

212 if draft201909: 

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

214 func, 

215 ) 

216 if draft202012: 

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

218 func, 

219 ) 

220 

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

222 # deprecation. See #519 and test_format_checkers_come_with_defaults 

223 FormatChecker._cls_checks( 

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

225 raises, 

226 )(func) 

227 return func 

228 

229 return wrap 

230 

231 

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

233@_checks_drafts(name="email") 

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

235 if not isinstance(instance, str): 

236 return True 

237 return "@" in instance 

238 

239 

240@_checks_drafts( 

241 draft3="ip-address", 

242 draft4="ipv4", 

243 draft6="ipv4", 

244 draft7="ipv4", 

245 draft201909="ipv4", 

246 draft202012="ipv4", 

247 raises=ipaddress.AddressValueError, 

248) 

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

250 if not isinstance(instance, str): 

251 return True 

252 return bool(ipaddress.IPv4Address(instance)) 

253 

254 

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

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

257 if not isinstance(instance, str): 

258 return True 

259 address = ipaddress.IPv6Address(instance) 

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

261 

262 

263with suppress(ImportError): 

264 from fqdn import FQDN 

265 

266 @_checks_drafts( 

267 draft3="host-name", 

268 draft4="hostname", 

269 draft6="hostname", 

270 draft7="hostname", 

271 draft201909="hostname", 

272 draft202012="hostname", 

273 ) 

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

275 if not isinstance(instance, str): 

276 return True 

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

278 

279 

280with suppress(ImportError): 

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

282 import idna 

283 

284 @_checks_drafts( 

285 draft7="idn-hostname", 

286 draft201909="idn-hostname", 

287 draft202012="idn-hostname", 

288 raises=(idna.IDNAError, UnicodeError), 

289 ) 

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

291 if not isinstance(instance, str): 

292 return True 

293 idna.encode(instance) 

294 return True 

295 

296 

297try: 

298 import rfc3987 

299except ImportError: 

300 with suppress(ImportError): 

301 from rfc3986_validator import validate_rfc3986 

302 

303 @_checks_drafts(name="uri") 

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

305 if not isinstance(instance, str): 

306 return True 

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

308 

309 @_checks_drafts( 

310 draft6="uri-reference", 

311 draft7="uri-reference", 

312 draft201909="uri-reference", 

313 draft202012="uri-reference", 

314 raises=ValueError, 

315 ) 

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

317 if not isinstance(instance, str): 

318 return True 

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

320 

321else: 

322 

323 @_checks_drafts( 

324 draft7="iri", 

325 draft201909="iri", 

326 draft202012="iri", 

327 raises=ValueError, 

328 ) 

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

330 if not isinstance(instance, str): 

331 return True 

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

333 

334 @_checks_drafts( 

335 draft7="iri-reference", 

336 draft201909="iri-reference", 

337 draft202012="iri-reference", 

338 raises=ValueError, 

339 ) 

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

341 if not isinstance(instance, str): 

342 return True 

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

344 

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

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

347 if not isinstance(instance, str): 

348 return True 

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

350 

351 @_checks_drafts( 

352 draft6="uri-reference", 

353 draft7="uri-reference", 

354 draft201909="uri-reference", 

355 draft202012="uri-reference", 

356 raises=ValueError, 

357 ) 

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

359 if not isinstance(instance, str): 

360 return True 

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

362 

363 

364with suppress(ImportError): 

365 from rfc3339_validator import validate_rfc3339 

366 

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

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

369 if not isinstance(instance, str): 

370 return True 

371 return validate_rfc3339(instance.upper()) 

372 

373 @_checks_drafts( 

374 draft7="time", 

375 draft201909="time", 

376 draft202012="time", 

377 ) 

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

379 if not isinstance(instance, str): 

380 return True 

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

382 

383 

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

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

386 if not isinstance(instance, str): 

387 return True 

388 return bool(re.compile(instance)) 

389 

390 

391@_checks_drafts( 

392 draft3="date", 

393 draft7="date", 

394 draft201909="date", 

395 draft202012="date", 

396 raises=ValueError, 

397) 

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

399 if not isinstance(instance, str): 

400 return True 

401 return bool( 

402 _RE_DATE.fullmatch(instance) 

403 and datetime.date.fromisoformat(instance) 

404 ) 

405 

406 

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

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

409 if not isinstance(instance, str): 

410 return True 

411 return bool(datetime.datetime.strptime(instance, "%H:%M:%S")) 

412 

413 

414with suppress(ImportError): 

415 from webcolors import CSS21_NAMES_TO_HEX 

416 import webcolors 

417 

418 def is_css_color_code(instance: object) -> bool: 

419 return webcolors.normalize_hex(instance) 

420 

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

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

423 if ( 

424 not isinstance(instance, str) 

425 or instance.lower() in CSS21_NAMES_TO_HEX 

426 ): 

427 return True 

428 return is_css_color_code(instance) 

429 

430 

431with suppress(ImportError): 

432 import jsonpointer 

433 

434 @_checks_drafts( 

435 draft6="json-pointer", 

436 draft7="json-pointer", 

437 draft201909="json-pointer", 

438 draft202012="json-pointer", 

439 raises=jsonpointer.JsonPointerException, 

440 ) 

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

442 if not isinstance(instance, str): 

443 return True 

444 return bool(jsonpointer.JsonPointer(instance)) 

445 

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

447 # needs to go either into jsonpointer (pending 

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

449 # into a new external library. 

450 @_checks_drafts( 

451 draft7="relative-json-pointer", 

452 draft201909="relative-json-pointer", 

453 draft202012="relative-json-pointer", 

454 raises=jsonpointer.JsonPointerException, 

455 ) 

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

457 # Definition taken from: 

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

459 if not isinstance(instance, str): 

460 return True 

461 if not instance: 

462 return False 

463 

464 non_negative_integer, rest = [], "" 

465 for i, character in enumerate(instance): 

466 if character.isdigit(): 

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

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

469 return False 

470 

471 non_negative_integer.append(character) 

472 continue 

473 

474 if not non_negative_integer: 

475 return False 

476 

477 rest = instance[i:] 

478 break 

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

480 

481 

482with suppress(ImportError): 

483 import uri_template 

484 

485 @_checks_drafts( 

486 draft6="uri-template", 

487 draft7="uri-template", 

488 draft201909="uri-template", 

489 draft202012="uri-template", 

490 ) 

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

492 if not isinstance(instance, str): 

493 return True 

494 return uri_template.validate(instance) 

495 

496 

497with suppress(ImportError): 

498 import isoduration 

499 

500 @_checks_drafts( 

501 draft201909="duration", 

502 draft202012="duration", 

503 raises=isoduration.DurationParsingException, 

504 ) 

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

506 if not isinstance(instance, str): 

507 return True 

508 isoduration.parse_duration(instance) 

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

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

511 

512 

513@_checks_drafts( 

514 draft201909="uuid", 

515 draft202012="uuid", 

516 raises=ValueError, 

517) 

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

519 if not isinstance(instance, str): 

520 return True 

521 UUID(instance) 

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