Repository: PrefectHQ/prefect

Issue:
PIN 16: Add `CustomResult` that will be used to hydrate user Results from cloud states
## Use Case
See [PIN 16](https://docs.prefect.io/core/PINs/PIN-16-Results-and-Targets.html) for more motivation on new style Result classes. 

Today, users may provide their own custom `ResultHandler` subclasses that do not serialize to Cloud; we expect to support the same for custom new style `Result` subclasses. 



## Solution
Add a `CustomResult` that wraps any custom result handler implementation or Result subclass from the user that we can serialize to Cloud and instantiate in `State._result` for a user when hydrating States from Cloud.

The important method to implement is:
`CustomResult.hydrate(result: Result) -> Result`
This should return an instance of the result arg, since the pipeline will call this a la `state._result.hydrate(result=self.task.result)`.

Other methods common to Results like store_safe_value, validate, read, write etc should not be implemented. (Decide if it should raise its own kind of warning or not especially if we miss an edge case and then get the NotImplementedError from tbhe base class.)



## Alternatives
*Please describe any alternatives you've considered, even if you've dismissed them*



Discussion:

Resolving patch:
diff --git a/src/prefect/engine/results/__init__.py b/src/prefect/engine/results/__init__.py
--- a/src/prefect/engine/results/__init__.py
+++ b/src/prefect/engine/results/__init__.py
@@ -5,3 +5,4 @@
 from prefect.engine.results.azure_result import AzureResult
 from prefect.engine.results.s3_result import S3Result
 from prefect.engine.results.secret_result import SecretResult
+from prefect.engine.results.result_handler_result import ResultHandlerResult
diff --git a/src/prefect/engine/results/gcs_result.py b/src/prefect/engine/results/gcs_result.py
--- a/src/prefect/engine/results/gcs_result.py
+++ b/src/prefect/engine/results/gcs_result.py
@@ -25,11 +25,8 @@ class GCSResult(Result):
         - **kwargs (Any, optional): any additional `Result` initialization options
     """
 
-    def __init__(
-        self, bucket: str = None, credentials_secret: str = None, **kwargs: Any
-    ) -> None:
+    def __init__(self, bucket: str = None, **kwargs: Any) -> None:
         self.bucket = bucket
-        self.credentials_secret = credentials_secret
         super().__init__(**kwargs)
 
     @property
diff --git a/src/prefect/engine/results/result_handler_result.py b/src/prefect/engine/results/result_handler_result.py
new file mode 100644
--- /dev/null
+++ b/src/prefect/engine/results/result_handler_result.py
@@ -0,0 +1,82 @@
+from typing import Any, List, Tuple, Type, Dict
+
+from prefect.engine.result import Result
+from prefect.engine.results import (
+    AzureResult,
+    ConstantResult,
+    GCSResult,
+    LocalResult,
+    PrefectResult,
+    SecretResult,
+    S3Result,
+)
+from prefect.engine.result_handlers import (
+    AzureResultHandler,
+    ConstantResultHandler,
+    GCSResultHandler,
+    JSONResultHandler,
+    LocalResultHandler,
+    ResultHandler,
+    S3ResultHandler,
+    SecretResultHandler,
+)
+
+
+# a list of tuples of the form (ResultHandlerType, dict(attributes=init_kwargs), ResultType)
+
+CONVERSIONS = [
+    (ConstantResultHandler, dict(value="value"), ConstantResult),
+    (GCSResultHandler, dict(bucket="bucket"), GCSResult),
+    (JSONResultHandler, dict(), PrefectResult),
+    (LocalResultHandler, dict(dir="dir"), LocalResult),
+    (S3ResultHandler, dict(bucket="bucket", boto3_kwargs="boto3_kwargs"), S3Result),
+    (SecretResultHandler, dict(secret_task="secret_task"), SecretResult),
+]  # type: List[Tuple[Type[ResultHandler], Dict[Any, Any], Type[Result]]]
+
+
+class ResultHandlerResult(Result):
+    def __init__(self, result_handler: ResultHandler, **kwargs: Any):
+        kwargs.update(result_handler=result_handler)
+        super().__init__(**kwargs)
+
+    @classmethod
+    def from_result_handler(cls, result_handler: ResultHandler) -> Result:
+        for handler_type, attr_map, result_type in CONVERSIONS:
+            if isinstance(result_handler, handler_type):
+                kwargs = {
+                    kwarg: getattr(result_handler, attr, None)
+                    for attr, kwarg in attr_map.items()
+                }
+                return result_type(**kwargs)
+        return cls(result_handler)
+
+    def read(self, location: str) -> Result:
+        """
+        Exposes the read method of the underlying custom result handler fitting the Result interface.
+        Returns a new Result with the value read from the custom result handler.
+
+        Args:
+            - location (str): the location to read from
+
+        Returns:
+            - Result: returns a copy of this Result with the value set
+        """
+        new = self.copy()
+        new.value = self.result_handler.read(location)
+        return new
+
+    def write(self, value: Any, **kwargs: Any) -> Result:
+        """
+        Exposes the write method of the underlying custom result handler fitting the Result interface.
+
+        Args:
+            - value (Any): the value to write and attach to the result
+            - **kwargs (Any, optional): unused, for interface compatibility
+
+        Returns:
+            - Result: returns a copy of this Result with the location and value set
+        """
+        new = self.copy()
+        new.location = self.result_handler.write(value)
+        new.value = value
+        return new


Test patch:
diff --git a/tests/engine/results/test_result_handler_result.py b/tests/engine/results/test_result_handler_result.py
new file mode 100644
--- /dev/null
+++ b/tests/engine/results/test_result_handler_result.py
@@ -0,0 +1,111 @@
+import os
+import json
+import tempfile
+import uuid
+from typing import Union
+
+import cloudpickle
+import pytest
+
+import prefect
+from prefect import config
+from prefect.engine.results import (
+    ConstantResult,
+    GCSResult,
+    LocalResult,
+    PrefectResult,
+    S3Result,
+    SecretResult,
+    ResultHandlerResult,
+)
+from prefect.engine.result_handlers import (
+    ConstantResultHandler,
+    GCSResultHandler,
+    LocalResultHandler,
+    JSONResultHandler,
+    ResultHandler,
+    S3ResultHandler,
+    SecretResultHandler,
+)
+from prefect.tasks.core.constants import Constant
+from prefect.tasks.secrets import PrefectSecret
+
+
+def test_basic_conversion_secret_result():
+    task = PrefectSecret("foo")
+    result_handler = SecretResultHandler(task)
+    result = ResultHandlerResult.from_result_handler(result_handler)
+    assert isinstance(result, SecretResult)
+    assert result.secret_task is task
+
+
+def test_basic_conversion_json_result():
+    result_handler = JSONResultHandler()
+    result = ResultHandlerResult.from_result_handler(result_handler)
+    assert isinstance(result, PrefectResult)
+    assert result.write(42).location == "42"
+
+
+def test_basic_conversion_constant_result():
+    result_handler = ConstantResultHandler(value=42)
+    result = ResultHandlerResult.from_result_handler(result_handler)
+    assert isinstance(result, ConstantResult)
+    assert result.value == 42
+
+
+def test_basic_conversion_local_result(tmpdir):
+    result_handler = LocalResultHandler(dir=str(tmpdir))
+    result = ResultHandlerResult.from_result_handler(result_handler)
+    assert isinstance(result, LocalResult)
+    assert result.dir == str(tmpdir)
+
+
+def test_basic_conversion_gcs_result():
+    result_handler = GCSResultHandler(bucket="foo")
+    result = ResultHandlerResult.from_result_handler(result_handler)
+    assert isinstance(result, GCSResult)
+    assert result.bucket == "foo"
+
+
+def test_basic_conversion_s3_result():
+    result_handler = S3ResultHandler(bucket="foo", boto3_kwargs=dict(x=42, y=[1, 2, 3]))
+    result = ResultHandlerResult.from_result_handler(result_handler)
+    assert isinstance(result, S3Result)
+    assert result.bucket == "foo"
+    assert result.boto3_kwargs == dict(x=42, y=[1, 2, 3])
+
+
+class TestCustomResultHandler:
+    @pytest.fixture
+    def handler(self):
+        class MyHandler(ResultHandler):
+            stuff = dict()
+
+            def read(self, loc):
+                return self.stuff[loc]
+
+            def write(self, result):
+                key = str(uuid.uuid4())
+                self.stuff[key] = result
+                return key
+
+        return MyHandler()
+
+    def test_conversion_type(self, handler):
+        result = ResultHandlerResult.from_result_handler(handler)
+        assert isinstance(result, ResultHandlerResult)
+        assert isinstance(result.result_handler, type(handler))
+
+    def test_converted_result_writes_and_reads(self, handler):
+        result = ResultHandlerResult.from_result_handler(handler)
+        new = result.write(42)
+
+        assert new is not result
+        assert new.value == 42
+        assert new.location
+        assert new.result_handler.stuff[new.location] == 42
+
+        another = new.read(new.location)
+        assert another is not new
+        assert another.value == 42
+        assert another.location == new.location