Repository: numba/numba

Issue:
Feature Request: Add support for vectorizing a jitted function
Currently it throws an _"unsupported callable"_ TypeError

``` python
In [151]: type(func)
Out[151]: numba.targets.registry.CPUDispatcher

In [152]: f = numba.vectorize(func.signatures, target='cpu')(func)
Traceback (most recent call last):

  File "<ipython-input-152-79c4e42b865b>", line 1, in <module>
    f = numba.vectorize(func.signatures, target='cpu')(func)

  File "C:\Python\lib\site-packages\numba\npyufunc\decorators.py", line 107, in wrap
    vec = Vectorize(func, **kws)

  File "C:\Python\lib\site-packages\numba\npyufunc\decorators.py", line 33, in __new__
    return imp(func, identity, kws)

  File "C:\Python\lib\site-packages\numba\npyufunc\dufunc.py", line 80, in __init__
    super(DUFunc, self).__init__(dispatcher, **kws)

  File "C:\Python\lib\inspect.py", line 1043, in getargspec
    getfullargspec(func)

  File "C:\Python\lib\inspect.py", line 1095, in getfullargspec
    raise TypeError('unsupported callable') from ex

TypeError: unsupported callable
```



Discussion:
A workaround for now:

``` python
def vectorize(func, *, target='cpu'):
    """Vectorizes a jumba jitted function `func`

    Parameters
    ----------
    func : numba.dispatcher.Dispatcher
        A function which has already been jitted

    Optional Parameters
    -------------------    
    target : str, {'cpu', 'parallel', 'cuda'}
        Any valid ufunc target

    Returns
    -------
    DUFunc
        A numba Dynamic Universal Function which wraps the 
        jitted function `func`

    """
    import inspect
    from textwrap import dedent
    from numba import vectorize
    from numba.dispatcher import Dispatcher
    assert isinstance(func, Dispatcher)
    code = """
    @vectorize({signatures}, target={target!r})
    def {func_name}({parameters}):
        return func({parameters})
    """
    name = func.__name__
    parameters = inspect.signature(func).parameters
    parameters = ', '.join(parameters.keys())
    code = code.format(
        signatures=func.signatures,
        target=target,
        func_name=name,
        parameters=parameters,
    )
    exec(dedent(code))
    return locals()[name]
```

@sklam: Ideas for how to best handle this inside Numba directly?

The simplest workaround is probably:

``` python
In [1]: from numba import njit, vectorize

In [2]: @njit
   ...: def foo(a, b):
   ...:     return a + b
   ...: 

In [3]: foo(1, 2)
Out[3]: 3

In [4]: foo(3, 5)
Out[4]: 8

In [5]: bar = vectorize(target='cpu')(foo.py_func)

In [6]: bar(1,2)
Out[6]: 3

In [7]: bar([1,2], [3, 5])
Out[7]: array([4, 7])

```

Directly using the signature from CPUDispatcher is probably not preferred, since `@vectorize` expects arguments types that can be stored in numpy arrays and CPUDispatcher supports more types.  

That's a lot cleaner than my solution! It could perhaps be made to work automatically with a simple `isinstance` check:

``` python
if isinstance(func, Dispatcher):
    return vectorize(**kwargs)(func.py_func)
else:
    ...
```


Resolving patch:
diff --git a/numba/npyufunc/dufunc.py b/numba/npyufunc/dufunc.py
--- a/numba/npyufunc/dufunc.py
+++ b/numba/npyufunc/dufunc.py
@@ -6,6 +6,7 @@
 from ..typing import npydecl
 from ..typing.templates import AbstractTemplate, signature
 from . import _internal, ufuncbuilder
+from ..dispatcher import Dispatcher
 
 
 def make_dufunc_kernel(_dufunc):
@@ -73,6 +74,8 @@ class DUFunc(_internal._DUFunc):
     __base_kwargs = set(('identity', '_keepalive', 'nin', 'nout'))
 
     def __init__(self, py_func, identity=None, targetoptions={}):
+        if isinstance(py_func, Dispatcher):
+            py_func = py_func.py_func
         self.targetoptions = targetoptions.copy()
         kws = {}
         kws['identity'] = ufuncbuilder.parse_identity(identity)


Test patch:
diff --git a/numba/tests/npyufunc/test_vectorize_decor.py b/numba/tests/npyufunc/test_vectorize_decor.py
--- a/numba/tests/npyufunc/test_vectorize_decor.py
+++ b/numba/tests/npyufunc/test_vectorize_decor.py
@@ -5,7 +5,7 @@
 import numpy as np
 
 from numba import unittest_support as unittest
-from numba import int32, uint32, float32, float64, vectorize
+from numba import int32, uint32, float32, float64, jit, vectorize
 from ..support import tag
 
 
@@ -28,88 +28,77 @@ def vector_add(a, b):
     return a + b
 
 
-class BaseVectorizeDecor(unittest.TestCase):
-    def _run_and_compare(self, numba_func, numpy_func):
-        A = np.arange(100, dtype=np.float64)
-        result = numba_func(A)
-        gold = numpy_func(A)
-        np.testing.assert_allclose(result, gold)
-
-    def _test_template_1(self, target):
-        numba_sinc = vectorize(['float64(float64)', 'float32(float32)'],
-                               target=target)(sinc)
-        numpy_sinc = np.vectorize(sinc)
-        self._run_and_compare(numba_sinc, numpy_sinc)
-
-    def _test_template_2(self, target):
-        numba_sinc = vectorize([float64(float64), float32(float32)],
-                               target=target)(sinc)
-        numpy_sinc = np.vectorize(sinc)
-        self._run_and_compare(numba_sinc, numpy_sinc)
-
-    def _test_template_3(self, target):
-        numba_scaled_sinc = vectorize(['float64(float64, uint32)'],
-                                      target=target)(scaled_sinc)
-        numpy_scaled_sinc = np.vectorize(scaled_sinc)
-        A = np.arange(100, dtype=np.float64)
-        scale = np.uint32(3)
-        result = numba_scaled_sinc(A, scale)
-        gold = numpy_scaled_sinc(A, scale)
-        np.testing.assert_allclose(result, gold, atol=1e-8)
-
-    def _test_template_4(self, target):
-        sig = [int32(int32, int32),
-               uint32(uint32, uint32),
-               float32(float32, float32),
-               float64(float64, float64)]
-        basic_ufunc = vectorize(sig, target=target)(vector_add)
-        np_ufunc = np.add
-
-        def test(ty):
-            data = np.linspace(0., 100., 500).astype(ty)
-            result = basic_ufunc(data, data)
-            gold = np_ufunc(data, data)
-            np.testing.assert_allclose(gold, result)
-
-        test(np.double)
-        test(np.float32)
-        test(np.int32)
-        test(np.uint32)
-
-
-class TestVectorizeDecor(BaseVectorizeDecor):
+class BaseVectorizeDecor(object):
+    target = None
+    wrapper = None
+    funcs = {
+        'func1': sinc,
+        'func2': scaled_sinc,
+        'func3': vector_add,
+    }
+
+    @classmethod
+    def _run_and_compare(cls, func, sig, A, *args, **kwargs):
+        if cls.wrapper is not None:
+            func = cls.wrapper(func)
+        numba_func = vectorize(sig, target=cls.target)(func)
+        numpy_func = np.vectorize(func)
+        result = numba_func(A, *args)
+        gold = numpy_func(A, *args)
+        np.testing.assert_allclose(result, gold, **kwargs)
 
     @tag('important')
-    def test_cpu_1(self):
-        self._test_template_1('cpu')
+    def test_1(self):
+        sig = ['float64(float64)', 'float32(float32)']
+        func = self.funcs['func1']
+        A = np.arange(100, dtype=np.float64)
+        self._run_and_compare(func, sig, A)
 
     @tag('important')
-    def test_parallel_1(self):
-        self._test_template_1('parallel')
-
+    def test_2(self):
+        sig = [float64(float64), float32(float32)]
+        func = self.funcs['func1']
+        A = np.arange(100, dtype=np.float64)
+        self._run_and_compare(func, sig, A)
+    
     @tag('important')
-    def test_cpu_2(self):
-        self._test_template_2('cpu')
+    def test_3(self):
+        sig = ['float64(float64, uint32)']
+        func = self.funcs['func2']
+        A = np.arange(100, dtype=np.float64)
+        scale = np.uint32(3)
+        self._run_and_compare(func, sig, A, scale, atol=1e-8)
 
     @tag('important')
-    def test_parallel_2(self):
-        self._test_template_2('parallel')
+    def test_4(self):
+        sig = [
+            int32(int32, int32),
+            uint32(uint32, uint32),
+            float32(float32, float32),
+            float64(float64, float64),
+        ]
+        func = self.funcs['func3']
+        A = np.arange(100, dtype=np.float64)
+        self._run_and_compare(func, sig, A, A)
+        A = A.astype(np.float32)
+        self._run_and_compare(func, sig, A, A)
+        A = A.astype(np.int32)
+        self._run_and_compare(func, sig, A, A)
+        A = A.astype(np.uint32)
+        self._run_and_compare(func, sig, A, A)
 
-    @tag('important')
-    def test_cpu_3(self):
-        self._test_template_3('cpu')
 
-    @tag('important')
-    def test_parallel_3(self):
-        self._test_template_3('parallel')
+class TestCPUVectorizeDecor(unittest.TestCase, BaseVectorizeDecor):
+    target = 'cpu'
 
-    @tag('important')
-    def test_cpu_4(self):
-        self._test_template_4('cpu')
 
-    @tag('important')
-    def test_parallel_4(self):
-        self._test_template_4('parallel')
+class TestParallelVectorizeDecor(unittest.TestCase, BaseVectorizeDecor):
+    target = 'parallel'
+
+
+class TestCPUVectorizeJitted(unittest.TestCase, BaseVectorizeDecor):
+    target = 'cpu'
+    wrapper = staticmethod(jit)  # staticmethod required for py27
 
 
 if __name__ == '__main__':