Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions frictionless/schema/__spec__/field/test_constraints.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import date

import pytest

from frictionless import Field
Expand Down Expand Up @@ -131,6 +133,50 @@ def test_field_constraint_field_type(constraints, type, valid):
assert not valid


@pytest.mark.parametrize(
"name", ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]
)
@pytest.mark.parametrize(
"type, value",
[
("integer", "abc"),
("integer", {}),
("number", "abc"),
("date", "not-a-date"),
("datetime", "not-a-datetime"),
("time", "not-a-time"),
("year", "abc"),
("yearmonth", "abc"),
],
)
def test_field_bound_constraint_invalid_value(name, type, value):
descriptor = {"name": "field", "type": type, "constraints": {name: value}}
with pytest.raises(FrictionlessException) as excinfo:
Field.from_descriptor(descriptor)
reasons = excinfo.value.reasons
assert [reason.type for reason in reasons] == ["field-error"]
assert (
reasons[0].note == f'constraint "{name}" value "{value}" is not a valid "{type}"'
)


def test_field_bound_constraint_value_read_with_field_options():
field = Field.from_descriptor(
{
"name": "field",
"type": "date",
"format": "%d/%m/%Y",
"constraints": {"minimum": "01/02/2020"},
}
)
read = field.read_cell
assert read("01/02/2020") == (date(2020, 2, 1), None)
assert read("31/01/2020") == (
date(2020, 1, 31),
{"minimum": 'constraint "minimum" is "01/02/2020"'},
)


def test_field_read_cell_required():
field = Field.from_descriptor(
{
Expand Down
23 changes: 23 additions & 0 deletions frictionless/schema/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,29 @@ def metadata_validate( # type: ignore
note=f'constraint "{name}" requires datapackage v2'
)

# Bound constraints
# They are cast with the field's value reader when reading cells, so a
# value that cannot be cast would silently disable the check.
constraints = descriptor.get("constraints", {})
bounds = [
name
for name in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum")
if constraints.get(name) is not None and name in cls.supported_constraints
]
if bounds:
Class = system.select_field_class(descriptor.get("type"))
field_descriptor = {
key: value
for key, value in descriptor.items()
if key not in ["constraints", "example"]
}
field = Class.from_descriptor(field_descriptor, allow_invalid=True)
value_reader = field.create_value_reader()
for name in bounds:
if value_reader(constraints[name]) is None:
note = f'constraint "{name}" value "{constraints[name]}" is not a valid "{field.type}"'
yield errors.FieldError(note=note)

# Examples
example = descriptor.get("example")
if example:
Expand Down