mansenfranzen / autodoc_pydantic

Seamlessly integrate pydantic models in your Sphinx documentation.
MIT License
158 stars 27 forks source link

Mocked objects as typehints for class variables #150

Open brian-phasecraft opened 1 year ago

brian-phasecraft commented 1 year ago

Hi,

I have noticed that using mock-imported objects as typehints within classes that inherit BaseModel causes the documentation to fail to build.

For example, mock_issue.py

from pydantic import BaseModel
from fake_package import FakeClass

class MockedImport(BaseModel):
    r"""
    Include some field which uses a typehint from a mocked package.
    """

    foo: FakeClass = None  #: Uses typehint which is imported from mocked package

    class Config:
        arbitrary_types_allowed = True

with conf.py containing

autodoc_mock_imports = [
    "fake_package",
]

extensions = [
    "sphinx.ext.autodoc",
    "sphinxcontrib.apidoc",
    "sphinxcontrib.autodoc_pydantic",
]

results in HTML files where MockedImport is not documented.

Secondarily, other files in the same project are also not documented due to this error. Commenting foo leads to complete documentation. I am not sure if this is intended / known behaviour, but any fix / workaround would be greatly appreciated.


The error is:

WARNING: autodoc: failed to import module 'mock_import' from module 'apidoc_issue'; the following exception was raised:
Traceback (most recent call last):
  File "/home/brian/.virtualenvs/harness/lib/python3.10/site-packages/sphinx/ext/autodoc/importer.py", line 66, in import_module
    return importlib.import_module(modname)
  File "/home/brian/.pyenv/versions/3.10.8/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/brian/projects/another_doc_demo/apidoc_issue/__init__.py", line 1, in <module>
    from apidoc_issue.mock_import import *
  File "/home/brian/projects/another_doc_demo/apidoc_issue/mock_import.py", line 5, in <module>
    class MockedImport(BaseModel):
  File "pydantic/main.py", line 197, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/fields.py", line 506, in pydantic.fields.ModelField.infer
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 552, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 668, in pydantic.fields.ModelField._type_analysis
  File "/home/brian/.pyenv/versions/3.10.8/lib/python3.10/typing.py", line 1158, in __subclasscheck__
    return issubclass(cls, self.__origin__)
TypeError: issubclass() arg 1 must be a class
brian-phasecraft commented 1 year ago

Note that omitting sphinxcontrib.autodoc_pydantic from conf.py has the same effect, so this may not be a problem of autodoc-pydantic. But removing BaseModel from the class declaration also allows for documentation to build, so the issue is somewhere in the interplay of sphinx/autodoc/pydantic, so hopefully this is the right place to start.

mansenfranzen commented 1 year ago

@brian-phasecraft Thanks for reporting this bug!

I agree with your observation. Given the stack trace, the error originates within sphinx/ext/autodoc/importer.py. In my opinion, it is not related to autodoc_pydantic but rather is a result of the interplay between autodoc_mock_imports, sphinx.ext.autodoc and pydantic.

Using autodoc_mock_imports, you typically want to prevent import errors for packages that are not available during build time. However, in your case, the configuration does not seem to apply.

It would be interesting to see if the same error occurs if you completely remove pydantic from your snippet and instead employ a plain python class.