Repository: PrefectHQ/prefect

Issue:
Use of @curry with task decorator breaks type checking
## Description
<!-- A clear description of the bug -->

If I use the task decorator with arguments, pylint complains that I am not passing `fn`.

e.g.
```python
@task(tags = ['foo'])
def foo():
    return True
```
Results in:
![image](https://user-images.githubusercontent.com/193170/88876559-85c7aa80-d1f1-11ea-8620-7e8cc80148fa.png)

## Expected Behavior
<!-- What did you expect to happen instead? -->

No linting error


## Reproduction
<!-- A minimal example that exhibits the behavior. -->

(perhaps not needed)


## Environment
<!-- Any additional information about your environment

Optionally run `prefect diagnostics` from the command line and paste the information here. -->

I'm not exactly sure how best to fix this; typing `@curry` seems to be tricky. Still I'm not sure if this is an upstream bug either (or what, exactly, upstream, is broken).
Using static type checking as qa on a Flow library
## Current behavior
As a user developing `Flow`s, I struggle with debugging flows, since each of the tasks represents delayed execution. Running a flow is the most complete test, but since flows can change the state of external systems, it would be helpful to have some qa that my flow is set-up correctly before running/registering. This is especially impactful in the situation where I have a [flow library](https://docs.prefect.io/orchestration/tutorial/multiple.html) and want to implement this qa as part of a CI pipeline.




## Proposed behavior
For flows with `Task`s that have data dependencies between them, I could qa on the types passed between different tasks. Right now, tasks are typed to hint at the types encountered when the flow is registered. Imagine a scenario, where the types hinted at the task execution result (the result of the delayed execution). You could run mypy and ensure that the dependencies between flows have sensible types (plus your IDE will be more helpful)




## Example
```
from typing import Any, Callable, TypeVar

import prefect
import prefect.tasks.core.function


F = TypeVar('F', bound=Callable[..., Any])


def typed_task(fn: F, **task_init_kwargs: Any) -> F:
    """Same dynamic code as `prefect.task`, but preserves function signature for debugging in a Flow context.
    """
    return prefect.tasks.core.function.FunctionTask(fn=fn, **task_init_kwargs)  # type: ignore


@typed_task
def task_one() -> str:
    return "1"


@typed_task
def task_two(arg: str) -> int:
    return int(arg)


with prefect.Flow("test_type_hints") as flow:
    result_one: str = task_one()  # typed to hint at the execution result 
    result_two: int = task_two(result_one)
flow.register()
```



Discussion:

Update: It won't be possible to support this until PEP 612 and changes to mypy go live https://github.com/python/mypy/issues/8645

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
@@ -77,6 +77,21 @@ def __new__(cls, name: str, parents: tuple, methods: dict) -> "SignatureValidato
         return type.__new__(cls, name, parents, methods)  # type: ignore
 
 
+class instance_property:
+    """Like property, but only available on instances, not the class"""
+
+    def __init__(self, func: Callable):
+        self.func = func
+
+    def __getattr__(self, k: str) -> Any:
+        return getattr(self.func, k)
+
+    def __get__(self, obj: Any, cls: Any) -> Any:
+        if obj is None:
+            raise AttributeError
+        return self.func(obj)
+
+
 class Task(metaclass=SignatureValidator):
     """
     The Task class which is used as the full representation of a unit of work.
@@ -426,7 +441,7 @@ def copy(self, **task_args: Any) -> "Task":
 
         return new
 
-    @property
+    @instance_property
     def __signature__(self) -> inspect.Signature:
         """Dynamically generate the signature, replacing ``*args``/``**kwargs``
         with parameters from ``run``"""
diff --git a/src/prefect/tasks/core/resource_manager.py b/src/prefect/tasks/core/resource_manager.py
--- a/src/prefect/tasks/core/resource_manager.py
+++ b/src/prefect/tasks/core/resource_manager.py
@@ -1,6 +1,4 @@
-from typing import Any, Callable, Dict
-
-from toolz import curry
+from typing import Any, Callable, Dict, Union, Set, overload
 
 import prefect
 from prefect import Task, Flow
@@ -67,7 +65,7 @@ def __init__(
         self.setup_task = setup_task
         self.cleanup_task = cleanup_task
         self._flow = flow
-        self._tasks = set()
+        self._tasks = set()  # type: Set[Task]
 
     def add_task(self, task: Task, flow: Flow) -> None:
         """Add a new task under the resource manager block.
@@ -82,12 +80,12 @@ def add_task(self, task: Task, flow: Flow) -> None:
             )
         self._tasks.add(task)
 
-    def __enter__(self):
+    def __enter__(self) -> Task:
         self.__prev_resource = prefect.context.get("resource")
         prefect.context.update(resource=self)
         return self.setup_task
 
-    def __exit__(self, *args):
+    def __exit__(self, *args: Any) -> None:
         if self.__prev_resource is None:
             prefect.context.pop("resource", None)
         else:
@@ -200,7 +198,7 @@ def __call__(self, *args: Any, flow: Flow = None, **kwargs: Any) -> ResourceCont
             if flow is None:
                 raise ValueError("Could not infer an active Flow context.")
 
-        init_task = prefect.task(self.resource_class, **self.init_task_kwargs)(
+        init_task = prefect.task(self.resource_class, **self.init_task_kwargs)(  # type: ignore
             *args, flow=flow, **kwargs
         )
 
@@ -213,14 +211,39 @@ def __call__(self, *args: Any, flow: Flow = None, **kwargs: Any) -> ResourceCont
         return ResourceContext(init_task, setup_task, cleanup_task, flow)
 
 
-@curry
+# To support mypy type checking with optional arguments to `resource_manager`,
+# we need to make use of `typing.overload`
+@overload
 def resource_manager(
     resource_class: Callable,
+    *,
     name: str = None,
     init_task_kwargs: dict = None,
     setup_task_kwargs: dict = None,
     cleanup_task_kwargs: dict = None,
 ) -> ResourceManager:
+    pass
+
+
+@overload
+def resource_manager(
+    *,
+    name: str = None,
+    init_task_kwargs: dict = None,
+    setup_task_kwargs: dict = None,
+    cleanup_task_kwargs: dict = None,
+) -> Callable[[Callable], ResourceManager]:
+    pass
+
+
+def resource_manager(
+    resource_class: Callable = None,
+    *,
+    name: str = None,
+    init_task_kwargs: dict = None,
+    setup_task_kwargs: dict = None,
+    cleanup_task_kwargs: dict = None,
+) -> Union[ResourceManager, Callable[[Callable], ResourceManager]]:
     """A decorator for creating a `ResourceManager` object.
 
     Used as a context manager, `ResourceManager` objects create tasks to setup
@@ -295,10 +318,14 @@ def cleanup(self, client):
     a trigger to always run if the `setup` task succeeds, and won't be set as
     a reference task.
     """
-    return ResourceManager(
-        resource_class,
-        name=name,
-        init_task_kwargs=init_task_kwargs,
-        setup_task_kwargs=setup_task_kwargs,
-        cleanup_task_kwargs=cleanup_task_kwargs,
-    )
+
+    def inner(resource_class: Callable) -> ResourceManager:
+        return ResourceManager(
+            resource_class,
+            name=name,
+            init_task_kwargs=init_task_kwargs,
+            setup_task_kwargs=setup_task_kwargs,
+            cleanup_task_kwargs=cleanup_task_kwargs,
+        )
+
+    return inner if resource_class is None else inner(resource_class)
diff --git a/src/prefect/utilities/tasks.py b/src/prefect/utilities/tasks.py
--- a/src/prefect/utilities/tasks.py
+++ b/src/prefect/utilities/tasks.py
@@ -4,10 +4,9 @@
 from contextlib import contextmanager
 from datetime import timedelta
 from functools import wraps
-from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional
+from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, overload, Union
 
 import pendulum
-from toolz import curry
 
 import prefect
 
@@ -330,10 +329,26 @@ def add(x, y):
         )
 
 
-@curry
+# To support mypy type checking with optional arguments to `task`, we need to
+# make use of `typing.overload`
+@overload
+def task(__fn: Callable) -> "prefect.tasks.core.function.FunctionTask":
+    pass
+
+
+@overload
+def task(
+    **task_init_kwargs: Any,
+) -> Callable[[Callable], "prefect.tasks.core.function.FunctionTask"]:
+    pass
+
+
 def task(
-    fn: Callable, **task_init_kwargs: Any
-) -> "prefect.tasks.core.function.FunctionTask":
+    fn: Callable = None, **task_init_kwargs: Any
+) -> Union[
+    "prefect.tasks.core.function.FunctionTask",
+    Callable[[Callable], "prefect.tasks.core.function.FunctionTask"],
+]:
     """
     A decorator for creating Tasks from functions.
 
@@ -380,6 +395,11 @@ def fn_with_args(x):
         fn_with_args(1)
     ```
     """
+    if fn is None:
+        return lambda fn: prefect.tasks.core.function.FunctionTask(
+            fn=fn,
+            **task_init_kwargs,
+        )
     return prefect.tasks.core.function.FunctionTask(fn=fn, **task_init_kwargs)
 
 


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
@@ -249,6 +249,10 @@ def run(self, x: int, y: bool, z: int = 1):
         )
         assert sig.return_annotation == "Task"
 
+        # doesn't override class signature
+        class_sig = inspect.signature(Test)
+        assert "name" in class_sig.parameters
+
     def test_create_task_with_and_without_cache_for(self):
         t1 = Task()
         assert t1.cache_validator is never_use