diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d78e340..d621595 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,11 +10,15 @@ Features ^^^^^^^^ * (Python) Can read per argument type comments with astroid > 2.2.5. +* (Python) Added autoapidecorator directive with Sphinx >= 2.0. +* (Python) Can use autodoc_docstring_signature with Autodoc-style directives. Bug Fixes ^^^^^^^^^ * (Python) Forward reference annotations are no longer rendered as strings. +* (Python) autoapifunction directive no longer documents async functions as + a normal function. v1.1.0 (2019-06-23) diff --git a/autoapi/documenters.py b/autoapi/documenters.py index c3197a2..336655c 100644 --- a/autoapi/documenters.py +++ b/autoapi/documenters.py @@ -1,3 +1,6 @@ +import re + +import sphinx from sphinx.ext import autodoc from .mappers.python import ( @@ -79,7 +82,23 @@ class AutoapiDocumenter(autodoc.Documenter): return False, sorted(children) -class AutoapiFunctionDocumenter(AutoapiDocumenter, autodoc.FunctionDocumenter): +class _AutoapiDocstringSignatureMixin(object): + 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 + + if self.retann is None: + self.retann = self.object.return_annotation + + return super(_AutoapiDocstringSignatureMixin, self).format_signature(**kwargs) + + +class AutoapiFunctionDocumenter( + AutoapiDocumenter, autodoc.FunctionDocumenter, _AutoapiDocstringSignatureMixin +): objtype = "apifunction" directivetype = "function" # Always prefer AutoapiDocumenters @@ -92,18 +111,49 @@ class AutoapiFunctionDocumenter(AutoapiDocumenter, autodoc.FunctionDocumenter): def format_args(self, **kwargs): return "(" + self.object.args + ")" - def format_signature(self, **kwargs): - # Set "introspected" attributes at the last possible minute - if self.args is None: - self.args = self.object.args + def add_directive_header(self, sig): + if sphinx.version_info >= (2, 1): + autodoc.Documenter.add_directive_header(self, sig) - if self.retann is None: - self.retann = self.object.return_annotation + if "async" in self.object.properties: + sourcename = self.get_sourcename() + self.add_line(" :async:", sourcename) + else: + super(AutoapiFunctionDocumenter, self).add_directive_header(sig) + + +if sphinx.version_info >= (2,): - return super(AutoapiFunctionDocumenter, self).format_signature(**kwargs) + class AutoapiDecoratorDocumenter( + AutoapiFunctionDocumenter, AutoapiDocumenter, autodoc.DecoratorDocumenter + ): + objtype = "apidecorator" + directivetype = "decorator" + priority = autodoc.DecoratorDocumenter.priority * 100 + 100 + def format_signature(self, **kwargs): + if self.args is None: + self.args = self.format_args(**kwargs) -class AutoapiClassDocumenter(AutoapiDocumenter, autodoc.ClassDocumenter): + return super(AutoapiDecoratorDocumenter, self).format_signature(**kwargs) + + def format_args(self, **kwargs): + to_format = self.object.args + + if re.match("func\W", to_format) or to_format == "func": + if "," not in to_format: + return None + + # We need to do better stripping here. + # An annotation with a comma will mess this up. + to_format = self.object.args.split(",", 1)[1] + + return "(" + to_format + ")" + + +class AutoapiClassDocumenter( + AutoapiDocumenter, autodoc.ClassDocumenter, _AutoapiDocstringSignatureMixin +): objtype = "apiclass" directivetype = "class" doc_as_attr = False @@ -129,7 +179,9 @@ class AutoapiClassDocumenter(AutoapiDocumenter, autodoc.ClassDocumenter): self.add_line(" " + "Bases: {}".format(", ".join(bases)), sourcename) -class AutoapiMethodDocumenter(AutoapiDocumenter, autodoc.MethodDocumenter): +class AutoapiMethodDocumenter( + AutoapiDocumenter, autodoc.MethodDocumenter, _AutoapiDocstringSignatureMixin +): objtype = "apimethod" directivetype = "method" priority = autodoc.MethodDocumenter.priority * 100 + 100 @@ -141,29 +193,33 @@ class AutoapiMethodDocumenter(AutoapiDocumenter, autodoc.MethodDocumenter): def format_args(self, **kwargs): return "(" + self.object.args + ")" - def format_signature(self, **kwargs): - # Set "introspected" attributes at the last possible minute - if self.args is None: - self.args = self.object.args - - if self.retann is None: - self.retann = self.object.return_annotation - - return super(AutoapiMethodDocumenter, self).format_signature(**kwargs) - def import_object(self): result = super(AutoapiMethodDocumenter, self).import_object() if result: if self.object.method_type != "method": - self.directivetype = self.object.method_type + if sphinx.version_info < (2, 1): + self.directivetype = self.object.method_type # document class and static members before ordinary ones self.member_order = self.member_order - 1 return result def add_directive_header(self, sig): - autodoc.Documenter.add_directive_header(self, sig) + if sphinx.version_info >= (2, 1): + autodoc.Documenter.add_directive_header(self, sig) + + sourcename = self.get_sourcename() + for property_type in ( + "abstractmethod", + "async", + "classmethod", + "staticmethod", + ): + if property_type in self.object.properties: + self.add_line(" :{}:".format(property_type), sourcename) + else: + autodoc.Documenter.add_directive_header(self, sig) class AutoapiDataDocumenter(AutoapiDocumenter, autodoc.DataDocumenter): diff --git a/autoapi/extension.py b/autoapi/extension.py index e638aa1..b1b1ae3 100644 --- a/autoapi/extension.py +++ b/autoapi/extension.py @@ -237,6 +237,8 @@ def setup(app): app.add_config_value("autoapi_python_class_content", "class", "html") app.add_config_value("autoapi_generate_api_docs", True, "html") app.add_autodocumenter(documenters.AutoapiFunctionDocumenter) + if sphinx.version_info >= (2,): + app.add_autodocumenter(documenters.AutoapiDecoratorDocumenter) app.add_autodocumenter(documenters.AutoapiClassDocumenter) app.add_autodocumenter(documenters.AutoapiMethodDocumenter) app.add_autodocumenter(documenters.AutoapiDataDocumenter) diff --git a/tests/python/pyexample/example/example.py b/tests/python/pyexample/example/example.py index 08462c3..56365b1 100644 --- a/tests/python/pyexample/example/example.py +++ b/tests/python/pyexample/example/example.py @@ -93,3 +93,12 @@ class Foo(object): str: A string. """ return "google" + + +def decorator_okay(func): + """This decorator should parse okay.""" + + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper diff --git a/tests/python/pyexample/manualapi.rst b/tests/python/pyexample/manualapi.rst index 3638ec9..78a5e6f 100644 --- a/tests/python/pyexample/manualapi.rst +++ b/tests/python/pyexample/manualapi.rst @@ -4,3 +4,6 @@ Autodoc Directives .. autoapimodule:: example :members: :noindex: + + +.. autoapidecorator:: example.decorator_okay diff --git a/tests/python/test_pyintegration.py b/tests/python/test_pyintegration.py index 47bc699..fa3c4fb 100644 --- a/tests/python/test_pyintegration.py +++ b/tests/python/test_pyintegration.py @@ -42,8 +42,15 @@ class TestSimpleModule(object): self.check_integration("_build/text/autoapi/example/index.txt") def test_manual_directives(self): + example_path = "_build/text/manualapi.txt" # The manual directives should contain the same information - self.check_integration("_build/text/manualapi.txt") + self.check_integration(example_path) + + with io.open(example_path, encoding="utf8") as example_handle: + example_file = example_handle.read() + + if sphinx.version_info >= (2,): + assert "@example.decorator_okay" in example_file def check_integration(self, example_path): with io.open(example_path, encoding="utf8") as example_handle: