Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add context argument to Port validator method #141

Merged
merged 2 commits into from
Jan 9, 2020
Merged
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
182 changes: 56 additions & 126 deletions plumpy/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@
import json
import logging
import six
import warnings

from plumpy.utils import is_mutable_property, type_check

if six.PY2:
import collections
from inspect import getargspec as get_arg_spec
else:
import collections.abc as collections
from inspect import getfullargspec as get_arg_spec

_LOGGER = logging.getLogger(__name__)
UNSPECIFIED = ()

__all__ = ['UNSPECIFIED', 'ValueSpec', 'PortValidationError', 'Port', 'InputPort', 'OutputPort']
__all__ = ['UNSPECIFIED', 'PortValidationError', 'Port', 'InputPort', 'OutputPort']


VALIDATOR_SIGNATURE_DEPRECATION_WARNING = """the validator `{}` has a signature that only takes a single argument.
This has been deprecated and the new signature is `validator(value, port)` where the `port` argument will be the
port instance to which the validator has been assigned."""


class PortValidationError(Exception):
Expand Down Expand Up @@ -55,7 +63,8 @@ def port(self):
return self._port


class ValueSpec(object):
@six.add_metaclass(abc.ABCMeta)
class Port(object):
"""
Specifications relating to a general input/output value including
properties like whether it is required, valid types, the help string, etc.
Expand All @@ -69,19 +78,17 @@ def __init__(self, name, valid_type=None, help=None, required=True, validator=No
self._validator = validator

def __str__(self):
"""
Get the string representing this value specification
"""Get the string representing this port.

:return: the string representation
:rtype: str
"""
return json.dumps(self.get_description())

def get_description(self):
"""
Return a description of the ValueSpec, which will be a dictionary of its attributes
"""Return a description of the Port, which will be a dictionary of its attributes

:returns: a dictionary of the stringified ValueSpec attributes
:returns: a dictionary of the stringified Port attributes
:rtype: dict
"""
description = {
Expand All @@ -102,178 +109,78 @@ def name(self):

@property
def valid_type(self):
return self._valid_type

@valid_type.setter
def valid_type(self, valid_type):
self._valid_type = valid_type

@property
def help(self):
return self._help

@help.setter
def help(self, help): # pylint: disable=redefined-builtin
self._help = help

@property
def required(self):
return self._required

@required.setter
def required(self, required):
self._required = required

@property
def validator(self):
return self._validator

@validator.setter
def validator(self, validator):
self._validator = validator

def validate(self, value):
"""
Validate the value against this specification

:param value: the value to validate
:type value: typing.Any
:return: if validation fails, returns a string containing the error message, otherwise None
:rtype: typing.Optional[str]
"""
if value is UNSPECIFIED:
if self._required:
return "required value was not provided for '{}'".format(self.name)
else:
if self._valid_type is not None and not isinstance(value, self._valid_type):
msg = "value '{}' is not of the right type. Got '{}', expected '{}'".format(
self.name, type(value), self._valid_type)
return msg

if self._validator is not None:
result = self._validator(value)
if result is not None:
assert isinstance(result, str), "Validator returned non string type"
return result

return


@six.add_metaclass(abc.ABCMeta)
class Port(object):
"""
Specifications relating to a general input/output value including
properties like whether it is required, valid types, the help string, etc.
"""

def __init__(self, name, valid_type=None, help=None, required=True, validator=None):
self._value_spec = ValueSpec(name, valid_type, help, required, validator)

def __str__(self):
"""
Get the string representing this value specification

:return: the string representation
:rtype: str
"""
return str(self._value_spec)

def get_description(self):
"""
Return a description of the ValueSpec, which will be a dictionary of its attributes

:returns: a dictionary of the stringified ValueSpec attributes
:rtype: dict
"""
return self._value_spec.get_description()

@property
def name(self):
return self._value_spec.name

@property
def valid_type(self):
"""
Get the valid value type for this port if one is specified
"""Get the valid value type for this port if one is specified

:return: the value value type
:rtype: type
"""
return self._value_spec.valid_type
return self._valid_type

@valid_type.setter
def valid_type(self, valid_type):
"""
Set the valid value type for this port
"""Set the valid value type for this port

:param valid_type: the value valid type
:type valid_type: type
"""
self._value_spec.valid_type = valid_type
self._valid_type = valid_type

@property
def help(self):
"""
Get the help string for this port
"""Get the help string for this port

:return: the help string
:rtype: str
"""
return self._value_spec.help
return self._help

@help.setter
def help(self, help):
"""
Set the help string for this port
"""Set the help string for this port

:param help: the help string
:type help: str
"""
self._value_spec.help = help
self._help = help

@property
def required(self):
"""
Is this port required?
"""Is this port required?

:return: True if required, False otherwise
:rtype: bool
"""
return self._value_spec.required
return self._required

@required.setter
def required(self, required):
"""
Set whether this port is required or not
"""Set whether this port is required or not

:return: True if required, False otherwise
:type required: bool
"""
self._value_spec.required = required
self._required = required

@property
def validator(self):
"""
Get the validator for this port
"""Get the validator for this port

:return: the validator
:rtype: typing.Callable[[typing.Any], typing.Tuple[bool, typing.Optional[str]]]
"""
return self._value_spec.validator
return self._validator

@validator.setter
def validator(self, validator):
"""
Set the validator for this port
"""Set the validator for this port

:param validator: a validator function
:type validator: typing.Callable[[typing.Any], typing.Tuple[bool, typing.Optional[str]]]
"""
self._value_spec.validator = validator
self._validator = validator

def validate(self, value, breadcrumbs=()):
"""
Validate a value to see if it is valid for this port
"""Validate a value to see if it is valid for this port

:param value: the value to check
:type value: typing.Any
Expand All @@ -282,7 +189,25 @@ def validate(self, value, breadcrumbs=()):
:return: None or tuple containing 0: error string 1: tuple of breadcrumb strings to where the validation failed
:rtype: typing.Optional[ValidationError]
"""
validation_error = self._value_spec.validate(value)
validation_error = None

if value is UNSPECIFIED and self._required:
validation_error = "required value was not provided for '{}'".format(self.name)
elif value is not UNSPECIFIED and self._valid_type is not None and not isinstance(value, self._valid_type):
validation_error = "value '{}' is not of the right type. Got '{}', expected '{}'".format(
self.name, type(value), self._valid_type)

if not validation_error and self._validator is not None:
spec = get_arg_spec(self.validator)
if len(spec[0]) == 1:
warnings.warn(VALIDATOR_SIGNATURE_DEPRECATION_WARNING.format(self.validator.__name__))
result = self.validator(value)
else:
result = self.validator(value, self)
if result is not None:
assert isinstance(result, str), "Validator returned non string type"
validation_error = result

if validation_error:
breadcrumbs += (self.name,)
return PortValidationError(validation_error, breadcrumbs_to_port(breadcrumbs))
Expand Down Expand Up @@ -694,7 +619,12 @@ def validate(self, port_values=None, breadcrumbs=()):

# Validate the validator after the ports themselves, as it most likely will rely on the port values
if self.validator is not None:
message = self.validator(port_values_clone)
spec = get_arg_spec(self.validator)
if len(spec[0]) == 1:
warnings.warn(VALIDATOR_SIGNATURE_DEPRECATION_WARNING.format(self.validator.__name__))
message = self.validator(port_values_clone)
else:
message = self.validator(port_values_clone, self)
if message is not None:
assert isinstance(message, str), \
"Validator returned something other than None or str: '{}'".format(type(message))
Expand Down
13 changes: 4 additions & 9 deletions test/test_expose.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TestExposeProcess(utils.TestCaseWithLoop):
def setUp(self):
super(TestExposeProcess, self).setUp()

def validator_function(input):
def validator_function(input, port):
pass

class BaseNamespaceProcess(NewLoopProcess):
Expand Down Expand Up @@ -72,12 +72,7 @@ def check_namespace_properties(self, process_left, namespace_left, process_right
port_namespace_left.__dict__.pop('_ports', None)
port_namespace_right.__dict__.pop('_ports', None)

# The `_value_spec` is a nested dictionary so should be compared explicitly separately
value_spec_left = port_namespace_left._value_spec
value_spec_right = port_namespace_right._value_spec

self.assertEqual(port_namespace_left.__dict__, port_namespace_right.__dict__)
self.assertEqual(value_spec_left.__dict__, value_spec_right.__dict__)

def test_expose_nested_namespace(self):
"""Test that expose_inputs can create nested namespaces while maintaining own ports."""
Expand Down Expand Up @@ -188,7 +183,7 @@ def test_expose_ports_top_level(self):
properties with that of the exposed process
"""

def validator_function(input):
def validator_function(input, port):
pass

# Define child process with all mutable properties of the inputs PortNamespace to a non-default value
Expand Down Expand Up @@ -235,7 +230,7 @@ def test_expose_ports_top_level_override(self):
namespace_options will be the end-all-be-all
"""

def validator_function(input):
def validator_function(input, port):
pass

# Define child process with all mutable properties of the inputs PortNamespace to a non-default value
Expand Down Expand Up @@ -288,7 +283,7 @@ def test_expose_ports_namespace(self):
namespace with the properties of the exposed port namespace
"""

def validator_function(input):
def validator_function(input, port):
pass

# Define child process with all mutable properties of the inputs PortNamespace to a non-default value
Expand Down
Loading