Skip to content

Commit

Permalink
"format" supports all types by default
Browse files Browse the repository at this point in the history
Previously, formats could set applicable types, but in the decorator
the types were defaulted to ("string",), contrary to the behavior of
keywords in general and the "format" keyword class in particular.

Since the tutorial example, unit tests, and the one format
validator all check the instance type before proceeding, it looked
very much like format validator functions were expected to do
their own type checking.

This change aligns the default type applicability of formats with
that of keywords, and updates all implementations, tests, and
examples to rely on the decorator parameter to handle the type check.
  • Loading branch information
handrews committed May 22, 2023
1 parent 445a4e7 commit 2269eaf
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 18 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

v0.10.4 (in development)
------------------------
Bug Fixes:

* "format" applies to all types


v0.10.3 (2023-05-21)
--------------------
Bug Fixes:
Expand Down
10 changes: 4 additions & 6 deletions examples/format_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@


# register an 'ipv4' format validator
@format_validator('ipv4')
@format_validator('ipv4', instance_types=('string',))
def validate_ipv4(value: str) -> None:
if isinstance(value, str):
ipaddress.IPv4Address(value) # raises ValueError for an invalid IPv4 address
ipaddress.IPv4Address(value) # raises ValueError for an invalid IPv4 address


# register an 'ipv6' format validator
@format_validator('ipv6')
@format_validator('ipv6', instance_types=('string',))
def validate_ipv6(value: str) -> None:
if isinstance(value, str):
ipaddress.IPv6Address(value) # raises ValueError for an invalid IPv6 address
ipaddress.IPv6Address(value) # raises ValueError for an invalid IPv6 address


# initialize the catalog, with JSON Schema 2020-12 vocabulary support
Expand Down
7 changes: 3 additions & 4 deletions jschon/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from jschon.vocabulary.format import format_validator


@format_validator('json-pointer')
@format_validator('json-pointer', instance_types=('string',))
def validate_json_pointer(value: str) -> None:
if isinstance(value, str):
if not JSONPointer._json_pointer_re.fullmatch(value):
raise ValueError
if not JSONPointer._json_pointer_re.fullmatch(value):
raise ValueError
6 changes: 4 additions & 2 deletions jschon/vocabulary/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self, parentschema: JSONSchema, value: str):
if parentschema.catalog.is_format_enabled(value):
self.validator, self.validates_types = _format_validators[value]
else:
self.validator = None
self.validator, self.validates_types = None, set()

def evaluate(self, instance: JSON, result: Result) -> None:
result.annotate(self.json.value)
Expand Down Expand Up @@ -47,7 +47,9 @@ def evaluate(self, instance: JSON, result: Result) -> None:
def format_validator(
format_attr: str,
*,
instance_types: Tuple[str, ...] = ('string',)
instance_types: Tuple[str, ...] = (
"null", "boolean", "number", "string", "array", "object",
)
):
"""A decorator for a format validation function.
Expand Down
35 changes: 29 additions & 6 deletions tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,28 @@ def setup_validators(catalog):
"ipv4",
"ipv6",
"json-pointer",
"uint8",
)
yield
catalog._enabled_formats.clear()


@format_validator('ipv4')
@format_validator('ipv4', instance_types=('string',))
def ipv4_validator(value):
if isinstance(value, str):
ipaddress.IPv4Address(value)
ipaddress.IPv4Address(value)


@format_validator('ipv6')
@format_validator('ipv6', instance_types=('string',))
def ipv6_validator(value):
if isinstance(value, str):
ipaddress.IPv6Address(value)
ipaddress.IPv6Address(value)


@format_validator('uint8', instance_types=('number',))
def uint8_validator(value):
if value % 1:
raise ValueError(f'{value} is not an integer (uint8)')
if value < 0 or value > 255:
raise ValueError(f'{value} is out of range for uint8')


def evaluate(format_attr, instval, assert_=True):
Expand Down Expand Up @@ -88,6 +95,22 @@ def test_jsonpointer_invalid(instval):
assert result is False


@pytest.mark.parametrize('instval', (3, "1", "what"))
def test_uint8_valid(instval):
result = evaluate(
"uint8",
instval,
isinstance(instval, (int, float))
)
assert result is True


@pytest.mark.parametrize('instval', (3.001, -1, 256))
def test_uint8_invalid(instval):
result = evaluate("uint8", instval)
assert result is False


@given(instval=hs.uuids() | hs.text())
def test_uuid(instval):
# we've not registered a "uuid" validator, so the test should always pass
Expand Down

0 comments on commit 2269eaf

Please sign in to comment.