Repository: numba/numba

Issue:
pytest doctest can't discover functions with @vectorize decorator
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/main/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.


This problem seems very similar to the issue raised in #6308, which has already been fixed.

Here's my minimum working example:

```python
"""
Test that all docstrings are the same:

>>> len({f.__doc__ for f in (a, b, c, d)})
1
"""
from numba import guvectorize, int64, njit, vectorize


def a():
    """>>> x = 1"""
    return 1


@njit
def b():
    """>>> x = 1"""
    return 1


@guvectorize([(int64[:], int64, int64[:])], "(n),()->(n)")
def c(x, y, res):
    """>>> x = 1"""
    for i in range(x.shape[0]):
        res[i] = x[i] + y


@vectorize([int64(int64, int64)])
def d(x, y):
    """>>> x = 1"""
    return x + y

```

When I run pytest I get the following results:

```
pytest -rs -sv --doctest-modules mwe.py
============================== test session starts ===============================
platform win32 -- Python 3.10.8, pytest-7.2.0, pluggy-1.0.0 -- C:\Users\MYUSER\Miniconda3\envs\fdm\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\MYUSER\local_work\sketches\temp
plugins: cov-4.0.0
collected 4 items

mwe.py::mwe PASSED
mwe.py::mwe.a PASSED
mwe.py::mwe.b PASSED
mwe.py::mwe.c PASSED

=============================== 4 passed in 0.71s ================================
```
As all docstrings are the same, I would expect pytest to run a doctest for the function `d`, as it did for `a`, `b`, and `c`.



Discussion:
I think doctest will only pickup functions. The `@vectorize` decorator transforms functions into some callable objects.
Does any of the information / code in #5755 help here with this issue?
```
import functools

def what():
    """
    >>> "HERE"
    """

class A:
    def __call__(self):
        """
        >>> "THERE"
        """
        return what()


a = functools.wraps(what)(A())

if __name__ == "__main__":
    import doctest
    doctest.testmod()
```

`functools.wraps` did help doctest to pick up `__main__.a`
It's just missing a `.__module__` on the vectorized object.

Resolving patch:
diff --git a/numba/np/ufunc/dufunc.py b/numba/np/ufunc/dufunc.py
--- a/numba/np/ufunc/dufunc.py
+++ b/numba/np/ufunc/dufunc.py
@@ -1,3 +1,5 @@
+import functools
+
 from numba import jit, typeof
 from numba.core import cgutils, types, serialize, sigutils
 from numba.core.extending import is_jitted
@@ -83,6 +85,7 @@ def __init__(self, py_func, identity=None, cache=False, targetoptions={}):
                          cache=cache,
                          **targetoptions)(py_func)
         self._initialize(dispatcher, identity)
+        functools.update_wrapper(self, py_func)
 
     def _initialize(self, dispatcher, identity):
         identity = ufuncbuilder.parse_identity(identity)


Test patch:
diff --git a/numba/tests/doctest_usecase.py b/numba/tests/doctest_usecase.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/doctest_usecase.py
@@ -0,0 +1,31 @@
+"""
+Test that all docstrings are the same:
+
+>>> len({f.__doc__ for f in (a, b, c, d)})
+1
+"""
+from numba import guvectorize, int64, njit, vectorize
+
+
+def a():
+    """>>> x = 1"""
+    return 1
+
+
+@njit
+def b():
+    """>>> x = 1"""
+    return 1
+
+
+@guvectorize([(int64[:], int64, int64[:])], "(n),()->(n)")
+def c(x, y, res):
+    """>>> x = 1"""
+    for i in range(x.shape[0]):
+        res[i] = x[i] + y
+
+
+@vectorize([int64(int64, int64)])
+def d(x, y):
+    """>>> x = 1"""
+    return x + y
diff --git a/numba/tests/test_doctest.py b/numba/tests/test_doctest.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_doctest.py
@@ -0,0 +1,28 @@
+import doctest
+import unittest
+from numba.tests.support import TestCase
+
+
+class TestDocTest(TestCase):
+    def test_basic_decorators(self):
+        from . import doctest_usecase
+
+        # Make sure the finder see all the doctest
+        finder = doctest.DocTestFinder()
+        tests = finder.find(doctest_usecase)
+        testnames = {x.name for x in tests}
+        expected = {
+            'numba.tests.doctest_usecase',
+            'numba.tests.doctest_usecase.a',
+            'numba.tests.doctest_usecase.b',
+            'numba.tests.doctest_usecase.c',
+            'numba.tests.doctest_usecase.d',
+        }
+        self.assertEqual(testnames, expected)
+
+        # Execute the doctest in the module
+        doctest.testmod(doctest_usecase)
+
+
+if __name__ == "__main__":
+    unittest.main()