from unittest import mock
#=========================================================================
# Decorator functions
#=========================================================================
[docs]def cast_validation_result(result):
"""
Casts the result of a validation method into a ValidationResult instance
:param result: The result of a validation check
:type result: bool or (bool, str) or `schrodinger.ui.qt.appframework2.validation.ValidationResult`
:return: A ValidationResult instance
:rtype: `schrodinger.ui.qt.appframework2.validation.ValidationResult`
"""
if isinstance(result, ValidationResult):
return result
if isinstance(result, bool):
return ValidationResult(passed=result)
if isinstance(result, tuple):
passed, message = result
return ValidationResult(passed=passed, message=message)
return ValidationResult(bool(result))
[docs]def validator(validation_order=100):
"""
Decorator function to mark a method as a validation test and define the
order in which is should be called. Validation order is optional and
relative only to the other validation methods within a class instance.
When the decorated method is called, the original method's result is cast
to a ValidationResult. This makes it a bit more natural to test validation
objects. A ValidationResult object evaluates to True or False depending on
whether the validation succeeded or not.
"""
def setOrder(to_func):
def inner(*args, **kwargs):
result = to_func(*args, **kwargs)
return cast_validation_result(result)
inner.validation_order = validation_order
inner.is_multi_validator = False
return inner
return setOrder
[docs]def multi_validator(validation_order=100):
"""
Use this decorator to mark methods that need to return multiple validation
results. This may be a list of validator return values (e.g. bool or (bool,
str)) or may be yielded from a generator.
"""
def setOrder(to_func):
def inner(*args, **kwargs):
for result in to_func(*args, **kwargs):
result = cast_validation_result(result)
yield result
inner.validation_order = validation_order
inner.is_multi_validator = True
return inner
return setOrder
[docs]def add_validator(obj, validator_func, validation_order=100):
"""
Function that allows validators to be added dynamically at runtime.
See the `validator` decorator for more information.
.. NOTE::
The `validator_func` is not bound to `obj`. If you want it to behave
like a method (ie take in `obj` as its first argument), then the
`validator_func` should be cast using `types.MethodType(obj, validator_func)`.
.. WARNING::
The validator is added as an attribute to `obj` using the name of
the validator function. This means that any attributes or methods with
the same name will be overwritten.
:param obj: An instance of a class that subclasses ValidationMixin.
:type obj: object
:param validator_func: A function to use as a validator. The function
should return a bool or a tuple consisting of a bool and a string.
:type validator_func: callable
:param validation_order: The order to call `validator_func`. This number
is used relative to other validators' validation_order values.
:type validation_order: int
"""
wrapped_validator = validator(
validation_order=validation_order)(validator_func)
setattr(obj, validator_func.__name__, wrapped_validator)
[docs]def remove_validator(obj, validator_func):
"""
This function is the inverse of `add_validator`. Note that this should only
be used with validators that were added wtih `add_validator`, not validators
that were built into a class using the @validator decorator.
:param obj: An instance of a class that subclasses ValidationMixin.
:type obj: object
:param validator_func: A function that's been added as a validator to `obj`.
:type validator_func: callable
"""
delattr(obj, validator_func.__name__)
#=========================================================================
# Main Validation Class
#=========================================================================
[docs]class ValidationMixin(object):
"""
This mix-in provides validation functionality to other classes, including
the ability to designate methods as validation methods, which will be called
when the validate method is invoked. These methods can be designated using
the `validator` decorator.
To enable validation functionality in a class, this mix-in can be inherited
as an additional parent class. It expects to be inherited by a class that
has defined `error` and `question` methods, e.g. a class that also
inherits from `widgetmixins.MessageBoxMixin`.
"""
[docs] def runValidation(self,
silent=False,
validate_children=True,
stop_on_fail=True):
"""
Runs validation and reports the results (unless run silently).
:param silent: run without any reporting (i.e. error messages to the
user). This is useful if we want to programmatically test validity.
Changes return value of this method from `ValidationResults` to a
boolean.
:type silent: bool
:param validate_children: run validation on all child objects. See
`_validateChildren` for documentation on what this entails.
:type validate_children: bool
:param stop_on_fail: stop validation when first failure is encountered
:type stop_on_fail: bool
:return: if silent is False, returns the validation results. If silent
is True, returns a boolean generated by `reportValidation`.
:rtype: `ValidationResults` or bool
"""
results = self._validate(validate_children, stop_on_fail)
if silent:
return results
return self.reportValidation(results)
[docs] def reportValidation(self, results):
"""
Present validation messages to the user. This is an implmentation of
the `ValidationMixin` interface and does not need to be called
directly.
This method assumes that `error` and `question` methods have been
defined in the subclass, as in e.g. `widget_mixins.MessageBoxMixin`.
:param results: Set of validation results generated by `validate`
:type results: `validation.ValidationResults`
:return: if True, there were no validation errors and the user decided
to continue despite any warnings. If False, there was at least one
validation error or the user decided to abort when faced with a warning.
"""
abort = False
for result in results:
if not result:
abort = True
message = result.message
if not message:
message = ('Validation failed. Check settings and try'
' again.')
self.error(message)
break
else:
if result.message:
cont = self.question(result.message,
button1='Continue',
title='Warning')
if not cont:
abort = True
break
return not abort
def _validate(self, validate_children=True, stop_on_fail=True):
"""
Run all validators defined as methods of self. Validation methods are
designated by the `validator` decorator.
:param validate_children: run validation on all child objects. See
`_validateChildren` for documentation on what this entails.
:type validate_children: bool
:param stop_on_fail: If True, stops validation on first failure.
:type stop_on_fail: bool
:param results: Set of validation results
:type results: `validation.ValidationResults`
"""
results = ValidationResults()
if validate_children:
results.add(self._validateChildren(stop_on_fail))
if not results and stop_on_fail:
return results
results.extend(validate_obj(self, stop_on_fail=stop_on_fail))
return results
def _validateChildren(self, stop_on_fail=True):
"""
Sequentially validates each of the children of self by attempting to
call child._validate() on all objects returned by a call to
self.children().
:param stop_on_fail: If True, stops validation on first failure.
:type stop_on_fail: bool
"""
results = ValidationResults()
try:
children = self.children()
except AttributeError:
children = []
for child in children:
try:
results.add(child._validate(stop_on_fail))
except AttributeError:
pass
if not results and stop_on_fail:
break
return results
[docs]def find_validators(obj):
"""
Searches through the methods on an object and finds methods that have been
decorated with the @validator decorator.
:param obj: the object containing validator methods
:type obj: object
:return: the validator methods, sorted by validation_order
:rtype: list of callable
"""
validators = []
for attribute in dir(obj):
method = getattr(obj, attribute)
# Mock objects must be explicitly ignored
if isinstance(method, mock.Mock):
continue
if hasattr(method, 'validation_order'):
validators.append(method)
validators.sort(key=lambda method: method.validation_order)
return validators
[docs]def validate_obj(obj, stop_on_fail=False):
"""
Runs validation on an object containing validator methods. Will not
recursively validate child objects.
:param obj: the object to be validated.
:type obj: object
:param stop_on_fail: whether to stop validation at the first failure
:type stop_on_fail: bool
:return: the validation results
:rtype: ValidationResults
"""
results = ValidationResults()
validators = find_validators(obj)
abort = False
for validate_method in validators:
if validate_method.is_multi_validator:
method_results = validate_method()
else:
result = validate_method()
method_results = [result]
for result in method_results:
results.add(result)
if not result and stop_on_fail:
abort = True
break
if abort:
break
return results
#=========================================================================
# Validation Result Handling
#=========================================================================
[docs]class ValidationResult(object):
"""
A class to store a single validation result.
"""
[docs] def __init__(self, passed=True, message=None):
"""
If passed is True and there is a message, this is generally interpreted
as a warning.
:param passed: Whether validation passed
:type passed: bool
:param message: Message to present to user, if applicable
:type message: str
"""
self.passed = passed
self.message = message
def __bool__(self):
"""
:return: Whether the validation passed
:rtype: bool
"""
return self.passed
def __str__(self):
if self.passed:
if not self.message:
return 'Passed'
else:
return 'WARNING: %s' % self.message
else:
if not self.message:
return 'FAILED'
else:
return 'FAILED: %s' % self.message
def __repr__(self):
return self.__str__()
def __iter__(self):
"""
Iterate through the contents of the ValidationResult instance
Allows us to treat a ValidationResult instance in the same way as a
tuple
"""
return iter([self.passed, self.message])
def __getitem__(self, index):
"""
Return the item at the specified index: 0 for passed and 1 for message
Allows us to treat a ValidationResult instance in the same way as a
tuple
:param index: The index of the item (either 0 or 1)
:type index: int
"""
return [self.passed, self.message][index]
[docs]class ValidationResults(list, object):
"""
A class to store validation results. This class can store multiple results,
and has methods for iterating through the results.
Inherits from object in order to fix issues from python 3 transition
"""
[docs] def add(self, result):
"""
Adds another result or list of validation results to the list. A list
of results must be of the ValidationResults type. Single results to add
can be given in several ways.
A ValidationResult can be added.
A tuple consisting of a (bool, message) will be converted into a
ValidationResult.
Any other value that has a bool value will be converted into a
ValidationResult with no message.
:param result: The result(s) to be added
:type result: ValidationResult, ValidationResults, tuple, or any type
with a truth value.
"""
if isinstance(result, ValidationResults):
self.extend(result)
return
validation_result = cast_validation_result(result)
self.append(validation_result)
def __bool__(self):
"""
Truth-value of a ValidationResults instance. Note that an empty list
evaluates True. If the list of results contain any validation failures,
the list evaluates False.
"""
return all(self)
def __str__(self):
return '\n'.join(str(result) for result in self)
def __repr__(self):
return self.__str__()