Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify FunctionTask wrapper handling with functools.update_wrapper #4608

Merged
merged 4 commits into from
Jun 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changes/pr4608.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

enhancement:
- "Use `functools.update_wrapper` for `FunctionTask` - [#4608](https://github.com/PrefectHQ/prefect/pull/4608)"
38 changes: 8 additions & 30 deletions src/prefect/tasks/core/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,19 @@
"""

from typing import Any, Callable
from functools import update_wrapper

import prefect


class _DocProxy(object):
"""A descriptor that proxies through the docstring for the wrapped task as
the docstring for a `FunctionTask` instance."""

def __init__(self, cls_doc):
self._cls_doc = cls_doc

def __get__(self, obj, cls):
if obj is None:
return self._cls_doc
else:
return getattr(obj.run, "__doc__", None) or self._cls_doc


class FunctionTask(prefect.Task):
__doc__ = _DocProxy(
"""A convenience Task for functionally creating Task instances with
"""A convenience Task for functionally creating Task instances with
arbitrary callable `run` methods.

Args:
- fn (callable): the function to be the task's `run` method
- name (str, optional): the name of this task
- name (str, optional): the name of this task; if not provided it is inferred
as the function name
- **kwargs: keyword arguments that will be passed to the Task
constructor

Expand All @@ -47,26 +34,17 @@ class FunctionTask(prefect.Task):
result = task(42)
```
"""
)

def __init__(self, fn: Callable, name: str = None, **kwargs: Any):
if not callable(fn):
raise TypeError("fn must be callable.")
raise TypeError("`fn` must be callable")

# set the name from the fn
# Set the Prefect name from the function
if name is None:
name = getattr(fn, "__name__", type(self).__name__)

prefect.core.task._validate_run_signature(fn) # type: ignore
prefect.core.task._validate_run_signature(fn)
self.run = fn
update_wrapper(self, fn)

super().__init__(name=name, **kwargs)

def __getattr__(self, k):
if k == "__wrapped__":
return self.run
raise AttributeError(
f"'FunctionTask' object has no attribute {k}."
" Did you call this object within a function that should have been"
"decorated with @prefect.task?"
)
12 changes: 6 additions & 6 deletions tests/tasks/core/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ def my_fn():
f = FunctionTask(fn=my_fn)
assert f.__doc__ == my_fn.__doc__

# Except when no docstring on wrapped function
# Lambdas do not have a function docstring
f = FunctionTask(fn=lambda x: x + 1)
assert "FunctionTask" in f.__doc__
assert f.__doc__ is None

def test_function_task_sets__wrapped__(self):
def my_fn():
Expand All @@ -77,12 +77,12 @@ def my_fn():
pass

t = FunctionTask(fn=my_fn)
with pytest.raises(AttributeError) as exc:
with pytest.raises(
AttributeError,
match="'FunctionTask' object has no attribute 'unknown_attribute'",
):
t.unknown_attribute

assert "unknown_attribute" in str(exc.value)
assert "@prefect.task" in str(exc.value)


class TestCollections:
def test_list_returns_a_list(self):
Expand Down