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

232 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +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_F = typing.TypeVar("_F", bound=_FormatCheckCallable) 

15_RaisesType = typing.Union[ 

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

17] 

18 

19 

20class FormatChecker: 

21 """ 

22 A ``format`` property checker. 

23 

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

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

26 be hooked into validators to enable format validation. 

27 

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

29 formats that they do not know how to validate. 

30 

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

32 decorator. 

33 

34 Arguments: 

35 

36 formats: 

37 

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

39 limit which formats will be used during validation. 

40 """ 

41 

42 checkers: dict[ 

43 str, 

44 tuple[_FormatCheckCallable, _RaisesType], 

45 ] = {} 

46 

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

48 if formats is None: 

49 formats = self.checkers.keys() 

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

51 

52 def __repr__(self): 

53 return "<FormatChecker checkers={}>".format(sorted(self.checkers)) 

54 

55 def checks( 

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

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

58 """ 

59 Register a decorated function as validating a new format. 

60 

61 Arguments: 

62 

63 format: 

64 

65 The format that the decorated function will check. 

66 

67 raises: 

68 

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

70 invalid instance is found. 

71 

72 The exception object will be accessible as the 

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

74 resulting validation error. 

75 """ 

76 

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

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

79 return func 

80 

81 return _checks 

82 

83 @classmethod 

84 def cls_checks( 

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

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

87 warnings.warn( 

88 ( 

89 "FormatChecker.cls_checks is deprecated. Call " 

90 "FormatChecker.checks on a specific FormatChecker instance " 

91 "instead." 

92 ), 

93 DeprecationWarning, 

94 stacklevel=2, 

95 ) 

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

97 

98 @classmethod 

99 def _cls_checks( 

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

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

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

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

104 return func 

105 

106 return _checks 

107 

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

109 """ 

110 Check whether the instance conforms to the given format. 

111 

112 Arguments: 

113 

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

115 

116 The instance to check 

117 

118 format: 

119 

120 The format that instance should conform to 

121 

122 Raises: 

123 

124 FormatError: 

125 

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

127 """ 

128 

129 if format not in self.checkers: 

130 return 

131 

132 func, raises = self.checkers[format] 

133 result, cause = None, None 

134 try: 

135 result = func(instance) 

136 except raises as e: 

137 cause = e 

138 if not result: 

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

140 

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

142 """ 

143 Check whether the instance conforms to the given format. 

144 

145 Arguments: 

146 

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

148 

149 The instance to check 

150 

151 format: 

152 

153 The format that instance should conform to 

154 

155 Returns: 

156 

157 bool: whether it conformed 

158 """ 

159 

160 try: 

161 self.check(instance, format) 

162 except FormatError: 

163 return False 

164 else: 

165 return True 

166 

167 

168draft3_format_checker = FormatChecker() 

169draft4_format_checker = FormatChecker() 

170draft6_format_checker = FormatChecker() 

171draft7_format_checker = FormatChecker() 

172draft201909_format_checker = FormatChecker() 

173draft202012_format_checker = FormatChecker() 

174 

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

176 draft3=draft3_format_checker, 

177 draft4=draft4_format_checker, 

178 draft6=draft6_format_checker, 

179 draft7=draft7_format_checker, 

180 draft201909=draft201909_format_checker, 

181 draft202012=draft202012_format_checker, 

182) 

183 

184 

185def _checks_drafts( 

186 name=None, 

187 draft3=None, 

188 draft4=None, 

189 draft6=None, 

190 draft7=None, 

191 draft201909=None, 

192 draft202012=None, 

193 raises=(), 

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

195 draft3 = draft3 or name 

196 draft4 = draft4 or name 

197 draft6 = draft6 or name 

198 draft7 = draft7 or name 

199 draft201909 = draft201909 or name 

200 draft202012 = draft202012 or name 

201 

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

203 if draft3: 

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

205 if draft4: 

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

207 if draft6: 

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

209 if draft7: 

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

211 if draft201909: 

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

213 func, 

214 ) 

215 if draft202012: 

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

217 func, 

218 ) 

219 

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

221 # deprecation. See #519 and test_format_checkers_come_with_defaults 

222 FormatChecker._cls_checks( 

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

224 raises, 

225 )(func) 

226 return func 

227 

228 return wrap 

229 

230 

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

232@_checks_drafts(name="email") 

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

234 if not isinstance(instance, str): 

235 return True 

236 return "@" in instance 

237 

238 

239@_checks_drafts( 

240 draft3="ip-address", 

241 draft4="ipv4", 

242 draft6="ipv4", 

243 draft7="ipv4", 

244 draft201909="ipv4", 

245 draft202012="ipv4", 

246 raises=ipaddress.AddressValueError, 

247) 

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

249 if not isinstance(instance, str): 

250 return True 

251 return bool(ipaddress.IPv4Address(instance)) 

252 

253 

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

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

256 if not isinstance(instance, str): 

257 return True 

258 address = ipaddress.IPv6Address(instance) 

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

260 

261 

262with suppress(ImportError): 

263 from fqdn import FQDN 

264 

265 @_checks_drafts( 

266 draft3="host-name", 

267 draft4="hostname", 

268 draft6="hostname", 

269 draft7="hostname", 

270 draft201909="hostname", 

271 draft202012="hostname", 

272 ) 

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

274 if not isinstance(instance, str): 

275 return True 

276 return FQDN(instance).is_valid 

277 

278 

279with suppress(ImportError): 

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

281 import idna 

282 

283 @_checks_drafts( 

284 draft7="idn-hostname", 

285 draft201909="idn-hostname", 

286 draft202012="idn-hostname", 

287 raises=(idna.IDNAError, UnicodeError), 

288 ) 

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

290 if not isinstance(instance, str): 

291 return True 

292 idna.encode(instance) 

293 return True 

294 

295 

296try: 

297 import rfc3987 

298except ImportError: 

299 with suppress(ImportError): 

300 from rfc3986_validator import validate_rfc3986 

301 

302 @_checks_drafts(name="uri") 

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

304 if not isinstance(instance, str): 

305 return True 

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

307 

308 @_checks_drafts( 

309 draft6="uri-reference", 

310 draft7="uri-reference", 

311 draft201909="uri-reference", 

312 draft202012="uri-reference", 

313 raises=ValueError, 

314 ) 

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

316 if not isinstance(instance, str): 

317 return True 

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

319 

320else: 

321 

322 @_checks_drafts( 

323 draft7="iri", 

324 draft201909="iri", 

325 draft202012="iri", 

326 raises=ValueError, 

327 ) 

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

329 if not isinstance(instance, str): 

330 return True 

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

332 

333 @_checks_drafts( 

334 draft7="iri-reference", 

335 draft201909="iri-reference", 

336 draft202012="iri-reference", 

337 raises=ValueError, 

338 ) 

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

340 if not isinstance(instance, str): 

341 return True 

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

343 

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

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

346 if not isinstance(instance, str): 

347 return True 

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

349 

350 @_checks_drafts( 

351 draft6="uri-reference", 

352 draft7="uri-reference", 

353 draft201909="uri-reference", 

354 draft202012="uri-reference", 

355 raises=ValueError, 

356 ) 

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

358 if not isinstance(instance, str): 

359 return True 

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

361 

362 

363with suppress(ImportError): 

364 from rfc3339_validator import validate_rfc3339 

365 

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

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

368 if not isinstance(instance, str): 

369 return True 

370 return validate_rfc3339(instance.upper()) 

371 

372 @_checks_drafts( 

373 draft7="time", 

374 draft201909="time", 

375 draft202012="time", 

376 ) 

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

378 if not isinstance(instance, str): 

379 return True 

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

381 

382 

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

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

385 if not isinstance(instance, str): 

386 return True 

387 return bool(re.compile(instance)) 

388 

389 

390@_checks_drafts( 

391 draft3="date", 

392 draft7="date", 

393 draft201909="date", 

394 draft202012="date", 

395 raises=ValueError, 

396) 

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

398 if not isinstance(instance, str): 

399 return True 

400 return bool(instance.isascii() and datetime.date.fromisoformat(instance)) 

401 

402 

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

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

405 if not isinstance(instance, str): 

406 return True 

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

408 

409 

410with suppress(ImportError): 

411 from webcolors import CSS21_NAMES_TO_HEX 

412 import webcolors 

413 

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

415 return webcolors.normalize_hex(instance) 

416 

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

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

419 if ( 

420 not isinstance(instance, str) 

421 or instance.lower() in CSS21_NAMES_TO_HEX 

422 ): 

423 return True 

424 return is_css_color_code(instance) 

425 

426 

427with suppress(ImportError): 

428 import jsonpointer 

429 

430 @_checks_drafts( 

431 draft6="json-pointer", 

432 draft7="json-pointer", 

433 draft201909="json-pointer", 

434 draft202012="json-pointer", 

435 raises=jsonpointer.JsonPointerException, 

436 ) 

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

438 if not isinstance(instance, str): 

439 return True 

440 return bool(jsonpointer.JsonPointer(instance)) 

441 

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

443 # needs to go either into jsonpointer (pending 

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

445 # into a new external library. 

446 @_checks_drafts( 

447 draft7="relative-json-pointer", 

448 draft201909="relative-json-pointer", 

449 draft202012="relative-json-pointer", 

450 raises=jsonpointer.JsonPointerException, 

451 ) 

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

453 # Definition taken from: 

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

455 if not isinstance(instance, str): 

456 return True 

457 if not instance: 

458 return False 

459 

460 non_negative_integer, rest = [], "" 

461 for i, character in enumerate(instance): 

462 if character.isdigit(): 

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

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

465 return False 

466 

467 non_negative_integer.append(character) 

468 continue 

469 

470 if not non_negative_integer: 

471 return False 

472 

473 rest = instance[i:] 

474 break 

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

476 

477 

478with suppress(ImportError): 

479 import uri_template 

480 

481 @_checks_drafts( 

482 draft6="uri-template", 

483 draft7="uri-template", 

484 draft201909="uri-template", 

485 draft202012="uri-template", 

486 ) 

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

488 if not isinstance(instance, str): 

489 return True 

490 return uri_template.validate(instance) 

491 

492 

493with suppress(ImportError): 

494 import isoduration 

495 

496 @_checks_drafts( 

497 draft201909="duration", 

498 draft202012="duration", 

499 raises=isoduration.DurationParsingException, 

500 ) 

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

502 if not isinstance(instance, str): 

503 return True 

504 isoduration.parse_duration(instance) 

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

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

507 

508 

509@_checks_drafts( 

510 draft201909="uuid", 

511 draft202012="uuid", 

512 raises=ValueError, 

513) 

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

515 if not isinstance(instance, str): 

516 return True 

517 UUID(instance) 

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