Skip to content

Commit b1c1d00

Browse files
authored
Merge pull request #958 from sirosen/bool-format-checks
Type annotate format checker methods
2 parents fd3a457 + ded065d commit b1c1d00

File tree

4 files changed

+67
-45
lines changed

4 files changed

+67
-45
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"sphinx.ext.intersphinx",
2626
"sphinx.ext.napoleon",
2727
"sphinx.ext.viewcode",
28+
"sphinx_autodoc_typehints",
2829
"sphinxcontrib.spelling",
2930
"jsonschema_role",
3031
]

docs/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ file:.#egg=jsonschema
22
furo
33
lxml
44
sphinx
5+
sphinx-autodoc-typehints
56
sphinxcontrib-spelling

docs/requirements.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jinja2==3.1.2
2828
# via sphinx
2929
file:.#egg=jsonschema
3030
# via -r docs/requirements.in
31-
lxml==4.8.0
31+
lxml==4.9.0
3232
# via -r docs/requirements.in
3333
markupsafe==2.1.1
3434
# via jinja2
@@ -56,7 +56,10 @@ sphinx==4.5.0
5656
# via
5757
# -r docs/requirements.in
5858
# furo
59+
# sphinx-autodoc-typehints
5960
# sphinxcontrib-spelling
61+
sphinx-autodoc-typehints==1.18.1
62+
# via -r docs/requirements.in
6063
sphinxcontrib-applehelp==1.0.2
6164
# via sphinx
6265
sphinxcontrib-devhelp==1.0.2
@@ -69,7 +72,7 @@ sphinxcontrib-qthelp==1.0.3
6972
# via sphinx
7073
sphinxcontrib-serializinghtml==1.1.5
7174
# via sphinx
72-
sphinxcontrib-spelling==7.4.1
75+
sphinxcontrib-spelling==7.5.0
7376
# via -r docs/requirements.in
7477
urllib3==1.26.9
7578
# via requests

jsonschema/_format.py

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99

1010
from jsonschema.exceptions import FormatError
1111

12+
_FormatCheckCallable = typing.Callable[[object], bool]
13+
_F = typing.TypeVar("_F", bound=_FormatCheckCallable)
14+
_RaisesType = typing.Union[
15+
typing.Type[Exception], typing.Tuple[typing.Type[Exception], ...],
16+
]
17+
1218

1319
class FormatChecker(object):
1420
"""
@@ -35,13 +41,10 @@ class FormatChecker(object):
3541

3642
checkers: dict[
3743
str,
38-
tuple[
39-
typing.Callable[[typing.Any], bool],
40-
Exception | tuple[Exception, ...],
41-
],
44+
tuple[_FormatCheckCallable, _RaisesType],
4245
] = {}
4346

44-
def __init__(self, formats=None):
47+
def __init__(self, formats: typing.Iterable[str] | None = None):
4548
if formats is None:
4649
self.checkers = self.checkers.copy()
4750
else:
@@ -50,7 +53,9 @@ def __init__(self, formats=None):
5053
def __repr__(self):
5154
return "<FormatChecker checkers={}>".format(sorted(self.checkers))
5255

53-
def checks(self, format, raises=()):
56+
def checks(
57+
self, format: str, raises: _RaisesType = (),
58+
) -> typing.Callable[[_F], _F]:
5459
"""
5560
Register a decorated function as validating a new format.
5661
@@ -70,14 +75,23 @@ def checks(self, format, raises=()):
7075
resulting validation error.
7176
"""
7277

73-
def _checks(func):
78+
def _checks(func: _F) -> _F:
7479
self.checkers[format] = (func, raises)
7580
return func
81+
7682
return _checks
7783

78-
cls_checks = classmethod(checks)
84+
@classmethod
85+
def cls_checks(
86+
cls, format: str, raises: _RaisesType = (),
87+
) -> typing.Callable[[_F], _F]:
88+
def _checks(func: _F) -> _F:
89+
cls.checkers[format] = (func, raises)
90+
return func
91+
92+
return _checks
7993

80-
def check(self, instance, format):
94+
def check(self, instance: object, format: str) -> None:
8195
"""
8296
Check whether the instance conforms to the given format.
8397
@@ -109,7 +123,7 @@ def check(self, instance, format):
109123
if not result:
110124
raise FormatError(f"{instance!r} is not a {format!r}", cause=cause)
111125

112-
def conforms(self, instance, format):
126+
def conforms(self, instance: object, format: str) -> bool:
113127
"""
114128
Check whether the instance conforms to the given format.
115129
@@ -143,7 +157,7 @@ def conforms(self, instance, format):
143157
draft201909_format_checker = FormatChecker()
144158
draft202012_format_checker = FormatChecker()
145159

146-
_draft_checkers = dict(
160+
_draft_checkers: dict[str, FormatChecker] = dict(
147161
draft3=draft3_format_checker,
148162
draft4=draft4_format_checker,
149163
draft6=draft6_format_checker,
@@ -162,15 +176,15 @@ def _checks_drafts(
162176
draft201909=None,
163177
draft202012=None,
164178
raises=(),
165-
):
179+
) -> typing.Callable[[_F], _F]:
166180
draft3 = draft3 or name
167181
draft4 = draft4 or name
168182
draft6 = draft6 or name
169183
draft7 = draft7 or name
170184
draft201909 = draft201909 or name
171185
draft202012 = draft202012 or name
172186

173-
def wrap(func):
187+
def wrap(func: _F) -> _F:
174188
if draft3:
175189
func = _draft_checkers["draft3"].checks(draft3, raises)(func)
176190
if draft4:
@@ -195,12 +209,13 @@ def wrap(func):
195209
raises,
196210
)(func)
197211
return func
212+
198213
return wrap
199214

200215

201216
@_checks_drafts(name="idn-email")
202217
@_checks_drafts(name="email")
203-
def is_email(instance):
218+
def is_email(instance: object) -> bool:
204219
if not isinstance(instance, str):
205220
return True
206221
return "@" in instance
@@ -215,14 +230,14 @@ def is_email(instance):
215230
draft202012="ipv4",
216231
raises=ipaddress.AddressValueError,
217232
)
218-
def is_ipv4(instance):
233+
def is_ipv4(instance: object) -> bool:
219234
if not isinstance(instance, str):
220235
return True
221-
return ipaddress.IPv4Address(instance)
236+
return bool(ipaddress.IPv4Address(instance))
222237

223238

224239
@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError)
225-
def is_ipv6(instance):
240+
def is_ipv6(instance: object) -> bool:
226241
if not isinstance(instance, str):
227242
return True
228243
address = ipaddress.IPv6Address(instance)
@@ -240,7 +255,7 @@ def is_ipv6(instance):
240255
draft201909="hostname",
241256
draft202012="hostname",
242257
)
243-
def is_host_name(instance):
258+
def is_host_name(instance: object) -> bool:
244259
if not isinstance(instance, str):
245260
return True
246261
return FQDN(instance).is_valid
@@ -256,7 +271,7 @@ def is_host_name(instance):
256271
draft202012="idn-hostname",
257272
raises=(idna.IDNAError, UnicodeError),
258273
)
259-
def is_idn_host_name(instance):
274+
def is_idn_host_name(instance: object) -> bool:
260275
if not isinstance(instance, str):
261276
return True
262277
idna.encode(instance)
@@ -270,7 +285,7 @@ def is_idn_host_name(instance):
270285
from rfc3986_validator import validate_rfc3986
271286

272287
@_checks_drafts(name="uri")
273-
def is_uri(instance):
288+
def is_uri(instance: object) -> bool:
274289
if not isinstance(instance, str):
275290
return True
276291
return validate_rfc3986(instance, rule="URI")
@@ -282,19 +297,20 @@ def is_uri(instance):
282297
draft202012="uri-reference",
283298
raises=ValueError,
284299
)
285-
def is_uri_reference(instance):
300+
def is_uri_reference(instance: object) -> bool:
286301
if not isinstance(instance, str):
287302
return True
288303
return validate_rfc3986(instance, rule="URI_reference")
289304

290305
else:
306+
291307
@_checks_drafts(
292308
draft7="iri",
293309
draft201909="iri",
294310
draft202012="iri",
295311
raises=ValueError,
296312
)
297-
def is_iri(instance):
313+
def is_iri(instance: object) -> bool:
298314
if not isinstance(instance, str):
299315
return True
300316
return rfc3987.parse(instance, rule="IRI")
@@ -305,13 +321,13 @@ def is_iri(instance):
305321
draft202012="iri-reference",
306322
raises=ValueError,
307323
)
308-
def is_iri_reference(instance):
324+
def is_iri_reference(instance: object) -> bool:
309325
if not isinstance(instance, str):
310326
return True
311327
return rfc3987.parse(instance, rule="IRI_reference")
312328

313329
@_checks_drafts(name="uri", raises=ValueError)
314-
def is_uri(instance):
330+
def is_uri(instance: object) -> bool:
315331
if not isinstance(instance, str):
316332
return True
317333
return rfc3987.parse(instance, rule="URI")
@@ -323,16 +339,17 @@ def is_uri(instance):
323339
draft202012="uri-reference",
324340
raises=ValueError,
325341
)
326-
def is_uri_reference(instance):
342+
def is_uri_reference(instance: object) -> bool:
327343
if not isinstance(instance, str):
328344
return True
329345
return rfc3987.parse(instance, rule="URI_reference")
330346

347+
331348
with suppress(ImportError):
332349
from rfc3339_validator import validate_rfc3339
333350

334351
@_checks_drafts(name="date-time")
335-
def is_datetime(instance):
352+
def is_datetime(instance: object) -> bool:
336353
if not isinstance(instance, str):
337354
return True
338355
return validate_rfc3339(instance.upper())
@@ -342,17 +359,17 @@ def is_datetime(instance):
342359
draft201909="time",
343360
draft202012="time",
344361
)
345-
def is_time(instance):
362+
def is_time(instance: object) -> bool:
346363
if not isinstance(instance, str):
347364
return True
348365
return is_datetime("1970-01-01T" + instance)
349366

350367

351368
@_checks_drafts(name="regex", raises=re.error)
352-
def is_regex(instance):
369+
def is_regex(instance: object) -> bool:
353370
if not isinstance(instance, str):
354371
return True
355-
return re.compile(instance)
372+
return bool(re.compile(instance))
356373

357374

358375
@_checks_drafts(
@@ -362,28 +379,28 @@ def is_regex(instance):
362379
draft202012="date",
363380
raises=ValueError,
364381
)
365-
def is_date(instance):
382+
def is_date(instance: object) -> bool:
366383
if not isinstance(instance, str):
367384
return True
368-
return instance.isascii() and datetime.date.fromisoformat(instance)
385+
return bool(instance.isascii() and datetime.date.fromisoformat(instance))
369386

370387

371388
@_checks_drafts(draft3="time", raises=ValueError)
372-
def is_draft3_time(instance):
389+
def is_draft3_time(instance: object) -> bool:
373390
if not isinstance(instance, str):
374391
return True
375-
return datetime.datetime.strptime(instance, "%H:%M:%S")
392+
return bool(datetime.datetime.strptime(instance, "%H:%M:%S"))
376393

377394

378395
with suppress(ImportError):
379396
from webcolors import CSS21_NAMES_TO_HEX
380397
import webcolors
381398

382-
def is_css_color_code(instance):
399+
def is_css_color_code(instance: object) -> bool:
383400
return webcolors.normalize_hex(instance)
384401

385402
@_checks_drafts(draft3="color", raises=(ValueError, TypeError))
386-
def is_css21_color(instance):
403+
def is_css21_color(instance: object) -> bool:
387404
if (
388405
not isinstance(instance, str)
389406
or instance.lower() in CSS21_NAMES_TO_HEX
@@ -402,10 +419,10 @@ def is_css21_color(instance):
402419
draft202012="json-pointer",
403420
raises=jsonpointer.JsonPointerException,
404421
)
405-
def is_json_pointer(instance):
422+
def is_json_pointer(instance: object) -> bool:
406423
if not isinstance(instance, str):
407424
return True
408-
return jsonpointer.JsonPointer(instance)
425+
return bool(jsonpointer.JsonPointer(instance))
409426

410427
# TODO: I don't want to maintain this, so it
411428
# needs to go either into jsonpointer (pending
@@ -417,7 +434,7 @@ def is_json_pointer(instance):
417434
draft202012="relative-json-pointer",
418435
raises=jsonpointer.JsonPointerException,
419436
)
420-
def is_relative_json_pointer(instance):
437+
def is_relative_json_pointer(instance: object) -> bool:
421438
# Definition taken from:
422439
# https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
423440
if not isinstance(instance, str):
@@ -437,7 +454,7 @@ def is_relative_json_pointer(instance):
437454

438455
rest = instance[i:]
439456
break
440-
return (rest == "#") or jsonpointer.JsonPointer(rest)
457+
return (rest == "#") or bool(jsonpointer.JsonPointer(rest))
441458

442459

443460
with suppress(ImportError):
@@ -449,7 +466,7 @@ def is_relative_json_pointer(instance):
449466
draft201909="uri-template",
450467
draft202012="uri-template",
451468
)
452-
def is_uri_template(instance):
469+
def is_uri_template(instance: object) -> bool:
453470
if not isinstance(instance, str):
454471
return True
455472
return uri_template.validate(instance)
@@ -463,18 +480,18 @@ def is_uri_template(instance):
463480
draft202012="duration",
464481
raises=isoduration.DurationParsingException,
465482
)
466-
def is_duration(instance):
483+
def is_duration(instance: object) -> bool:
467484
if not isinstance(instance, str):
468485
return True
469-
return isoduration.parse_duration(instance)
486+
return bool(isoduration.parse_duration(instance))
470487

471488

472489
@_checks_drafts(
473490
draft201909="uuid",
474491
draft202012="uuid",
475492
raises=ValueError,
476493
)
477-
def is_uuid(instance):
494+
def is_uuid(instance: object) -> bool:
478495
if not isinstance(instance, str):
479496
return True
480497
UUID(instance)

0 commit comments

Comments
 (0)