Repository: Quantum-Accelerators/quacc
Pull request #1440 (2023-12-30 00:31:54)

Rely on `strip_decorator` instead of `.__wrapped__`

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1440?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`1c920d6`)](https://app.codecov.io/gh/Quantum-Accelerators/quacc/commit/1c920d6b1ebfabe056ee4ec7fe94956ba90eb48e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) 98.97% compared to head [(`9597a8b`)](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1440?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) 98.97%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1440   +/-   ##
=======================================
  Coverage   98.97%   98.97%           
=======================================
  Files          77       77           
  Lines        3014     3025   +11     
=======================================
+ Hits         2983     2994   +11     
  Misses         31       31           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1440?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94dbd6c2..71d111d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.5.1]
+
+### Added
+
+- `quacc.wflow_tools.customizers.strip_decorator` and `quacc.wflow_tools.customizers.redecorate` are imported in the base `__init__.py` for easy user access
+
+### Changed
+
+- The `quacc.wflow_tools.customizers.strip_decorator` function is recommended instead of calling `.__wrapped__`
+
 ## [0.5.0]
 
 ### Added
diff --git a/docs/dev/recipes/flows.md b/docs/dev/recipes/flows.md
index fcc0f63d..de1b2c15 100644
--- a/docs/dev/recipes/flows.md
+++ b/docs/dev/recipes/flows.md
@@ -12,6 +12,10 @@ The flow itself does not contain computationally intensive tasks. It simply call
 
 A simple, representative flow can be found in [quacc.recipes.vasp.mp.mp_relax_flow][].
 
+!!! Note
+
+    All `#!Python @flow`-decorated functions distributed with quacc must allow for the individual job parameters and decorators to be updated by the user, which is typically done via the [quacc.wflow_tools.customizers.customize_funcs][] function. Refer to the example above for details.
+
 ## Dynamic Flows
 
 quacc fully supports complex, dynamic flows where the number of jobs is not known _a priori_. In this case, a common pattern is the use of a subflow, defined with a `#!Python @subflow` decorator. A subflow is just like a flow, except the returned object is a list of job outputs.
diff --git a/docs/user/basics/wflow_syntax.md b/docs/user/basics/wflow_syntax.md
index 7d28fafc..5f279b70 100644
--- a/docs/user/basics/wflow_syntax.md
+++ b/docs/user/basics/wflow_syntax.md
@@ -254,10 +254,10 @@ graph LR
 
 ## Stripping the Decorator from a Job
 
-If you ever want to strip the decorator from a pre-decorated `#!Python @job` (e.g. to test out a calculation locally without changing your quacc settings), you can call the `.__wrapped__` attribute. This returns the original function.
+If you ever want to strip the decorator from a pre-decorated `#!Python @job` (e.g. to test out a calculation locally without changing your quacc settings), you can do so with [quacc.wflow_tools.customizers.strip_decorator][] as follows:
 
 ```python
-from quacc import job
+from quacc import job, strip_decorator
 
 
 @job
@@ -265,7 +265,8 @@ def add(a, b):
     return a + b
 
 
-add.__wrapped__(1, 2)  # 3
+original_add = strip_decorator(add)
+original_add(1, 2)  # 3
 ```
 
 ## Learn More
diff --git a/docs/user/wflow_engine/wflow_engines1.md b/docs/user/wflow_engine/wflow_engines1.md
index 72d3f50e..500dbfaa 100644
--- a/docs/user/wflow_engine/wflow_engines1.md
+++ b/docs/user/wflow_engine/wflow_engines1.md
@@ -69,30 +69,30 @@ graph LR
     atoms = bulk("Cu")
 
     # Define the workflow
-    workflow = flow(relax_job)  # (1)!
+    @flow  # (1)!
+    def workflow(atoms):
+        return relax_job(atoms)  # (2)!
 
     # Dispatch the workflow to the Covalent server
     # with the bulk Cu Atoms object as the input
-    dispatch_id = ct.dispatch(workflow)(atoms)  # (2)!
+    dispatch_id = ct.dispatch(workflow)(atoms)  # (3)!
 
     # Fetch the result from the server
-    result = ct.get_result(dispatch_id, wait=True)  # (3)!
+    result = ct.get_result(dispatch_id, wait=True)  # (4)!
     print(result)
     ```
 
-    1. This is shorthand for the following:
+    1. This can be written more compactly as:
 
         ```python
-        @flow
-        def workflow(atoms):
-            return relax_job(atoms)
+        workflow = flow(relax_job)
         ```
 
-        Also note that the `relax_job` function was pre-defined in quacc with a `#!Python @job` decorator, which is why we did not need to include it here.
+    2. The `relax_job` function was pre-defined in quacc with a `#!Python @job` decorator, which is why we did not need to include it here.
 
-    2. This will dispatch the workflow to the Covalent server.
+    3. This will dispatch the workflow to the Covalent server.
 
-    3. The `ct.get_result` function is used to fetch the workflow status and results from the server. You don't need to set `wait=True` in practice. Once you dispatch the workflow, it will begin running (if the resources are available).
+    4. The `ct.get_result` function is used to fetch the workflow status and results from the server. You don't need to set `wait=True` in practice. Once you dispatch the workflow, it will begin running (if the resources are available).
 
 === "Dask"
 
@@ -219,20 +219,6 @@ graph LR
     print(future.result())
     ```
 
-    !!! Tip "Selectively Modifying Decorators in a Pre-Made Flow"
-
-        If you want to modify the decorators of select jobs in a pre-made workflow, such as to modify the executor of a given function, you can use the `job_decorators` keyword argument:
-
-        ```python
-        bulk_to_slabs_flow(atoms, job_decorators={"static_job": job(executors=["all"])})
-        ```
-
-        As a shorthand, all of the decorators can be modified at once using the "all" keyword:
-
-        ```python
-        bulk_to_slabs_flow(atoms, job_decorators={"all": job(executors=["all"])})
-        ```
-
 === "Covalent"
 
     ```python
@@ -253,32 +239,6 @@ graph LR
 
     1. We didn't need to wrap `bulk_to_slabs_flow` with a decorator because it is already pre-decorated with a `#!Python @flow` decorator.
 
-    !!! Tip "Selectively Modifying Decorators in a Pre-Made Flow"
-
-        If you want to modify the decorators of select jobs in a pre-made workflow, such as to modify the executor of a given function, you can use the `job_decorators` keyword argument:
-
-        ```python
-        ct.dispatch(bulk_to_slabs_flow)(atoms, job_decorators={"static_job": job(executor="local")})
-        ```
-
-        As a shorthand, all of the decorators can be modified at once using the "all" keyword:
-
-        ```python
-        ct.dispatch(bulk_to_slabs_flow)(atoms, job_decorators={"all": job(executor="local")})  # (1)!
-        ```
-
-        1. Alternatively, you can simply modify the `#!Python @flow` decorator itself:
-
-            ```python
-            from quacc import flow
-
-            @flow(executor="local")
-            def bulk_to_slabs_flow_(*args, **kwargs):
-                return bulk_to_slabs_flow(*args, **kwargs)
-
-            ct.dispatch(bulk_to_slabs_flow_)(atoms)
-            ```
-
 === "Dask"
 
     ```python
diff --git a/docs/user/wflow_engine/wflow_engines2.md b/docs/user/wflow_engine/wflow_engines2.md
index 9accc6ac..40740365 100644
--- a/docs/user/wflow_engine/wflow_engines2.md
+++ b/docs/user/wflow_engine/wflow_engines2.md
@@ -38,7 +38,7 @@ graph LR
         future1 = relax_job(atoms)
 
         # Define Job 2, which takes the output of Job 1 as input
-        future2 = static_job(future1["atoms"])
+        future2 = static_job(future1["atoms"])  # (1)!
 
         return future2
 
@@ -54,9 +54,17 @@ graph LR
     print(result)
     ```
 
-    !!! Note
+    1. Parsl `PythonApp` objects will implicitly know to call `.result()` on any `AppFuture` it receives, and it is good to rely on this fact to avoid unnecessary blocking.
 
-        Parsl `PythonApp` objects will implicitly know to call `.result()` on any `AppFuture` it receives, and it is good to rely on this fact to avoid unnecessary blocking.
+    ??? Tip "Modifying the Decorator of a Pre-Made Job"
+
+        If you want to modify the decorator of a pre-made job, such as to modify the allowed executors of a given function, you can use the [quacc.wflow_tools.customizers.redecorate][] function:
+
+        ```python
+        from quacc import redecorate
+
+        relax_job = redecorate(relax_job, job(executors=["all"]))
+        ```
 
 === "Covalent"
 
@@ -102,6 +110,27 @@ graph LR
 
     1. Because the workflow was defined with a `#!Python @flow` decorator, it will be sent to the Covalent server and a dispatch ID will be returned.
 
+    ??? Tip "Modifying the Decorator of a Pre-Made Job"
+
+        If you want to modify the decorator of a pre-made job, such as to modify the executor, you can use the [quacc.wflow_tools.customizers.redecorate][] function:
+
+        ```python
+        from quacc import redecorate
+
+        relax_job = redecorate(relax_job, job(executor="local"))
+        result1 = relax_job(atoms)
+        ```
+
+        This is the same as simply re-decorating it directly and using this new function instead:
+
+        ```python
+        from quacc import job
+
+        @job(executor="local")
+        def relax_job_(*args, **kwargs):
+            return relax_job(*args, **kwargs)
+        ```
+
 === "Dask"
 
     !!! Important
@@ -420,6 +449,20 @@ graph LR
     print(result)
     ```
 
+    ??? Tip "Selectively Modifying Job Decorators in a Pre-Made Flow"
+
+        If you want to modify the decorators of select jobs in a pre-made workflow, such as to modify the allowed executors of a given function, you can use the `job_decorators` keyword argument:
+
+        ```python
+        bulk_to_slabs_flow(atoms, job_decorators={"static_job": job(executors=["MyFavoriteExecutor"])})
+        ```
+
+        As a shorthand, all of the decorators can be modified at once using the "all" keyword:
+
+        ```python
+        bulk_to_slabs_flow(atoms, job_decorators={"all": job(executors=["MyFavoriteExecutor"])})
+        ```
+
 === "Covalent"
 
     ```python
@@ -448,6 +491,50 @@ graph LR
     print(result)
     ```
 
+    ??? Tip "Modifying the Decorator of a Pre-Made Flow"
+
+        If you want to modify the decorator of a pre-made flow, such as to modify the flow's executor, you can use the [quacc.wflow_tools.customizers.redecorate][] function:
+
+        ```python
+        from quacc import redecorate
+
+        bulk_to_slabs_flow = redecorate(bulk_to_slabs_flow, flow(executor="local"))
+        ```
+
+        This achieves functionally the same result as simply re-decorating it directly and using this new function instead:
+
+        ```python
+        @flow(executor="local")
+        def bulk_to_slabs_flow_(*args, **kwargs):
+            return bulk_to_slabs_flow(*args, **kwargs)
+        ```
+
+    ??? Tip "Selectively Modifying Job Decorators in a Pre-Made Flow"
+
+        If you want to modify the decorators of select jobs in a pre-made workflow, such as to modify the executor of a given function, you can use the `job_decorators` keyword argument:
+
+        ```python
+        ct.dispatch(bulk_to_slabs_flow)(atoms, job_decorators={"static_job": job(executor="local")})
+        ```
+
+        As a shorthand, all of the decorators can be modified at once using the "all" keyword:
+
+        ```python
+        ct.dispatch(bulk_to_slabs_flow)(atoms, job_decorators={"all": job(executor="local")})  # (1)!
+        ```
+
+        1. Alternatively, you can simply modify the `#!Python @flow` decorator itself:
+
+            ```python
+            from quacc import flow
+
+            @flow(executor="local")
+            def bulk_to_slabs_flow_(*args, **kwargs):
+                return bulk_to_slabs_flow(*args, **kwargs)
+
+            ct.dispatch(bulk_to_slabs_flow_)(atoms)
+            ```
+
 === "Dask"
 
     ```python
diff --git a/pyproject.toml b/pyproject.toml
index ec8fc074..3904b227 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -144,13 +144,12 @@ select = [
 extend-ignore = [
   "PLR",    # Design related pylint codes
   "E501",   # Line too long
-  "PT004",  # Use underscore for non-returning fixture (use usefixture instead)
   "B028",   # No explicit stacklevel
   "EM101",  # Exception must not use a string literal
   "EM102",  # Exception must not use an f-string literal
   "G004",   # f-string in Logging statement
-  "RET503", # Missing explicit return
   "RUF015", # Prefer next(iter())
+  "RET505", # Unnecessary `elif` after `return`
 ]
 typing-modules = ["mypackage._compat.typing"]
 src = ["src"]
diff --git a/src/quacc/__init__.py b/src/quacc/__init__.py
index 2d6f38f7..17e90ca4 100644
--- a/src/quacc/__init__.py
+++ b/src/quacc/__init__.py
@@ -7,12 +7,22 @@ from typing import TYPE_CHECKING
 from ase.atoms import Atoms
 
 from quacc.settings import QuaccSettings
+from quacc.wflow_tools.customizers import redecorate, strip_decorator
 from quacc.wflow_tools.decorators import Flow, Job, Subflow, flow, job, subflow
 
 if TYPE_CHECKING:
     from typing import Any
 
-__all__ = ["flow", "job", "subflow", "Flow", "Job", "Subflow"]
+__all__ = [
+    "flow",
+    "job",
+    "subflow",
+    "Flow",
+    "Job",
+    "Subflow",
+    "redecorate",
+    "strip_decorator",
+]
 
 
 def atoms_as_dict(s: Atoms) -> dict[str, Any]:
diff --git a/src/quacc/recipes/_qchem_legacy/ts.py b/src/quacc/recipes/_qchem_legacy/ts.py
index 473369a4..f0ddb95d 100644
--- a/src/quacc/recipes/_qchem_legacy/ts.py
+++ b/src/quacc/recipes/_qchem_legacy/ts.py
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
 import psutil
 from monty.dev import requires
 
-from quacc import SETTINGS, job
+from quacc import SETTINGS, job, strip_decorator
 from quacc.recipes._qchem_legacy._base import base_opt_fn
 from quacc.recipes._qchem_legacy.core import relax_job
 from quacc.utils.dicts import recursive_dict_merge
@@ -277,7 +277,7 @@ def quasi_irc_job(
     irc_opt_params = recursive_dict_merge(irc_opt_params_defaults, irc_opt_params)
 
     SETTINGS.CHECK_CONVERGENCE = False
-    irc_summary = irc_job.__wrapped__(
+    irc_summary = strip_decorator(irc_job)(
         atoms,
         charge,
         spin_multiplicity,
@@ -294,7 +294,7 @@ def quasi_irc_job(
     )
 
     SETTINGS.CHECK_CONVERGENCE = default_settings.CHECK_CONVERGENCE
-    relax_summary = relax_job.__wrapped__(
+    relax_summary = strip_decorator(relax_job)(
         irc_summary["atoms"],
         charge,
         spin_multiplicity,
diff --git a/src/quacc/recipes/newtonnet/ts.py b/src/quacc/recipes/newtonnet/ts.py
index 59822616..0b5933bf 100644
--- a/src/quacc/recipes/newtonnet/ts.py
+++ b/src/quacc/recipes/newtonnet/ts.py
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
 
 from monty.dev import requires
 
-from quacc import SETTINGS, job
+from quacc import SETTINGS, job, strip_decorator
 from quacc.recipes.newtonnet.core import _add_stdev_and_hess, freq_job, relax_job
 from quacc.runners.ase import run_opt
 from quacc.schemas.ase import summarize_opt_run
@@ -114,7 +114,7 @@ def ts_job(
 
     # Run a frequency calculation
     freq_summary = (
-        freq_job.__wrapped__(opt_ts_summary["atoms"], **freq_job_kwargs)
+        strip_decorator(freq_job)(opt_ts_summary["atoms"], **freq_job_kwargs)
         if run_freq
         else None
     )
@@ -194,7 +194,7 @@ def irc_job(
 
     # Run frequency job
     freq_summary = (
-        freq_job.__wrapped__(opt_irc_summary["atoms"], **freq_job_kwargs)
+        strip_decorator(freq_job)(opt_irc_summary["atoms"], **freq_job_kwargs)
         if run_freq
         else None
     )
@@ -246,16 +246,16 @@ def quasi_irc_job(
     irc_job_kwargs = recursive_dict_merge(irc_job_defaults, irc_job_kwargs)
 
     # Run IRC
-    irc_summary = irc_job.__wrapped__(
+    irc_summary = strip_decorator(irc_job)(
         atoms, direction=direction, run_freq=False, **irc_job_kwargs
     )
 
     # Run opt
-    relax_summary = relax_job.__wrapped__(irc_summary["atoms"], **relax_job_kwargs)
+    relax_summary = strip_decorator(relax_job)(irc_summary["atoms"], **relax_job_kwargs)
 
     # Run frequency
     freq_summary = (
-        freq_job.__wrapped__(relax_summary["atoms"], **freq_job_kwargs)
+        strip_decorator(freq_job)(relax_summary["atoms"], **freq_job_kwargs)
         if run_freq
         else None
     )
diff --git a/src/quacc/recipes/qchem/ts.py b/src/quacc/recipes/qchem/ts.py
index 387c74af..2016f45c 100644
--- a/src/quacc/recipes/qchem/ts.py
+++ b/src/quacc/recipes/qchem/ts.py
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
 
 from monty.dev import requires
 
-from quacc import SETTINGS, job
+from quacc import SETTINGS, job, strip_decorator
 from quacc.recipes.qchem._base import base_opt_fn
 from quacc.recipes.qchem.core import _BASE_SET, relax_job
 from quacc.utils.dicts import recursive_dict_merge
@@ -232,10 +232,10 @@ def quasi_irc_job(
     relax_job_kwargs = recursive_dict_merge(relax_job_defaults, relax_job_kwargs)
 
     SETTINGS.CHECK_CONVERGENCE = False
-    irc_summary = irc_job.__wrapped__(atoms, **irc_job_kwargs)
+    irc_summary = strip_decorator(irc_job)(atoms, **irc_job_kwargs)
 
     SETTINGS.CHECK_CONVERGENCE = default_settings.CHECK_CONVERGENCE
-    relax_summary = relax_job.__wrapped__(irc_summary["atoms"], **relax_job_kwargs)
+    relax_summary = strip_decorator(relax_job)(irc_summary["atoms"], **relax_job_kwargs)
 
     relax_summary["initial_irc"] = irc_summary
 
diff --git a/src/quacc/recipes/vasp/core.py b/src/quacc/recipes/vasp/core.py
index 84a8f49f..5b046517 100644
--- a/src/quacc/recipes/vasp/core.py
+++ b/src/quacc/recipes/vasp/core.py
@@ -4,7 +4,7 @@ from __future__ import annotations
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-from quacc import job
+from quacc import job, strip_decorator
 from quacc.recipes.vasp._base import base_fn
 
 if TYPE_CHECKING:
@@ -158,12 +158,12 @@ def double_relax_job(
     relax2_kwargs = relax2_kwargs or {}
 
     # Run first relaxation
-    summary1 = relax_job.__wrapped__(
+    summary1 = strip_decorator(relax_job)(
         atoms, preset=preset, relax_cell=relax_cell, **relax1_kwargs
     )
 
     # Run second relaxation
-    summary2 = relax_job.__wrapped__(
+    summary2 = strip_decorator(relax_job)(
         summary1["atoms"],
         preset=preset,
         relax_cell=relax_cell,
diff --git a/src/quacc/wflow_tools/customizers.py b/src/quacc/wflow_tools/customizers.py
index 73e65de8..4f15214a 100644
--- a/src/quacc/wflow_tools/customizers.py
+++ b/src/quacc/wflow_tools/customizers.py
@@ -4,8 +4,6 @@ from __future__ import annotations
 from functools import partial
 from typing import TYPE_CHECKING
 
-from quacc import SETTINGS, job
-
 if TYPE_CHECKING:
     from typing import Any, Callable
 
@@ -24,15 +22,33 @@ def strip_decorator(func: Callable) -> Callable:
     Callable
         The function with all decorators removed.
     """
-
-    if hasattr(func, "__wrapped__"):
-        func = func.__wrapped__
+    from quacc import SETTINGS
 
     if SETTINGS.WORKFLOW_ENGINE == "covalent":
         from covalent._workflow.lattice import Lattice
 
-        if isinstance(func, Lattice):
+        if hasattr(func, "electron_object"):
+            func = func.electron_object.function
+
+        if hasattr(func, "workflow_function"):
             func = func.workflow_function.get_deserialized()
+
+    elif SETTINGS.WORKFLOW_ENGINE == "dask":
+        if hasattr(func, "__wrapped__"):
+            func = func.__wrapped__
+
+    elif SETTINGS.WORKFLOW_ENGINE == "jobflow":
+        if hasattr(func, "original"):
+            func = func.original
+
+    elif SETTINGS.WORKFLOW_ENGINE == "parsl":
+        if hasattr(func, "func"):
+            func = func.func
+
+    elif SETTINGS.WORKFLOW_ENGINE == "redun":
+        if hasattr(func, "func"):
+            func = func.func
+
     return func
 
 
@@ -73,13 +89,13 @@ def update_parameters(func: Callable, params: dict[str, Any]) -> Callable:
     Callable
         The updated function.
     """
-    if SETTINGS.WORKFLOW_ENGINE == "dask":
-        func = strip_decorator(func)
-        updated_func = job(partial(func, **params))
-    else:
-        updated_func = partial(func, **params)
+    from quacc import SETTINGS, job
 
-    return updated_func
+    if SETTINGS.WORKFLOW_ENGINE != "dask":
+        return partial(func, **params)
+
+    func = strip_decorator(func)
+    return job(partial(func, **params))
 
 
 def customize_funcs(
@@ -126,11 +142,11 @@ def customize_funcs(
         raise ValueError("Invalid function name: 'all' is a reserved name.")
     if bad_decorator_keys := [k for k in decorators if k not in names and k != "all"]:
         raise ValueError(
-            f"Invalid decorator keys: {bad_decorator_keys}. " f"Valid keys are: {names}"
+            f"Invalid decorator keys: {bad_decorator_keys}. Valid keys are: {names}"
         )
     if bad_parameter_keys := [k for k in parameters if k not in names and k != "all"]:
         raise ValueError(
-            f"Invalid parameter keys: {bad_parameter_keys}. " f"Valid keys are: {names}"
+            f"Invalid parameter keys: {bad_parameter_keys}. Valid keys are: {names}"
         )
 
     for func_name, func in funcs_dict.items():
@@ -145,7 +161,4 @@ def customize_funcs(
             func_ = redecorate(func_, decorators[func_name])
         updated_funcs.append(func_)
 
-    if len(updated_funcs) == 1:
-        return updated_funcs[0]
-
-    return tuple(updated_funcs)
+    return updated_funcs[0] if len(updated_funcs) == 1 else tuple(updated_funcs)
diff --git a/src/quacc/wflow_tools/decorators.py b/src/quacc/wflow_tools/decorators.py
index dde2adaf..028adbd9 100644
--- a/src/quacc/wflow_tools/decorators.py
+++ b/src/quacc/wflow_tools/decorators.py
@@ -24,9 +24,6 @@ def job(_func: Callable | None = None, **kwargs) -> Job:
     All `#!Python @job`-decorated functions are transformed into their corresponding
     decorator.
 
-    The wrapped function can also be stripped of its decorator by calling the
-    `#!Python .__wrapped__` attribute.
-
     ```python
     from quacc import job
 
@@ -114,43 +111,34 @@ def job(_func: Callable | None = None, **kwargs) -> Job:
 
     from quacc import SETTINGS
 
-    wflow_engine = SETTINGS.WORKFLOW_ENGINE
-
     if _func is None:
         return partial(job, **kwargs)
 
-    if wflow_engine == "covalent":
+    elif SETTINGS.WORKFLOW_ENGINE == "covalent":
         import covalent as ct
 
-        decorated = ct.electron(_func, **kwargs)
-    elif wflow_engine == "jobflow":
+        return ct.electron(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "dask":
+        from dask import delayed
+
+        return delayed(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "jobflow":
         from jobflow import job as jf_job
 
-        decorated = jf_job(_func, **kwargs)
-    elif wflow_engine == "parsl":
+        return jf_job(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "parsl":
         from parsl import python_app
 
-        decorated = python_app(_func, **kwargs)
-    elif wflow_engine == "redun":
+        return python_app(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "redun":
         from redun import task as redun_task
 
-        decorated = redun_task(_func, **kwargs)
-    elif wflow_engine == "dask":
-        from dask import delayed
-
-        decorated = delayed(_func, **kwargs)
+        return redun_task(_func, **kwargs)
     else:
-        decorated = _func
-
-    if not hasattr(decorated, "__wrapped__"):
-        decorated.__wrapped__ = _func
-
-    return decorated
+        return _func
 
 
-def flow(
-    _func: Callable | None = None, **kwargs
-) -> Flow:  # sourcery skip: lift-return-into-if, switch
+def flow(_func: Callable | None = None, **kwargs) -> Flow:
     """
     Decorator for workflows, which consist of at least one compute job. This is a
     `#!Python @flow` decorator.
@@ -263,27 +251,19 @@ def flow(
     if _func is None:
         return partial(flow, **kwargs)
 
-    wflow_engine = SETTINGS.WORKFLOW_ENGINE
-    if wflow_engine == "covalent":
+    elif SETTINGS.WORKFLOW_ENGINE == "covalent":
         import covalent as ct
 
-        decorated = ct.lattice(_func, **kwargs)
-    elif wflow_engine == "redun":
+        return ct.lattice(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "redun":
         from redun import task as redun_task
 
-        decorated = redun_task(_func, **kwargs)
+        return redun_task(_func, **kwargs)
     else:
-        decorated = _func
+        return _func
 
-    if wflow_engine != "covalent" and not hasattr(decorated, "__wrapped__"):
-        decorated.__wrapped__ = _func
 
-    return decorated
-
-
-def subflow(
-    _func: Callable | None = None, **kwargs
-) -> Subflow:  # sourcery skip: lift-return-into-if, switch
+def subflow(_func: Callable | None = None, **kwargs) -> Subflow:
     """
     Decorator for (dynamic) sub-workflows. This is a `#!Python @subflow` decorator.
 
@@ -446,6 +426,7 @@ def subflow(
     callable
         The decorated function.
     """
+    from quacc import SETTINGS
 
     @wraps(_func)
     def _inner(
@@ -473,36 +454,30 @@ def subflow(
         """
         decorator_kwargs = decorator_kwargs if decorator_kwargs is not None else kwargs
 
-        if wflow_engine == "dask":
+        if SETTINGS.WORKFLOW_ENGINE == "dask":
             from dask import delayed
 
             decorated = delayed(_func, **decorator_kwargs)
             return decorated(*f_args, **f_kwargs).compute()
-
-    from quacc import SETTINGS
+        else:
+            raise NotImplementedError("Only Dask is supported for this inner function.")
 
     if _func is None:
         return partial(subflow, **kwargs)
 
-    wflow_engine = SETTINGS.WORKFLOW_ENGINE
-    if wflow_engine == "covalent":
+    elif SETTINGS.WORKFLOW_ENGINE == "covalent":
         import covalent as ct
 
-        decorated = ct.electron(ct.lattice(_func), **kwargs)
-    elif wflow_engine == "parsl":
+        return ct.electron(ct.lattice(_func), **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "parsl":
         from parsl import join_app
 
-        decorated = join_app(_func, **kwargs)
-    elif wflow_engine == "redun":
+        return join_app(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "redun":
         from redun import task as redun_task
 
-        decorated = redun_task(_func, **kwargs)
-    elif wflow_engine == "dask":
+        return redun_task(_func, **kwargs)
+    elif SETTINGS.WORKFLOW_ENGINE == "dask":
         return _inner
     else:
-        decorated = _func
-
-    if wflow_engine != "covalent" and not hasattr(decorated, "__wrapped__"):
-        decorated.__wrapped__ = _func
-
-    return decorated
+        return _func


Test patch:
diff --git a/tests/core/wflow/test_decorators.py b/tests/core/wflow/test_decorators.py
index cdce91f3..53587cd6 100644
--- a/tests/core/wflow/test_decorators.py
+++ b/tests/core/wflow/test_decorators.py
@@ -35,9 +35,6 @@ def test_decorators(tmp_path, monkeypatch):
     assert workflow(1, 2, 3) == 9
     assert dynamic_workflow(1, 2, 3) == [6, 6, 6]
 
-    assert add.__wrapped__(1, 2) == 3
-    assert mult.__wrapped__(1, 2) == 2
-
 
 def test_decorators_v2(tmp_path, monkeypatch):
     monkeypatch.chdir(tmp_path)
diff --git a/tests/covalent/test_covalent_syntax.py b/tests/covalent/test_covalent_syntax.py
index e9aff441..114ffb18 100644
--- a/tests/covalent/test_covalent_syntax.py
+++ b/tests/covalent/test_covalent_syntax.py
@@ -1,12 +1,13 @@
 import pytest
 
-from quacc import SETTINGS, flow, job, subflow
+from quacc import SETTINGS, flow, job, strip_decorator, subflow
 
 ct = pytest.importorskip("covalent")
 pytestmark = pytest.mark.skipif(
     SETTINGS.WORKFLOW_ENGINE != "covalent",
     reason="This test requires the Covalent workflow engine",
 )
+from covalent._workflow.lattice import Lattice
 
 
 def test_covalent_decorators(tmp_path, monkeypatch):
@@ -88,3 +89,34 @@ def test_covalent_decorators_args(tmp_path, monkeypatch):
         6,
         6,
     ]
+
+
+def test_strip_decorators():
+    @job
+    def add(a, b):
+        return a + b
+
+    @flow
+    def add2(a, b):
+        return a + b
+
+    @subflow
+    def add3(a, b):
+        return a + b
+
+    assert hasattr(add, "electron_object")
+    stripped_add = strip_decorator(add)
+    assert stripped_add(1, 2) == 3
+    assert not hasattr(stripped_add, "electron_object")
+
+    assert isinstance(add2, Lattice)
+    stripped_add2 = strip_decorator(add2)
+    assert stripped_add2(1, 2) == 3
+    assert not hasattr(stripped_add2, "electron_object")
+    assert not isinstance(stripped_add2, Lattice)
+
+    assert hasattr(add3, "electron_object")
+    stripped_add3 = strip_decorator(add3)
+    assert stripped_add3(1, 2) == 3
+    assert not hasattr(stripped_add3, "electron_object")
+    assert not isinstance(stripped_add3, Lattice)
diff --git a/tests/dask/test_dask_syntax.py b/tests/dask/test_dask_syntax.py
index 696b9b67..46f43315 100644
--- a/tests/dask/test_dask_syntax.py
+++ b/tests/dask/test_dask_syntax.py
@@ -1,6 +1,6 @@
 import pytest
 
-from quacc import SETTINGS, flow, job, subflow
+from quacc import SETTINGS, flow, job, strip_decorator, subflow
 
 dask = pytest.importorskip("dask")
 pytestmark = pytest.mark.skipif(
@@ -80,3 +80,26 @@ def test_dask_decorators_args(tmp_path, monkeypatch):
     assert client.compute(mult(1, 2)).result() == 2
     assert client.compute(workflow(1, 2, 3)).result() == 9
     assert dask.compute(*client.gather(dynamic_workflow(1, 2, 3))) == (6, 6, 6)
+
+
+def test_strip_decorators():
+    @job
+    def add(a, b):
+        return a + b
+
+    @flow
+    def add2(a, b):
+        return a + b
+
+    @subflow
+    def add3(a, b):
+        return a + b
+
+    stripped_add = strip_decorator(add)
+    assert stripped_add(1, 2) == 3
+
+    stripped_add2 = strip_decorator(add2)
+    assert stripped_add2(1, 2) == 3
+
+    stripped_add3 = strip_decorator(add3)
+    assert stripped_add3(1, 2) == 3
diff --git a/tests/jobflow/test_jobflow_syntax.py b/tests/jobflow/test_jobflow_syntax.py
index 31df3f9a..e7dc8385 100644
--- a/tests/jobflow/test_jobflow_syntax.py
+++ b/tests/jobflow/test_jobflow_syntax.py
@@ -1,6 +1,6 @@
 import pytest
 
-from quacc import SETTINGS, flow, job, subflow
+from quacc import SETTINGS, flow, job, strip_decorator, subflow
 
 jf = pytest.importorskip("jobflow")
 pytestmark = pytest.mark.skipif(
@@ -65,3 +65,12 @@ def test_jobflow_decorators_args(tmp_path, monkeypatch):
     assert isinstance(mult(1, 2), jf.Job)
     assert isinstance(workflow(1, 2, 3), jf.Job)
     assert isinstance(add_distributed([1, 2, 3], 4)[0], jf.Job)
+
+
+def test_strip_decorators():
+    @job
+    def add(a, b):
+        return a + b
+
+    stripped_add = strip_decorator(add)
+    assert stripped_add(1, 2) == 3
diff --git a/tests/parsl/test_parsl_syntax.py b/tests/parsl/test_parsl_syntax.py
index 19032807..1f7a2e0b 100644
--- a/tests/parsl/test_parsl_syntax.py
+++ b/tests/parsl/test_parsl_syntax.py
@@ -2,7 +2,7 @@ import contextlib
 
 import pytest
 
-from quacc import SETTINGS, flow, job, subflow
+from quacc import SETTINGS, flow, job, strip_decorator, subflow
 
 parsl = pytest.importorskip("parsl")
 pytestmark = pytest.mark.skipif(
@@ -88,3 +88,26 @@ def test_parsl_decorators_args(tmp_path, monkeypatch):
     assert mult(1, 2).result() == 2
     assert workflow(1, 2, 3).result() == 9
     assert dynamic_workflow(1, 2, 3).result() == [6, 6, 6]
+
+
+def test_strip_decorators():
+    @job
+    def add(a, b):
+        return a + b
+
+    @flow
+    def add2(a, b):
+        return a + b
+
+    @subflow
+    def add3(a, b):
+        return a + b
+
+    stripped_add = strip_decorator(add)
+    assert stripped_add(1, 2) == 3
+
+    stripped_add2 = strip_decorator(add2)
+    assert stripped_add2(1, 2) == 3
+
+    stripped_add3 = strip_decorator(add3)
+    assert stripped_add3(1, 2) == 3
diff --git a/tests/redun/test_redun_syntax.py b/tests/redun/test_redun_syntax.py
index f1a98671..eae779b7 100644
--- a/tests/redun/test_redun_syntax.py
+++ b/tests/redun/test_redun_syntax.py
@@ -1,6 +1,6 @@
 import pytest
 
-from quacc import SETTINGS, flow, job, subflow
+from quacc import SETTINGS, flow, job, strip_decorator, subflow
 
 redun = pytest.importorskip("redun")
 pytestmark = pytest.mark.skipif(
@@ -47,3 +47,26 @@ def test_redun_decorators(tmp_path, monkeypatch, scheduler):
     assert scheduler.run(mult(1, 2)) == 2
     assert scheduler.run(workflow(1, 2, 3)) == 9
     assert scheduler.run(dynamic_workflow(1, 2, 3)) == [6, 6, 6]
+
+
+def test_strip_decorators():
+    @job
+    def add(a, b):
+        return a + b
+
+    @flow
+    def add2(a, b):
+        return a + b
+
+    @subflow
+    def add3(a, b):
+        return a + b
+
+    stripped_add = strip_decorator(add)
+    assert stripped_add(1, 2) == 3
+
+    stripped_add2 = strip_decorator(add2)
+    assert stripped_add2(1, 2) == 3
+
+    stripped_add3 = strip_decorator(add3)
+    assert stripped_add3(1, 2) == 3