diff --git a/src/plumpy/ports.py b/src/plumpy/ports.py index 39c87e77..1e4c3375 100644 --- a/src/plumpy/ports.py +++ b/src/plumpy/ports.py @@ -730,18 +730,22 @@ def validate_dynamic_ports( :return: if invalid returns a string with the reason for the validation failure, otherwise None :rtype: typing.Optional[str] """ - breadcrumbs = (*breadcrumbs, self.name) - if port_values and not self.dynamic: msg = f'Unexpected ports {port_values}, for a non dynamic namespace' + return PortValidationError(msg, breadcrumbs_to_port((*breadcrumbs, self.name))) + + if self.valid_type is None: + return None + + if isinstance(port_values, dict): + for key, value in port_values.items(): + result = self.validate_dynamic_ports(value, (*breadcrumbs, self.name, key)) + if result is not None: + return result + elif not isinstance(port_values, self.valid_type): + msg = f'Invalid type {type(port_values)} for dynamic port value: expected {self.valid_type}' return PortValidationError(msg, breadcrumbs_to_port(breadcrumbs)) - if self.valid_type is not None: - valid_type = self.valid_type - for port_name, port_value in port_values.items(): - if not isinstance(port_value, valid_type): - msg = f'Invalid type {type(port_value)} for dynamic port value: expected {valid_type}' - return PortValidationError(msg, breadcrumbs_to_port(breadcrumbs + (port_name,))) return None @staticmethod diff --git a/test/test_port.py b/test/test_port.py index 029c109c..55ddbe66 100644 --- a/test/test_port.py +++ b/test/test_port.py @@ -249,9 +249,14 @@ def test_port_namespace_set_valid_type(self): self.assertIsNone(self.port_namespace.valid_type) def test_port_namespace_validate(self): - """Check that validating of sub namespaces works correctly""" + """Check that validating of sub namespaces works correctly. + + By setting a valid type on a port namespace, it automatically becomes dynamic. Port namespaces that are dynamic + should accept arbitrarily nested input and should validate, as long as all leaf values satisfy the `valid_type`. + """ port_namespace_sub = self.port_namespace.create_port_namespace('sub.space') port_namespace_sub.valid_type = int + assert port_namespace_sub.dynamic # Check that passing a non mapping type raises validation_error = self.port_namespace.validate(5) @@ -261,7 +266,12 @@ def test_port_namespace_validate(self): validation_error = self.port_namespace.validate({'sub': {'space': {'output': 5}}}) self.assertIsNone(validation_error) - # Invalid input + # Valid input: `sub.space` is dynamic, so should allow arbitrarily nested namespaces as long as the leaf values + # match the valid type, which is `int` in this example. + validation_error = self.port_namespace.validate({'sub': {'space': {'output': {'invalid': 5}}}}) + self.assertIsNone(validation_error) + + # Invalid input - the value in ``space`` is not ``int`` but a ``str`` validation_error = self.port_namespace.validate({'sub': {'space': {'output': '5'}}}) self.assertIsNotNone(validation_error)