pdoc3 / pdoc

:snake: :arrow_right: :scroll: Auto-generate API documentation for Python projects
https://pdoc3.github.io/pdoc/
GNU Affero General Public License v3.0
1.12k stars 145 forks source link

pdoc trips over unittest.mock.Mock #350

Closed Terrance closed 2 months ago

Terrance commented 3 years ago

Expected behavior

I can document classes that include mock object members (for example, when documenting testing utilities).

Actual behavior

Traceback ``` Traceback (most recent call last): File "/usr/bin/pdoc", line 33, in sys.exit(load_entry_point('pdoc3', 'console_scripts', 'pdoc')()) File "/usr/lib/python3.9/site-packages/pdoc/cli.py", line 575, in main recursive_write_files(module, ext='.html', **template_config) File "/usr/lib/python3.9/site-packages/pdoc/cli.py", line 351, in recursive_write_files recursive_write_files(submodule, ext=ext, **kwargs) File "/usr/lib/python3.9/site-packages/pdoc/cli.py", line 346, in recursive_write_files f.write(m.html(**kwargs)) File "/usr/lib/python3.9/site-packages/pdoc/__init__.py", line 880, in html html = _render_template('/html.mako', module=self, **kwargs) File "/usr/lib/python3.9/site-packages/pdoc/__init__.py", line 155, in _render_template return t.render(**config).strip() File "/usr/lib/python3.9/site-packages/mako/template.py", line 473, in render return runtime._render(self, self.callable_, args, data) File "/usr/lib/python3.9/site-packages/mako/runtime.py", line 878, in _render _render_context( File "/usr/lib/python3.9/site-packages/mako/runtime.py", line 920, in _render_context _exec_template(inherit, lclcontext, args=args, kwargs=kwargs) File "/usr/lib/python3.9/site-packages/mako/runtime.py", line 947, in _exec_template callable_(context, *args, **kwargs) File "_html_mako", line 143, in render_body File "_html_mako", line 45, in show_module File "_html_mako", line 500, in render_show_module File "_html_mako", line 321, in show_func File "/usr/lib/python3.9/site-packages/pdoc/__init__.py", line 1431, in params return self._params(self, annotate=annotate, link=link, module=self.module) File "/usr/lib/python3.9/site-packages/pdoc/__init__.py", line 1447, in _params signature = inspect.signature(doc_obj.obj) File "/usr/lib/python3.9/inspect.py", line 3130, in signature return Signature.from_callable(obj, follow_wrapped=follow_wrapped) File "/usr/lib/python3.9/inspect.py", line 2879, in from_callable return _signature_from_callable(obj, sigcls=cls, File "/usr/lib/python3.9/inspect.py", line 2330, in _signature_from_callable return _signature_from_function(sigcls, obj, File "/usr/lib/python3.9/inspect.py", line 2173, in _signature_from_function positional = arg_names[:pos_count] TypeError: 'Mock' object is not subscriptable ```

A naive solution would be to just not treat mocks as functions:

diff --git a/pdoc/__init__.py b/pdoc/__init__.py
index 0f05b2c..7ac7eb7 100644
--- a/pdoc/__init__.py
+++ b/pdoc/__init__.py
@@ -27,4 +27,5 @@ from typing import (  # noqa: F401
     Optional, Set, Tuple, Type, TypeVar, Union,
 )
+from unittest.mock import Mock
 from warnings import warn

@@ -411,5 +412,5 @@ def _is_public(ident_name):

 def _is_function(obj):
-    return inspect.isroutine(obj) and callable(obj)
+    return inspect.isroutine(obj) and callable(obj) and not isinstance(obj, Mock)

To reproduce

class MockService:

    async def _run(value: int) -> int: ...
    run = AsyncMock(spec=_run, side_effect=lambda value: value + 1)

Additional info

kernc commented 3 years ago

This looks like one of the mock bugs in Python since inspect.signature(MockService.run) is what raises.

I guess we can apply the proposed workaround. Can you make it a pull request?

kernc commented 3 years ago
+    return inspect.isroutine(obj) and callable(obj) and not isinstance(obj, Mock)

This will then fall through to interpreting MockService.run as a non-callable variable. How do we feel about that?

Terrance commented 3 years ago

For my use case it's acceptable enough -- I only really need the bare minimum for documentation on these classes (namely the class docstring), and having non-descript members for mock objects at least indicates they're present.

Ideally it'd show the signature of the mock's function spec if it has one, though that doesn't seem to be easily accessible (Mock._spec_signature), and there's probably a whole rabbit hole of other mock "types" to consider. Can we just identify it as a Mock object somehow?