Skip to content

Commit

Permalink
Fix schema validation and add custom validators
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien-berchet committed Jan 17, 2023
1 parent cf3be33 commit e60b153
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 8 deletions.
67 changes: 63 additions & 4 deletions luigi/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,26 @@ def run(self):
$ luigi --module my_tasks MyTask --tags '{"role": "UNKNOWN_VALUE", "env": "staging"}'
Finally, the provided schema can be a custom validator:
.. code-block:: python
custom_validator = jsonschema.Draft4Validator(
schema={
"type": "object",
"patternProperties": {
".*": {"type": "string", "enum": ["web", "staging"]},
}
}
)
class MyTask(luigi.Task):
tags = luigi.DictParameter(schema=custom_validator)
def run(self):
logging.info("Find server with role: %s", self.tags['role'])
server = aws.ec2.find_my_resource(self.tags)
"""

def __init__(
Expand All @@ -1105,7 +1125,9 @@ def __init__(
"The 'jsonschema' package is not installed so the parameter can not be validated "
"even though a schema is given."
)
self.schema = schema
self.schema = None
else:
self.schema = schema
super().__init__(
*args,
**kwargs,
Expand All @@ -1117,7 +1139,12 @@ def normalize(self, value):
"""
frozen_value = recursively_freeze(value)
if self.schema is not None:
jsonschema.validate(instance=recursively_unfreeze(frozen_value), schema=self.schema)
unfrozen_value = recursively_unfreeze(frozen_value)
try:
self.schema.validate(unfrozen_value) # Validators may update the instance inplace
frozen_value = super().normalize(unfrozen_value)
except AttributeError:
jsonschema.validate(instance=unfrozen_value, schema=self.schema)
return frozen_value

def parse(self, source):
Expand Down Expand Up @@ -1212,6 +1239,31 @@ def run(self):
$ luigi --module my_tasks MyTask --numbers '[]' # must have at least 1 element
$ luigi --module my_tasks MyTask --numbers '[-999, 999]' # elements must be in [0, 10]
Finally, the provided schema can be a custom validator:
.. code-block:: python
custom_validator = jsonschema.Draft4Validator(
schema={
"type": "array",
"items": {
"type": "number",
"minimum": 0,
"maximum": 10
},
"minItems": 1
}
)
class MyTask(luigi.Task):
grades = luigi.ListParameter(schema=custom_validator)
def run(self):
sum = 0
for element in self.grades:
sum += element
avg = sum / len(self.grades)
"""

def __init__(
Expand All @@ -1225,7 +1277,9 @@ def __init__(
"The 'jsonschema' package is not installed so the parameter can not be validated "
"even though a schema is given."
)
self.schema = schema
self.schema = None
else:
self.schema = schema
super().__init__(
*args,
**kwargs,
Expand All @@ -1240,7 +1294,12 @@ def normalize(self, x):
"""
frozen_value = recursively_freeze(x)
if self.schema is not None:
jsonschema.validate(instance=recursively_unfreeze(frozen_value), schema=self.schema)
unfrozen_value = recursively_unfreeze(frozen_value)
try:
self.schema.validate(unfrozen_value) # Validators may update the instance inplace
frozen_value = super().normalize(unfrozen_value)
except AttributeError:
jsonschema.validate(instance=unfrozen_value, schema=self.schema)
return frozen_value

def parse(self, x):
Expand Down
16 changes: 16 additions & 0 deletions test/dict_parameter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# limitations under the License.
#

from jsonschema import Draft4Validator
from jsonschema.exceptions import ValidationError
from helpers import unittest, in_parse

Expand Down Expand Up @@ -113,6 +114,7 @@ def test_schema(self):
with pytest.raises(ValidationError, match=r"'UNKNOWN_VALUE' is not one of \['web', 'staging'\]"):
b.normalize({"role": "UNKNOWN_VALUE", "env": "staging"})

# Check that warnings are properly emitted
with mock.patch('luigi.parameter._JSONSCHEMA_ENABLED', False):
with pytest.warns(
UserWarning,
Expand All @@ -122,3 +124,17 @@ def test_schema(self):
)
):
luigi.ListParameter(schema={"type": "object"})

# Test with a custom validator
validator = Draft4Validator(
schema={
"type": "object",
"patternProperties": {
".*": {"type": "string", "enum": ["web", "staging"]},
},
}
)
c = luigi.DictParameter(schema=validator)
c.normalize({"role": "web", "env": "staging"})
with pytest.raises(ValidationError, match=r"'UNKNOWN_VALUE' is not one of \['web', 'staging'\]"):
c.normalize({"role": "UNKNOWN_VALUE", "env": "staging"})
24 changes: 20 additions & 4 deletions test/list_parameter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# limitations under the License.
#

from jsonschema import Draft4Validator
from jsonschema.exceptions import ValidationError
from helpers import unittest, in_parse

Expand Down Expand Up @@ -76,10 +77,7 @@ def test_schema(self):
)

# Check that the default value is validated
with pytest.raises(
ValidationError,
match=r"'INVALID_ATTRIBUTE' is not of type 'number'",
):
with pytest.raises(ValidationError, match=r"'INVALID_ATTRIBUTE' is not of type 'number'"):
a.normalize(["INVALID_ATTRIBUTE"])

# Check that empty list is not valid
Expand All @@ -100,6 +98,7 @@ def test_schema(self):
with pytest.raises(ValidationError, match="-999 is less than the minimum of 0"):
a.normalize(invalid_list_value)

# Check that warnings are properly emitted
with mock.patch('luigi.parameter._JSONSCHEMA_ENABLED', False):
with pytest.warns(
UserWarning,
Expand All @@ -109,3 +108,20 @@ def test_schema(self):
)
):
luigi.ListParameter(schema={"type": "array", "items": {"type": "number"}})

# Test with a custom validator
validator = Draft4Validator(
schema={
"type": "array",
"items": {
"type": "number",
"minimum": 0,
"maximum": 10,
},
"minItems": 1,
}
)
c = luigi.DictParameter(schema=validator)
c.normalize(valid_list)
with pytest.raises(ValidationError, match=r"'INVALID_ATTRIBUTE' is not of type 'number'",):
c.normalize(["INVALID_ATTRIBUTE"])

0 comments on commit e60b153

Please sign in to comment.