Repository: AgnostiqHQ/covalent (python)

Issue:
Result object cannot be printed if a node fails
Consider the following trivial workflow:

```python
@ct.electron(executor="local")
def task():
    raise RuntimeError("error")

@ct.lattice(executor="local")
def workflow():
    return task()
```
I cannot print the result object:
```
dispatch_id = ct.dispatch(workflow)()
result_object = ct.get_result(dispatch_id, wait=True)
print(result_object)
```

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [18], in <cell line: 11>()
      9 dispatch_id = ct.dispatch(workflow)()
     10 result_object = ct.get_result(dispatch_id, wait=True)
---> 11 print(result_object)

File ~/Agnostiq/code/covalent/covalent/_results_manager/result.py:127, in Result.__str__(self)
    125 node_outputs = self.get_all_node_outputs()
    126 for k, v in node_outputs.items():
--> 127     show_result_str += f"{k}: {v.object_string}\n"
    129 return show_result_str

AttributeError: 'NoneType' object has no attribute 'object_string'
```

This happens because when Covalent encounters an exception when processing a task, the transport graph node's `output` property is set to `None`; for any task that completes normally, the output is a `TransportableObject` (tasks that don't return a value have output `TransportableObject(None)`).

A simple fix is to convert all `None` node outputs to `TransportableObject(None)` in the `Result.__str__` method; the idempotent `TransportableObject.make_transportable()` method is convenient for this.

Pull request:
Ensure that each node output is a TransportableObject when printing
Convert None outputs to TransportableObject(None)

<!--
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Increment the version number in the /VERSION file. If this is a bugfix, increment the patch version (the rightmost number), for example 0.18.2 becomes 0.18.3. If it's a new feature, increment the minor version (the middle number), for example 0.18.2 becomes 0.19.0.
⚠️ Add a note to /CHANGELOG.md with your version number and the date, summarizing the changes.
⚠️ If your pull request fixes an open issue, please link to the issue.
⚠️ Rebase the latest changes from the develop branch. Assuming origin points to https://github.com/AgnostiqHQ/covalent, you should run git rebase origin/develop.
-->

- [x] I have added the tests to cover my changes.
- [x] I have updated the documentation and CHANGELOG accordingly.
- [x] I have read the CONTRIBUTING document.

Closes #1393.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6ba6955..a613f6ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Support for transferring the contents of folders to and from S3 buckets using the file transfer module.
 
+### Fixed
+
+- Result objects now print correctly when nodes fail
+
 ## [0.207.0-rc.0] - 2022-10-26
 
 ### Authors
diff --git a/covalent/_results_manager/result.py b/covalent/_results_manager/result.py
index 6851175b..c0571336 100644
--- a/covalent/_results_manager/result.py
+++ b/covalent/_results_manager/result.py
@@ -124,7 +124,7 @@ Node Outputs
 
         node_outputs = self.get_all_node_outputs()
         for k, v in node_outputs.items():
-            show_result_str += f"{k}: {v.object_string}\n"
+            show_result_str += f"{k}: {TransportableObject.make_transportable(v).object_string}\n"
 
         return show_result_str
 


Test patch:
diff --git a/tests/covalent_tests/results_manager_tests/results_test.py b/tests/covalent_tests/results_manager_tests/results_test.py
index 9c0da777..538a3ecb 100644
--- a/tests/covalent_tests/results_manager_tests/results_test.py
+++ b/tests/covalent_tests/results_manager_tests/results_test.py
@@ -88,6 +88,29 @@ def result_1():
     return result
 
 
+@pytest.fixture
+def result_2():
+    @ct.electron(executor=le)
+    def task_1(x, y):
+        raise RuntimeError("error")
+
+    @ct.lattice(executor=le, workflow_executor=le, results_dir=TEMP_RESULTS_DIR)
+    def workflow_1(a, b):
+        """Docstring"""
+        res_1 = task_1(a, b)
+        return res_1
+
+    Path(f"{TEMP_RESULTS_DIR}/dispatch_1").mkdir(parents=True, exist_ok=True)
+    workflow_1.build_graph(a=1, b=2)
+    received_lattice = LatticeClass.deserialize_from_json(workflow_1.serialize_to_json())
+    result = Result(
+        lattice=received_lattice, results_dir=TEMP_RESULTS_DIR, dispatch_id="dispatch_1"
+    )
+    result.lattice.metadata["results_dir"] = TEMP_RESULTS_DIR
+    result._initialize_nodes()
+    return result
+
+
 def test_get_node_error(result_1):
     """Test result method to get the node error."""
     assert not result_1._get_node_error(node_id=0)
@@ -103,6 +126,12 @@ def test_get_all_node_results(result_1, mocker):
             assert data_row["node_name"] == ":parameter:1"
 
 
+def test_str_result(result_2, mocker):
+    """Test result __str__ method"""
+    s = str(result_2)
+    assert "task_1" in s
+
+
 def test_result_root_dispatch_id(result_1):
     """Test the `root_dispatch_id` property`"""