Repository: PrefectHQ/prefect

Issue:
PIN 16: Implement `Result.validate`
## Use Case
~Blocked by #2143 and #2139~. See [PIN 16](https://github.com/PrefectHQ/prefect/pull/2134) for background. 

## Solution
Implement `Result.validate` such that
- if `Result.run_validators=True`
    - all `Result.validators` are called on the result value
    - if any fail, raise ValidationFailed signal to trigger ValidationFailed state (*see alternative)
    - if all succeed, return True (*see alternative)

## Alternatives
*Please describe any alternatives you've considered, even if you've dismissed them*
- *I may be misrepresenting the intended method signature; if this method will be called by a TaskRunner method, we may want to receive `False` instead of a raise a ValidationFailed signal to allow the TaskRunner method to construct the ValidationFailed state and return it as part of the pipeline a la https://github.com/PrefectHQ/prefect/blob/66703e5f0a2d0d035d4d48af013b87d24a7a2a56/src/prefect/engine/task_runner.py#L957-L964



Discussion:

Resolving patch:
diff --git a/src/prefect/engine/result/base.py b/src/prefect/engine/result/base.py
--- a/src/prefect/engine/result/base.py
+++ b/src/prefect/engine/result/base.py
@@ -25,6 +25,7 @@
 
 from prefect.engine.cache_validators import duration_only
 from prefect.engine.result_handlers import ResultHandler
+from prefect.utilities import logging
 
 
 class ResultInterface:
@@ -110,6 +111,7 @@ def __init__(
         self.cache_validator = cache_validator
         self.filepath_template = filepath_template
         self._rendered_filepath = None  # type: Optional[str]
+        self.logger = logging.get_logger(type(self).__name__)
 
     def store_safe_value(self) -> None:
         """
@@ -127,6 +129,33 @@ def store_safe_value(self) -> None:
                 value=value, result_handler=self.result_handler
             )
 
+    def validate(self) -> bool:
+        """
+        Run any validator functions associated with this result and return whether the result is valid or not.
+        All individual validator functions must return True for this method to return True.
+        Emits a warning log if run_validators isn't true, and proceeds to run validation functions anyway.
+
+
+        Returns:
+            - bool: whether or not the Result passed all validation functions
+        """
+        if not self.run_validators:
+            self.logger.warning(
+                "A Result's validate method has been called, but its run_validators attribute is False. "
+                "Prefect will not honor the validators without run_validators=True, so please change it "
+                "if you expect validation to occur automatically for this Result in your pipeline."
+            )
+
+        if self.validators:
+            for validation_fn in self.validators:
+                is_valid = validation_fn(self)
+                if not is_valid:
+                    # once a validator is found to be false, this result is invalid
+                    return False
+
+        # if all validators passed or we had none, this result is valid
+        return True
+
     def copy(self) -> "Result":
         """
         Return a copy of the current result object.


Test patch:
diff --git a/tests/engine/result/test_base.py b/tests/engine/result/test_base.py
--- a/tests/engine/result/test_base.py
+++ b/tests/engine/result/test_base.py
@@ -1,8 +1,11 @@
 import datetime
+import logging
 
 import cloudpickle
 import pytest
 
+from unittest.mock import MagicMock
+
 import prefect
 from prefect.engine.result import NoResult, NoResultType, Result, SafeResult
 from prefect.engine.result_handlers import (
@@ -305,3 +308,59 @@ def test_result_render_fails_on_no_template_given():
     with pytest.raises(ValueError):
         res = Result()
         res.format()
+
+
+class TestResultValidate:
+    def test_result_validate_calls_validate_functions_from_attribute(self):
+        _example_function = MagicMock(return_value=True)
+
+        r = Result(value=None, validators=[_example_function])
+        r.validate()
+
+        _example_function.assert_called_once_with(r)
+
+    def test_result_validate_returns_false_on_any_invalid(self):
+        one_false_validators_fns = [lambda r: True, lambda r: False]
+
+        r = Result(value=None, validators=one_false_validators_fns)
+        is_valid = r.validate()
+
+        assert is_valid is False
+
+    def test_result_validate_returns_true_on_none_invalid(self):
+        no_false_validators_fns = [lambda r: True, lambda r: True]
+
+        r = Result(value=None, validators=no_false_validators_fns)
+        is_valid = r.validate()
+
+        assert is_valid is True
+
+    @pytest.mark.parametrize(
+        "r", [Result(value=None), Result(value=None, validators=[])]
+    )
+    def test_result_validate_ok_when_none_provided(self, r):
+        is_valid = r.validate()
+        assert is_valid is True
+
+    def test_result_validate_raises_exceptions(self):
+        def _example_function(result):
+            raise TypeError
+
+        r = Result(value=None, validators=[_example_function])
+        with pytest.raises(TypeError):
+            r.validate()
+
+    def test_result_validate_warns_when_run_without_run_validators_flag(self, caplog):
+        _example_function = MagicMock(return_value=True)
+
+        r = Result(value=None, validators=[_example_function], run_validators=False)
+        with caplog.at_level(logging.WARNING, "prefect.Result"):
+            is_valid = r.validate()
+
+        # it should have acted normal and called the validate functions
+        _example_function.assert_called_once_with(r)
+        assert is_valid is True
+
+        # but ALSO it should published a warning log, going on about run_validators not being set
+        assert caplog.text.find("WARNING") > -1
+        assert caplog.text.find("run_validators") > -1