Source code for schrodinger.ui.qt.appframework2.validation

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__()