diff --git a/autoapi/documenters.py b/autoapi/documenters.py index 8464e91a..e2687832 100644 --- a/autoapi/documenters.py +++ b/autoapi/documenters.py @@ -1,5 +1,7 @@ import re +import sphinx.util.logging + from sphinx.ext import autodoc from ._objects import ( @@ -13,6 +15,9 @@ ) +LOGGER = sphinx.util.logging.getLogger(__name__) + + class AutoapiDocumenter(autodoc.Documenter): def get_attr(self, obj, name, *defargs): attrgetters = self.env.app.registry.autodoc_attrgettrs @@ -33,7 +38,14 @@ def get_attr(self, obj, name, *defargs): raise AttributeError(name) - def import_object(self): + def import_object(self) -> bool: + """Imports and sets the object to be documented. + + The object is searched in the autoapi_objects dict based on the fullname attribute of the documenter. + + Returns: + bool: True if the object was successfully imported and set, False otherwise. + """ max_splits = self.fullname.count(".") for num_splits in range(max_splits, -1, -1): path_stack = list(reversed(self.fullname.rsplit(".", num_splits))) @@ -50,6 +62,14 @@ def import_object(self): self._method_parent = parent return True + # If we get here, the object was not found. Emit a warning as autodoc does. + LOGGER.warning( + "Failed to import %s '%s'", + self.directivetype, + self.fullname, + type="autoapi", + subtype="import" + ) return False def get_real_modname(self): diff --git a/docs/changes/419.feature.rst b/docs/changes/419.feature.rst new file mode 100644 index 00000000..1e1fec01 --- /dev/null +++ b/docs/changes/419.feature.rst @@ -0,0 +1 @@ +Add warning message for missing objects diff --git a/tests/python/pymissing_import/conf.py b/tests/python/pymissing_import/conf.py new file mode 100644 index 00000000..1c8c118a --- /dev/null +++ b/tests/python/pymissing_import/conf.py @@ -0,0 +1,15 @@ +templates_path = ["_templates"] +source_suffix = ".rst" +master_doc = "index" +project = "pymissing_import" +author = "Arwed Starke" +version = "0.1" +release = "0.1" +language = "en" +exclude_patterns = ["_build"] +pygments_style = "sphinx" +html_theme = "alabaster" +extensions = ["sphinx.ext.autodoc", "autoapi.extension"] +autoapi_dirs = ["example"] +autoapi_python_class_content = "both" +autoapi_generate_api_docs = False diff --git a/tests/python/pymissing_import/example/example.py b/tests/python/pymissing_import/example/example.py new file mode 100644 index 00000000..b915cf6e --- /dev/null +++ b/tests/python/pymissing_import/example/example.py @@ -0,0 +1,201 @@ +"""Example module + +This is a description +""" + +from dataclasses import dataclass +from functools import cached_property + +A_TUPLE = ("a", "b") +"""A tuple to be rendered as a tuple.""" +A_LIST = ["c", "d"] +"""A list to be rendered as a list.""" + + +class Foo: + """Can we parse arguments from the class docstring? + + :param attr: Set an attribute. + :type attr: str + """ + + class_var = 42 #: Class var docstring + + another_class_var = 42 + """Another class var docstring""" + + class Meta: + """A nested class just to test things out""" + + @classmethod + def foo(): + """The foo class method""" + return True + + def __init__(self, attr): + """Constructor docstring""" + self.attr = attr + self.attr2 = attr + """This is the docstring of an instance attribute. + + :type: str + """ + + @property + def attr(self): + return 5 + + @attr.setter + def attr(self, value): + pass + + @property + def property_simple(self) -> int: + """This property should parse okay.""" + return 42 + + @cached_property + def my_cached_property(self) -> int: + """This cached property should be a property.""" + return 42 + + def method_okay(self, foo=None, bar=None): + """This method should parse okay""" + return True + + def method_multiline(self, foo=None, bar=None, baz=None): + """This is on multiple lines, but should parse okay too + + pydocstyle gives us lines of source. Test if this means that multiline + definitions are covered in the way we're anticipating here + """ + return True + + def method_tricky(self, foo=None, bar=dict(foo=1, bar=2)): + """This will likely fail our argument testing + + We parse naively on commas, so the nested dictionary will throw this off + """ + return True + + def method_sphinx_docs(self, foo, bar=0): + """This method is documented with sphinx style docstrings. + + :param foo: The first argument. + :type foo: int + + :param int bar: The second argument. + + :returns: The sum of foo and bar. + :rtype: int + """ + return foo + bar + + def method_google_docs(self, foo, bar=0): + """This method is documented with google style docstrings. + + Args: + foo (int): The first argument. + bar (int): The second argument. + + Returns: + int: The sum of foo and bar. + """ + return foo + bar + + def method_sphinx_unicode(self): + """This docstring uses unicodé. + + :returns: A string. + :rtype: str + """ + return "sphinx" + + def method_google_unicode(self): + """This docstring uses unicodé. + + Returns: + str: A string. + """ + return "google" + + +Foo.bar = "dinglebop" + + +def decorator_okay(func): + """This decorator should parse okay.""" + + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +class Bar(Foo): + def method_okay(self, foo=None, bar=None): + pass + + +class ClassWithNoInit: + pass + + +class One: + """One.""" + + def __init__(self): + """One __init__.""" + super().__init__() + + +class MultilineOne(One): + """This is a naughty summary line + that exists on two lines.""" + + +class Two(One): + """Two.""" + + +class Three: + __init__ = Two.__init__ + + +def fn_with_long_sig( + this, + *, + function=None, + has=True, + quite=True, + a, + long, + signature, + many, + keyword, + arguments, +): + """A function with a long signature.""" + + +TYPED_DATA: int = 1 +"""This is TYPED_DATA.""" + + +@dataclass +class TypedAttrs: + one: str + """This is TypedAttrs.one.""" + two: int = 1 + """This is TypedAttrs.two.""" + + +class TypedClassInit: + """This is TypedClassInit.""" + + def __init__(self, one: int = 1) -> None: + self._one = one + + def typed_method(self, two: int) -> int: + """This is TypedClassInit.typed_method.""" + return self._one + two diff --git a/tests/python/pymissing_import/index.rst b/tests/python/pymissing_import/index.rst new file mode 100644 index 00000000..cfaf4763 --- /dev/null +++ b/tests/python/pymissing_import/index.rst @@ -0,0 +1,20 @@ +.. pyexample documentation master file, created by + sphinx-quickstart on Fri May 29 13:34:37 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to pyexample's documentation! +===================================== + +.. toctree:: + :glob: + + manualapi + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/tests/python/pymissing_import/manualapi.rst b/tests/python/pymissing_import/manualapi.rst new file mode 100644 index 00000000..b3320865 --- /dev/null +++ b/tests/python/pymissing_import/manualapi.rst @@ -0,0 +1,9 @@ +Autodoc Directives +================== + +.. autoapimodule:: example + :members: + + +.. autoapimodule:: nonexisting_module + :members: diff --git a/tests/python/test_pyintegration.py b/tests/python/test_pyintegration.py index b589d6a8..77caf265 100644 --- a/tests/python/test_pyintegration.py +++ b/tests/python/test_pyintegration.py @@ -1329,6 +1329,21 @@ def test_nothing_to_render_raises_warning(builder, caplog): ) +def test_missing_object_raises_warning(builder, caplog): + caplog.set_level(logging.WARNING, logger="autoapi._mapper") + if sphinx_version >= (8, 1): + status = builder("pymissing_import", warningiserror=True) + assert status + else: + with pytest.raises(sphinx.errors.SphinxWarning): + builder("pymissing_import", warningiserror=True) + + assert any( + "Failed to import module 'nonexisting_module'" in record.message + for record in caplog.records + ) + + class TestStdLib: """Check that modules with standard library names are still documented."""