Repository: AgnostiqHQ/covalent (python)

Issue:
Error when setting `executor` in `@ct.lattice`: `DaskExecutor` object has no attribute `scheduler_address` 
### Environment

- **Covalent version**: >=0.227.0rc0
- **Python version**: 3.10.13
- **Operating system**: Linux


### What is happening?

When setting the `executor` kwarg in the `@ct.lattice` decorator, I receive the following error message in the post-processing step of the workflow.

```bas
'DaskExecutor' object has no attribute 'scheduler_address'
```

**Note**: setting the `executor` kwarg in the `@ct.electron` (but not `@ct.lattice`) works fine and the workflow goes to completion. This seems very closely related to #1733, which is perhaps not fully solved.

![image](https://github.com/AgnostiqHQ/covalent/assets/8674072/609fb67e-be72-4749-b34d-835ec45bf05d)

### How can we reproduce the issue?

```python
import covalent as ct

executor = ct.executor.LocalExecutor()

@ct.electron
def add(a, b):
    return a + b

@ct.lattice(executor=executor)
def workflow(a, b):
    return add(a, b)


dispatch_id = ct.dispatch(workflow)(1, 2)
result = ct.get_result(dispatch_id,wait=True)
assert result.status == "COMPLETED"
```

### What should happen?

The workflow post-processing step should complete.

### Any suggestions?

I need to see if the same behavior occurs on `main`.

Pull request:
Fix propagation of lattice-default attributes to electrons
Also use `workflow_executor` to build sublattice graphs

Closes #1781 .

<!--
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Add a note to /CHANGELOG.md summarizing the changes.
⚠️ If your pull request fixes an open issue, please link to the issue.
⚠️ Ensure that your branch is up-to-date with the base branch.
-->

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation and CHANGELOG accordingly.
- [ ] I have read the CONTRIBUTING document.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08f5daad..22d9959a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 
+- Lattice-default metadata attributes are now applied correctly
+- Sublattices are built using `workflow_executor`
 - Added covalent version attribute to Remote Executors
 - Removed unassigned variable names
 - Contributing guidelines steps for installing for the first time
diff --git a/covalent/_workflow/electron.py b/covalent/_workflow/electron.py
index d8697d83..67f3044b 100644
--- a/covalent/_workflow/electron.py
+++ b/covalent/_workflow/electron.py
@@ -391,7 +391,7 @@ class Electron:
             if (
                 k not in consumable_constraints
                 and k in DEFAULT_METADATA_VALUES
-                and not self.get_metadata(k)
+                and self.get_metadata(k) is None
             ):
                 meta = active_lattice.get_metadata(k) or DEFAULT_METADATA_VALUES[k]
                 self.set_metadata(k, meta)
@@ -426,8 +426,8 @@ class Electron:
             parent_metadata = active_lattice.metadata.copy()
             app_log.debug(f"Parent lattice metadata: {parent_metadata}")
             e_meta = parent_metadata.copy()
-            e_meta.pop("workflow_executor")
-            e_meta.pop("workflow_executor_data")
+            e_meta["executor"] = e_meta.pop("workflow_executor")
+            e_meta["executor_data"] = e_meta.pop("workflow_executor_data")
 
             sub_electron = Electron(
                 function=_build_sublattice_graph,
@@ -688,8 +688,8 @@ def electron(
     files: List[FileTransfer] = [],
     deps_bash: Union[DepsBash, List, str] = None,
     deps_pip: Union[DepsPip, list] = None,
-    call_before: Union[List[DepsCall], DepsCall] = [],
-    call_after: Union[List[DepsCall], DepsCall] = [],
+    call_before: Union[List[DepsCall], DepsCall] = None,
+    call_after: Union[List[DepsCall], DepsCall] = None,
 ) -> Callable:  # sourcery skip: assign-if-exp
     """
     Electron decorator to be called upon a function. Returns the wrapper function with the same functionality as `_func`.
@@ -719,7 +719,7 @@ def electron(
         )
         executor = backend
 
-    deps = {}
+    deps = {} if deps_bash or deps_pip else None
 
     if isinstance(deps_bash, DepsBash):
         deps["bash"] = deps_bash
@@ -758,14 +758,23 @@ def electron(
     if isinstance(call_after, DepsCall):
         call_after = [call_after]
 
-    call_before = internal_call_before_deps + call_before
-    call_after = internal_call_after_deps + call_after
+    call_before_final = [] if internal_call_before_deps or call_before else None
+    if internal_call_before_deps:
+        call_before_final.extend(internal_call_before_deps)
+    if call_before:
+        call_before_final.extend(call_before)
+
+    call_after_final = [] if internal_call_after_deps or call_after else None
+    if internal_call_after_deps:
+        call_after_final.extend(internal_call_after_deps)
+    if call_after:
+        call_after_final.extend(call_after)
 
     constraints = {
         "executor": executor,
         "deps": deps,
-        "call_before": call_before,
-        "call_after": call_after,
+        "call_before": call_before_final,
+        "call_after": call_after_final,
     }
 
     constraints = encode_metadata(constraints)
diff --git a/covalent/_workflow/postprocessing.py b/covalent/_workflow/postprocessing.py
index 8047849d..1072cd9d 100644
--- a/covalent/_workflow/postprocessing.py
+++ b/covalent/_workflow/postprocessing.py
@@ -28,7 +28,7 @@ from .._shared_files.defaults import (
     prefix_separator,
     sublattice_prefix,
 )
-from .transport import _TransportGraph, encode_metadata
+from .transport import _TransportGraph
 
 if TYPE_CHECKING:
     from .electron import Electron
@@ -142,12 +142,13 @@ class Postprocessor:
             Dictionary of metadata for the postprocess electron.
 
         """
-        executor = self.lattice.get_metadata("workflow_executor")
-        executor_data = self.lattice.get_metadata("workflow_executor_data")
-        pp_metadata = encode_metadata(DEFAULT_METADATA_VALUES.copy())
-        pp_metadata["executor"] = executor
-        pp_metadata["executor_data"] = executor_data
-        pp_metadata = encode_metadata(pp_metadata)
+
+        # This is already initialized and JSON-encoded at the
+        # beginning of Lattice.build_graph
+        pp_metadata = self.lattice.metadata.copy()
+
+        pp_metadata["executor"] = pp_metadata.pop("workflow_executor")
+        pp_metadata["executor_data"] = pp_metadata.pop("workflow_executor_data")
         return pp_metadata
 
     def add_exhaustive_postprocess_node(self, bound_electrons: Dict) -> None:


Test patch:
diff --git a/tests/covalent_tests/workflow/electron_test.py b/tests/covalent_tests/workflow/electron_test.py
index 5b9f45a7..941934f6 100644
--- a/tests/covalent_tests/workflow/electron_test.py
+++ b/tests/covalent_tests/workflow/electron_test.py
@@ -426,7 +426,10 @@ def test_as_transportable_dict():
 def test_call_sublattice():
     """Test the sublattice logic when the __call__ method is invoked."""
 
-    @ct.lattice(executor="mock")
+    le = LocalExecutor()
+    bash_dep = ct.DepsBash("apt install rustc cargo")
+
+    @ct.lattice(executor=le, workflow_executor="dask", deps_bash=bash_dep)
     def mock_workflow(x):
         return sublattice(x)
 
@@ -441,7 +444,9 @@ def test_call_sublattice():
 
     with active_lattice_manager.claim(mock_workflow):
         bound_electron = sublattice()
-        assert bound_electron.metadata["executor"] == "mock"
+        assert bound_electron.metadata["executor"] == "dask"
+        assert bound_electron.metadata["executor_data"] == {}
+        assert bound_electron.metadata["deps"]["bash"] == bash_dep.to_dict()
         for _, node_data in mock_workflow.transport_graph._graph.nodes(data=True):
             if node_data["name"].startswith(sublattice_prefix):
                 assert "mock_task" in node_data["function_string"]
diff --git a/tests/covalent_tests/workflow/postprocessing_test.py b/tests/covalent_tests/workflow/postprocessing_test.py
index f4a04bb1..e273a854 100644
--- a/tests/covalent_tests/workflow/postprocessing_test.py
+++ b/tests/covalent_tests/workflow/postprocessing_test.py
@@ -25,12 +25,15 @@ import covalent as ct
 from covalent._shared_files.defaults import postprocess_prefix
 from covalent._workflow.electron import Electron
 from covalent._workflow.postprocessing import Postprocessor
+from covalent.executor import LocalExecutor
 
 
 @pytest.fixture
 def postprocessor():
     """Get postprocessor object."""
 
+    le = LocalExecutor()
+
     @ct.electron(executor="local")
     def task_1(x):
         return x
@@ -39,7 +42,7 @@ def postprocessor():
     def task_2(x):
         return [x * 2]
 
-    @ct.lattice(executor="local")
+    @ct.lattice(executor=le, workflow_executor="dask")
     def workflow(x):
         res_1 = task_1(x)
         res_2 = task_2(x)
@@ -130,23 +133,10 @@ def test_get_node_ids_from_retval(postprocessor, retval, node_ids):
 
 def test_get_electron_metadata(postprocessor):
     """Test method that retrieves the postprocessing electron metadata."""
-    assert postprocessor._get_electron_metadata() == {
-        "executor": None,
-        "deps": {},
-        "call_before": [],
-        "call_after": [],
-        "workflow_executor": "local",
-        "executor_data": {},
-        "workflow_executor_data": {},
-    } or {
-        "executor": None,
-        "deps": {},
-        "call_before": [],
-        "call_after": [],
-        "workflow_executor": "dask",
-        "executor_data": {},
-        "workflow_executor_data": {},
-    }
+    metadata_copy = postprocessor.lattice.metadata.copy()
+    metadata_copy["executor"] = metadata_copy.pop("workflow_executor")
+    metadata_copy["executor_data"] = metadata_copy.pop("workflow_executor_data")
+    assert postprocessor._get_electron_metadata() == metadata_copy
 
 
 def test_add_exhaustive_postprocess_node(postprocessor):
diff --git a/tests/functional_tests/workflow_stack_test.py b/tests/functional_tests/workflow_stack_test.py
index 8d2a8f90..7bab310b 100644
--- a/tests/functional_tests/workflow_stack_test.py
+++ b/tests/functional_tests/workflow_stack_test.py
@@ -25,6 +25,7 @@ import covalent as ct
 import covalent._dispatcher_plugins.local as local
 import covalent._results_manager.results_manager as rm
 from covalent._results_manager.result import Result
+from covalent.executor import LocalExecutor
 
 
 def construct_temp_cache_dir():
@@ -139,7 +140,7 @@ def test_lattice_electron_metadata_propagation():
     def task_2():
         pass
 
-    @ct.lattice(deps_bash=l_bash_dep)
+    @ct.lattice(executor=LocalExecutor(), deps_bash=l_bash_dep)
     def workflow():
         task_1()
         task_2()