From b14c0ccd5d7a5c947d04444ce59e96df79ce9734 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Wed, 25 Sep 2024 16:47:18 -0700 Subject: [PATCH] Fix types missing from documentation generated by autodoc-style directives Fixes #473 --- autoapi/documenters.py | 35 ++++++++---- docs/changes/473.bugfix.rst | 1 + tests/python/pyexample/example/example.py | 24 ++++++++ tests/python/test_pyintegration.py | 68 +++++++++++++++++++++++ 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 docs/changes/473.bugfix.rst diff --git a/autoapi/documenters.py b/autoapi/documenters.py index d37d3558..8464e91a 100644 --- a/autoapi/documenters.py +++ b/autoapi/documenters.py @@ -98,7 +98,7 @@ def format_signature(self, **kwargs): class AutoapiFunctionDocumenter( - AutoapiDocumenter, autodoc.FunctionDocumenter, _AutoapiDocstringSignatureMixin + AutoapiDocumenter, _AutoapiDocstringSignatureMixin, autodoc.FunctionDocumenter ): objtype = "apifunction" directivetype = "function" @@ -147,9 +147,7 @@ def format_args(self, **kwargs): return "(" + to_format + ")" -class AutoapiClassDocumenter( - AutoapiDocumenter, autodoc.ClassDocumenter, _AutoapiDocstringSignatureMixin -): +class AutoapiClassDocumenter(AutoapiDocumenter, autodoc.ClassDocumenter): objtype = "apiclass" directivetype = "class" doc_as_attr = False @@ -174,9 +172,18 @@ def add_directive_header(self, sig): bases = ", ".join(f":class:`{base}`" for base in self.object.bases) self.add_line(f" Bases: {bases}", sourcename) + def format_signature(self, **kwargs): + # Set "manual" attributes at the last possible moment. + # This is to let a manual entry or docstring searching happen first, + # and falling back to the discovered signature only when necessary. + if self.args is None: + self.args = self.object.args + + return super().format_signature(**kwargs) + class AutoapiMethodDocumenter( - AutoapiDocumenter, autodoc.MethodDocumenter, _AutoapiDocstringSignatureMixin + AutoapiDocumenter, _AutoapiDocstringSignatureMixin, autodoc.MethodDocumenter ): objtype = "apimethod" directivetype = "method" @@ -230,9 +237,6 @@ def add_directive_header(self, sig): autodoc.ClassLevelDocumenter.add_directive_header(self, sig) sourcename = self.get_sourcename() - if self.options.annotation and self.options.annotation is not autodoc.SUPPRESS: - self.add_line(f" :type: {self.options.annotation}", sourcename) - for property_type in ( "abstractmethod", "classmethod", @@ -240,6 +244,9 @@ def add_directive_header(self, sig): if property_type in self.object.properties: self.add_line(f" :{property_type}:", sourcename) + if self.object.annotation: + self.add_line(f" :type: {self.object.annotation}", sourcename) + class AutoapiDataDocumenter(AutoapiDocumenter, autodoc.DataDocumenter): objtype = "apidata" @@ -254,14 +261,16 @@ def add_directive_header(self, sig): autodoc.ModuleLevelDocumenter.add_directive_header(self, sig) sourcename = self.get_sourcename() if not self.options.annotation: - # TODO: Change sphinx to allow overriding of object description if self.object.value is not None: - self.add_line(f" :annotation: = {self.object.value}", sourcename) + self.add_line(f" :value: {self.object.value}", sourcename) elif self.options.annotation is autodoc.SUPPRESS: pass else: self.add_line(f" :annotation: {self.options.annotation}", sourcename) + if self.object.annotation: + self.add_line(f" :type: {self.object.annotation}", sourcename) + class AutoapiAttributeDocumenter(AutoapiDocumenter, autodoc.AttributeDocumenter): objtype = "apiattribute" @@ -277,14 +286,16 @@ def add_directive_header(self, sig): autodoc.ClassLevelDocumenter.add_directive_header(self, sig) sourcename = self.get_sourcename() if not self.options.annotation: - # TODO: Change sphinx to allow overriding of object description if self.object.value is not None: - self.add_line(f" :annotation: = {self.object.value}", sourcename) + self.add_line(f" :value: {self.object.value}", sourcename) elif self.options.annotation is autodoc.SUPPRESS: pass else: self.add_line(f" :annotation: {self.options.annotation}", sourcename) + if self.object.annotation: + self.add_line(f" :type: {self.object.annotation}", sourcename) + class AutoapiModuleDocumenter(AutoapiDocumenter, autodoc.ModuleDocumenter): objtype = "apimodule" diff --git a/docs/changes/473.bugfix.rst b/docs/changes/473.bugfix.rst new file mode 100644 index 00000000..2df86421 --- /dev/null +++ b/docs/changes/473.bugfix.rst @@ -0,0 +1 @@ +Fix types missing from documentation generated by autodoc-style directives. diff --git a/tests/python/pyexample/example/example.py b/tests/python/pyexample/example/example.py index cdc69cca..4f88de68 100644 --- a/tests/python/pyexample/example/example.py +++ b/tests/python/pyexample/example/example.py @@ -3,6 +3,7 @@ This is a description """ +from dataclasses import dataclass from functools import cached_property A_TUPLE = ("a", "b") @@ -171,3 +172,26 @@ def fn_with_long_sig( 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/test_pyintegration.py b/tests/python/test_pyintegration.py index dddfbe49..4e3dbd61 100644 --- a/tests/python/test_pyintegration.py +++ b/tests/python/test_pyintegration.py @@ -177,6 +177,74 @@ def test_manual_directives(self, parse): example_file = parse("_build/html/manualapi.html") assert example_file.find(id="example.decorator_okay") + def test_dataclass(self, parse): + example_file = parse("_build/html/manualapi.html") + + typedattrs_sig = example_file.find(id="example.TypedAttrs") + assert typedattrs_sig + + typedattrs = typedattrs_sig.parent + + one = typedattrs.find(id="example.TypedAttrs.one") + assert one + one_value = one.find_all(class_="property") + assert one_value[0].text == ": str" + one_docstring = one.parent.find("dd").contents[0].text + assert one_docstring.strip() == "This is TypedAttrs.one." + + two = typedattrs.find(id="example.TypedAttrs.two") + assert two + two_value = two.find_all(class_="property") + assert two_value[0].text == ": int" + assert two_value[1].text == " = 1" + two_docstring = two.parent.find("dd").contents[0].text + assert two_docstring.strip() == "This is TypedAttrs.two." + + def test_data(self, parse): + example_file = parse("_build/html/manualapi.html") + + typed_data = example_file.find(id="example.TYPED_DATA") + assert typed_data + typed_data_value = typed_data.find_all(class_="property") + assert typed_data_value[0].text == ": int" + assert typed_data_value[1].text == " = 1" + + typed_data_docstring = typed_data.parent.find("dd").contents[0].text + assert typed_data_docstring.strip() == "This is TYPED_DATA." + + def test_class(self, parse): + example_file = parse("_build/html/manualapi.html") + + typed_cls = example_file.find(id="example.TypedClassInit") + assert typed_cls + arg = typed_cls.find(class_="sig-param") + assert arg.text == "one: int = 1" + typed_cls_docstring = typed_cls.parent.find("dd").contents[0].text + assert typed_cls_docstring.strip() == "This is TypedClassInit." + + typed_method = example_file.find(id="example.TypedClassInit.typed_method") + assert typed_method + arg = typed_method.find(class_="sig-param") + assert arg.text == "two: int" + return_type = typed_method.find(class_="sig-return-typehint") + assert return_type.text == "int" + typed_method_docstring = typed_method.parent.find("dd").contents[0].text + assert typed_method_docstring.strip() == "This is TypedClassInit.typed_method." + + def test_property(self, parse): + example_file = parse("_build/html/manualapi.html") + + foo_sig = example_file.find(id="example.Foo") + assert foo_sig + foo = foo_sig.parent + + property_simple = foo.find(id="example.Foo.property_simple") + assert property_simple + property_simple_value = property_simple.find_all(class_="property") + assert property_simple_value[-1].text == ": int" + property_simple_docstring = property_simple.parent.find("dd").text.strip() + assert property_simple_docstring == "This property should parse okay." + class TestMovedConfPy(TestSimpleModule): @pytest.fixture(autouse=True, scope="class")