Repository: PrefectHQ/prefect

Issue:
Result location not set when passing target with task_args
## Description
When `target` is passed to @task it is set on the `result` in the task.__init__() method.  If the target is passed through `task_args={"target"="a.data"}`. It is not set and the `result` falls back to the default_location.

## Expected Behavior
The location that the `result` uses should be the same regardless of the method used to set the `target`.

## Reproduction
```python
"""
Set in environment.

PREFECT__FLOWS__CHECKPOINTING=true
"""

from prefect import task, Flow
from prefect.engine.results import LocalResult


@task(
    target="decorator.data", result=LocalResult(dir="."),
)
def gen_decorator():
    return "gen-decorator"


@task
def gen_task_args():
    return "gen-task-args"


with Flow("Target") as flow:
    gen_decorator()
    gen_task_args(
        task_args={"target": "task-args.data", "result": LocalResult(dir="."),}
    )

    state = flow.run()
```

## Environment
Prefect 0.11.1

## Discussion
In `Task.__init__()`:

```python
        if target and result:
            self.result = result.copy()  # type: ignore
            self.result.location = target

            if getattr(result, "location", None):
                warnings.warn(
                    "Both `result.location` and `target` set on task. Task result will use target as location."
                )
```

This works fine if target is available through @task.  If it isn't set yet then `location` isn't set.

In `Task.__call__()` the task is copied.

```python
new = self.copy(**(task_args or {}))
```

In `Task.copy(self, **task_args)`. The `target` and `result` are added to the copy of the task, but location is not set on the result.

```python
        # check task_args
        for attr, val in task_args.items():
            if not hasattr(new, attr):
                raise AttributeError(
                    "{0} does not have {1} as an attribute".format(self, attr)
                )
            else:
                setattr(new, attr, val)
```




Discussion:
That's a good call @nathan5280 - I think the best way to solve it is for us to move all task initialization logic to a new method (`Task.initialize`?) and call that method at the end of both `Task.__init__()` and `Task.copy()`. That way any time attributes are set (creation and copying), the same processing logic can be applied.

Resolving patch:
diff --git a/src/prefect/core/task.py b/src/prefect/core/task.py
--- a/src/prefect/core/task.py
+++ b/src/prefect/core/task.py
@@ -125,33 +125,40 @@ def run(self, x, y):
         - max_retries (int, optional): The maximum amount of times this task can be retried
         - retry_delay (timedelta, optional): The amount of time to wait until task is retried
         - timeout (int, optional): The amount of time (in seconds) to wait while
-            running this task before a timeout occurs; note that sub-second resolution is not supported
-        - trigger (callable, optional): a function that determines whether the task should run, based
-                on the states of any upstream tasks.
+            running this task before a timeout occurs; note that sub-second
+            resolution is not supported
+        - trigger (callable, optional): a function that determines whether the
+            task should run, based on the states of any upstream tasks.
         - skip_on_upstream_skip (bool, optional): if `True`, if any immediately
-                upstream tasks are skipped, this task will automatically be skipped as well,
-                regardless of trigger. By default, this prevents tasks from attempting to use either state or data
-                from tasks that didn't run. If `False`, the task's trigger will be called as normal,
-                with skips considered successes. Defaults to `True`.
+            upstream tasks are skipped, this task will automatically be skipped as
+            well, regardless of trigger. By default, this prevents tasks from
+            attempting to use either state or data from tasks that didn't run. If
+            `False`, the task's trigger will be called as normal, with skips
+            considered successes. Defaults to `True`.
         - cache_for (timedelta, optional, DEPRECATED): The amount of time to maintain a cache
             of the outputs of this task.  Useful for situations where the containing Flow
             will be rerun multiple times, but this task doesn't need to be.
         - cache_validator (Callable, optional, DEPRECATED): Validator that will determine
             whether the cache for this task is still valid (only required if `cache_for`
             is provided; defaults to `prefect.engine.cache_validators.duration_only`)
-        - cache_key (str, optional, DEPRECATED): if provided, a `cache_key` serves as a unique identifier for this Task's cache, and can
-            be shared across both Tasks _and_ Flows; if not provided, the Task's _name_ will be used if running locally, or the
-            Task's database ID if running in Cloud
+        - cache_key (str, optional, DEPRECATED): if provided, a `cache_key`
+            serves as a unique identifier for this Task's cache, and can be shared
+            across both Tasks _and_ Flows; if not provided, the Task's _name_ will
+            be used if running locally, or the Task's database ID if running in
+            Cloud 
         - checkpoint (bool, optional): if this Task is successful, whether to
-            store its result using the `result_handler` available during the run; Also note that
-            checkpointing will only occur locally if `prefect.config.flows.checkpointing` is set to `True`
-        - result_handler (ResultHandler, optional, DEPRECATED): the handler to use for
-            retrieving and storing state results during execution; if not provided, will default to the
-            one attached to the Flow
-        - result (Result, optional): the result instance used to retrieve and store task results during execution
-        - target (str, optional): location to check for task Result. If a result exists at that location then
-            the task run will enter a cached state.  `target` strings can be templated formatting strings which will be formatted
-            at runtime with values from `prefect.context`
+            store its result using the `result_handler` available during the run;
+            Also note that checkpointing will only occur locally if
+            `prefect.config.flows.checkpointing` is set to `True`
+        - result_handler (ResultHandler, optional, DEPRECATED): the handler to
+            use for retrieving and storing state results during execution; if not
+            provided, will default to the one attached to the Flow
+        - result (Result, optional): the result instance used to retrieve and
+            store task results during execution
+        - target (str, optional): location to check for task Result. If a result
+            exists at that location then the task run will enter a cached state.
+            `target` strings can be templated formatting strings which will be
+            formatted at runtime with values from `prefect.context`
         - state_handlers (Iterable[Callable], optional): A list of state change handlers
             that will be called whenever the task changes state, providing an
             opportunity to inspect or modify the new state. The handler
@@ -160,8 +167,9 @@ def run(self, x, y):
                 `state_handler(task: Task, old_state: State, new_state: State) -> Optional[State]`
             If multiple functions are passed, then the `new_state` argument will be the
             result of the previous handler.
-        - on_failure (Callable, optional): A function with signature `fn(task: Task, state: State) -> None`
-            with will be called anytime this Task enters a failure state
+        - on_failure (Callable, optional): A function with signature 
+            `fn(task: Task, state: State) -> None` that will be called anytime this 
+            Task enters a failure state
         - log_stdout (bool, optional): Toggle whether or not to send stdout messages to
             the Prefect logger. Defaults to `False`.
 
@@ -268,14 +276,19 @@ def __init__(
 
         self.target = target
 
-        if target and result:
-            self.result = result.copy()  # type: ignore
-            self.result.location = target
-
-            if getattr(result, "location", None):
+        # if both a target and a result were provided, update the result location
+        # to point at the target
+        if self.target and self.result:
+            if (
+                getattr(self.result, "location", None)
+                and self.result.location != self.target
+            ):
                 warnings.warn(
-                    "Both `result.location` and `target` set on task. Task result will use target as location."
+                    "Both `result.location` and `target` were provided. "
+                    "The `target` value will be used."
                 )
+            self.result = self.result.copy()
+            self.result.location = self.target
 
         if state_handlers and not isinstance(state_handlers, collections.abc.Sequence):
             raise TypeError("state_handlers should be iterable.")
@@ -367,6 +380,20 @@ def copy(self, **task_args: Any) -> "Task":
             else:
                 setattr(new, attr, val)
 
+        # if both a target and a result were provided, update the result location
+        # to point at the target
+        if new.target and new.result:
+            if (
+                getattr(new.result, "location", None)
+                and new.result.location != new.target
+            ):
+                warnings.warn(
+                    "Both `result.location` and `target` were provided. "
+                    "The `target` value will be used."
+                )
+            new.result = new.result.copy()
+            new.result.location = new.target
+
         new.tags = copy.deepcopy(self.tags).union(set(new.tags))
         tags = set(prefect.context.get("tags", set()))
         new.tags.update(tags)


Test patch:
diff --git a/tests/core/test_task.py b/tests/core/test_task.py
--- a/tests/core/test_task.py
+++ b/tests/core/test_task.py
@@ -1,3 +1,4 @@
+from unittest.mock import MagicMock
 import json
 import logging
 import uuid
@@ -10,7 +11,7 @@
 from prefect.core import Edge, Flow, Parameter, Task
 from prefect.engine.cache_validators import all_inputs, duration_only, never_use
 from prefect.engine.result_handlers import JSONResultHandler, ResultHandler
-from prefect.engine.results import PrefectResult
+from prefect.engine.results import PrefectResult, LocalResult
 from prefect.utilities.configuration import set_temporary_config
 from prefect.utilities.tasks import task
 
@@ -418,6 +419,23 @@ def test_copy_accepts_slug_as_task_args(self):
         assert t.slug == "test"
         assert t2.slug == "test-2"
 
+    def test_copy_appropriately_sets_result_target_if_target_provided(self):
+        # https://github.com/PrefectHQ/prefect/issues/2588
+        @task(target="target", result=LocalResult(dir="."))
+        def X():
+            pass
+
+        @task
+        def Y():
+            pass
+
+        with Flow("test"):
+            x = X()
+            y = Y(task_args=dict(target="target", result=LocalResult(dir=".")))
+
+        assert x.result.location == "target"
+        assert y.result.location == "target"
+
 
 def test_task_has_slug():
     t1 = Task()