Repository: PrefectHQ/prefect

Issue:
Handle "unsupported callables" for task arg introspection
Follow up from https://github.com/PrefectHQ/prefect/pull/897#discussion_r272286805

Builtin functions and functions like `np.dot` are "unsupported callables" because `inspect` can't determine their signature, which means we can't build arguments automatically. For example, this fails:

```pytyon
prefect.task(np.dot)(x, y)
```

Currently, functions like this need to be nested in a pure-Python function:

```python
@task
def mydot(x, y):
    return np.dot(x, y)
```


Discussion:
How strange. I tried just about every inspection library I could think of:
`inspect.signature(np.dot) --> ValueError: no signature found for builtin <built-in function dot>`
`dis.dis(np.dot) --> TypeError: don't know how to disassemble builtin_function_or_method objects`
`dill.source.getsource(np.dot) --> OSError: could not find class definition`

Nothing seems to be able to get at the source of Numpy's builtins.

Looking into Numpy source, it seems like something even they haven't found a way around: https://github.com/numpy/numpy/blob/master/numpy/core/multiarray.py#L66

So the signature is right there in the file (https://github.com/numpy/numpy/blob/master/numpy/core/multiarray.py#L694), and the file would no doubt be on the machine / in the container of the user. Which makes it seem like there has to be some way to at least get the signature as a string. But I haven't found it yet.
Purely for informational purposes: the way Python interfaces with CPython is via "code objects" which describe how a block of Python code should be executed by CPython.  For standard Python objects (like the ones we all create), Python creates a code object for you:
```python
def f(x, y):
    pass

f.__code__
# code-object

f.__code__.co_varnames
# ('x', 'y')
```
As you can see, the code object contains information about the call signature.  I'm fairly certain all of these "inspection" packages inspect this code object.  

However, for built-ins (and also numpy) which themselves are basically just interfaces to C / CPython code, Python doesn't do this nice step for us (the step has already been done behind the scenes, in a sense).  Consequently there's no nicely packaged code object to inspect, and they error out.

[the-more-you-know.jpg]

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
@@ -34,7 +34,19 @@
 
 def _validate_run_signature(run: Callable) -> None:
     func = getattr(run, "__wrapped__", run)
-    run_sig = inspect.getfullargspec(func)
+    try:
+        run_sig = inspect.getfullargspec(func)
+    except TypeError as exc:
+        if str(exc) == "unsupported callable":
+            raise ValueError(
+                "This function can not be inspected (this is common "
+                "with `builtin` and `numpy` functions). In order to "
+                "use it as a task, please wrap it in a standard "
+                "Python function. For more detail, see "
+                "https://docs.prefect.io/core/tutorials/task-guide.html#the-task-decorator"
+            )
+        raise
+
     if run_sig.varargs:
         raise ValueError(
             "Tasks with variable positional arguments (*args) are not "
@@ -126,7 +138,7 @@ def run(self, x, y):
             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 
+            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): the handler to use for
             retrieving and storing state results during execution; if not provided, will default to the


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
@@ -228,6 +228,10 @@ def run(x, task_args):
             def run(x, task_args=None):
                 pass
 
+    def test_class_instantiation_raises_helpful_warning_for_unsupported_callables(self):
+        with pytest.raises(ValueError, match="This function can not be inspected"):
+            task(zip)
+
     def test_create_task_with_and_without_cache_for(self):
         t1 = Task()
         assert t1.cache_validator is never_use