diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5f20d235..f9eb23ba 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,9 +3,21 @@ Changelog
Versions follow `Semantic Versioning `_ (``..``).
-v1.9.1 (TBC)
+v2.0.0 (TBC)
------------
+Breaking Changes
+^^^^^^^^^^^^^^^^
+
+* Dropped support for Sphinx <4.
+* `#352 ~https://github.com/readthedocs/sphinx-autoapi/issues/352>`: (Python)
+ Properties are rendered with the ``property`` directive,
+ fixing support for Sphinx 5.2.
+ A new ``PythonPythonMapper`` object (``PythonProperty``) has been created
+ to support this change. This object can be passed to templates, filters,
+ and hooks.
+ A new ``property.rst`` template has also been created to support this change.
+
Trivial/Internal Changes
^^^^^^^^^^^^^^^^^^^^^^^^
* Use https links where possible in documentation.
diff --git a/autoapi/documenters.py b/autoapi/documenters.py
index c92a9fc2..ed37e7a7 100644
--- a/autoapi/documenters.py
+++ b/autoapi/documenters.py
@@ -6,6 +6,7 @@
PythonFunction,
PythonClass,
PythonMethod,
+ PythonProperty,
PythonData,
PythonAttribute,
PythonException,
@@ -192,8 +193,11 @@ def import_object(self):
if result:
self.parent = self._method_parent
- if self.object.method_type != "method":
- # document class and static members before ordinary ones
+ if "staticmethod" in self.object.properties:
+ # document static members before ordinary ones
+ self.member_order = self.member_order - 2
+ elif "classmethod" in self.object.properties:
+ # document class members before ordinary ones but after static ones
self.member_order = self.member_order - 1
return result
@@ -212,22 +216,28 @@ def add_directive_header(self, sig):
self.add_line(" :{}:".format(property_type), sourcename)
-class AutoapiPropertyDocumenter(
- AutoapiMethodDocumenter, AutoapiDocumenter, autodoc.PropertyDocumenter
-):
+class AutoapiPropertyDocumenter(AutoapiDocumenter, autodoc.PropertyDocumenter):
objtype = "apiproperty"
- directivetype = "method"
- # Always prefer AutoapiDocumenters
- priority = autodoc.MethodDocumenter.priority * 100 + 100 + 1
+ directivetype = "property"
+ priority = autodoc.PropertyDocumenter.priority * 100 + 100
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
- return isinstance(member, PythonMethod) and "property" in member.properties
+ return isinstance(member, PythonProperty)
def add_directive_header(self, sig):
- super(AutoapiPropertyDocumenter, self).add_directive_header(sig)
+ autodoc.ClassLevelDocumenter.add_directive_header(self, sig)
+
sourcename = self.get_sourcename()
- self.add_line(" :property:", sourcename)
+ if self.options.annotation and self.options.annotation is not autodoc.SUPPRESS:
+ self.add_line(" :type: %s" % self.options.annotation, sourcename)
+
+ for property_type in (
+ "abstractmethod",
+ "classmethod",
+ ):
+ if property_type in self.object.properties:
+ self.add_line(" :{}:".format(property_type), sourcename)
class AutoapiDataDocumenter(AutoapiDocumenter, autodoc.DataDocumenter):
diff --git a/autoapi/mappers/python/__init__.py b/autoapi/mappers/python/__init__.py
index 2be0cae6..cef10217 100644
--- a/autoapi/mappers/python/__init__.py
+++ b/autoapi/mappers/python/__init__.py
@@ -5,6 +5,7 @@
PythonModule,
PythonMethod,
PythonPackage,
+ PythonProperty,
PythonAttribute,
PythonData,
PythonException,
diff --git a/autoapi/mappers/python/mapper.py b/autoapi/mappers/python/mapper.py
index 64a15b39..3b87e4ed 100644
--- a/autoapi/mappers/python/mapper.py
+++ b/autoapi/mappers/python/mapper.py
@@ -19,6 +19,7 @@
PythonModule,
PythonMethod,
PythonPackage,
+ PythonProperty,
PythonAttribute,
PythonData,
PythonException,
@@ -237,12 +238,12 @@ class PythonSphinxMapper(SphinxMapperBase):
PythonModule,
PythonMethod,
PythonPackage,
+ PythonProperty,
PythonAttribute,
PythonData,
PythonException,
)
}
- _OBJ_MAP["property"] = PythonMethod
def __init__(self, app, template_dir=None, url_root=None):
super(PythonSphinxMapper, self).__init__(app, template_dir, url_root)
@@ -421,7 +422,7 @@ def _record_typehints(self, obj):
if (
isinstance(obj, (PythonClass, PythonFunction, PythonMethod))
and not obj.overloads
- ):
+ ) or isinstance(obj, PythonProperty):
obj_annotations = {}
include_return_annotation = True
diff --git a/autoapi/mappers/python/objects.py b/autoapi/mappers/python/objects.py
index d3324be0..999d0ee4 100644
--- a/autoapi/mappers/python/objects.py
+++ b/autoapi/mappers/python/objects.py
@@ -169,7 +169,7 @@ class PythonFunction(PythonPythonMapper):
type = "function"
is_callable = True
- member_order = 40
+ member_order = 30
def __init__(self, obj, **kwargs):
super(PythonFunction, self).__init__(obj, **kwargs)
@@ -219,13 +219,6 @@ class PythonMethod(PythonFunction):
def __init__(self, obj, **kwargs):
super(PythonMethod, self).__init__(obj, **kwargs)
- self.method_type = obj.get("method_type")
- """The type of method that this object represents.
-
- This can be one of: method, staticmethod, or classmethod.
-
- :type: str
- """
self.properties = obj["properties"]
"""The properties that describe what type of method this is.
@@ -242,11 +235,34 @@ def _should_skip(self): # type: () -> bool
return self._ask_ignore(skip)
+class PythonProperty(PythonPythonMapper):
+ """The representation of a property on a class."""
+
+ type = "property"
+ member_order = 60
+
+ def __init__(self, obj, **kwargs):
+ super(PythonProperty, self).__init__(obj, **kwargs)
+
+ self.annotation = obj["return_annotation"]
+ """The type annotation of this property.
+
+ :type: str or None
+ """
+ self.properties = obj["properties"]
+ """The properties that describe what type of property this is.
+
+ Can be any of: abstractmethod, classmethod
+
+ :type: list(str)
+ """
+
+
class PythonData(PythonPythonMapper):
"""Global, module level data."""
type = "data"
- member_order = 10
+ member_order = 40
def __init__(self, obj, **kwargs):
super(PythonData, self).__init__(obj, **kwargs)
@@ -272,7 +288,7 @@ class PythonAttribute(PythonData):
"""An object/class level attribute."""
type = "attribute"
- member_order = 10
+ member_order = 60
class TopLevelPythonPythonMapper(PythonPythonMapper):
@@ -335,7 +351,7 @@ class PythonClass(PythonPythonMapper):
"""The representation of a class."""
type = "class"
- member_order = 30
+ member_order = 20
def __init__(self, obj, **kwargs):
super(PythonClass, self).__init__(obj, **kwargs)
@@ -409,6 +425,10 @@ def docstring(self, value):
def methods(self):
return self._children_of_type("method")
+ @property
+ def properties(self):
+ return self._children_of_type("property")
+
@property
def attributes(self):
return self._children_of_type("attribute")
@@ -446,4 +466,4 @@ class PythonException(PythonClass):
"""The representation of an exception class."""
type = "exception"
- member_order = 20
+ member_order = 10
diff --git a/autoapi/mappers/python/parser.py b/autoapi/mappers/python/parser.py
index e14b81ed..41d1ba59 100644
--- a/autoapi/mappers/python/parser.py
+++ b/autoapi/mappers/python/parser.py
@@ -150,18 +150,23 @@ def parse_functiondef(self, node): # pylint: disable=too-many-branches
if node.type == "function":
type_ = "function"
+
+ if isinstance(node, astroid.AsyncFunctionDef):
+ properties.append("async")
elif astroid_utils.is_decorated_with_property(node):
type_ = "property"
- properties.append("property")
+ if node.type == "classmethod":
+ properties.append(node.type)
+ if node.is_abstract(pass_is_abstract=False):
+ properties.append("abstractmethod")
else:
# "__new__" method is implicit classmethod
if node.type in ("staticmethod", "classmethod") and node.name != "__new__":
properties.append(node.type)
if node.is_abstract(pass_is_abstract=False):
properties.append("abstractmethod")
-
- if isinstance(node, astroid.AsyncFunctionDef):
- properties.append("async")
+ if isinstance(node, astroid.AsyncFunctionDef):
+ properties.append("async")
data = {
"type": type_,
@@ -177,9 +182,6 @@ def parse_functiondef(self, node): # pylint: disable=too-many-branches
"overloads": [],
}
- if type_ in ("method", "property"):
- data["method_type"] = node.type
-
result = [data]
if node.name == "__init__":
diff --git a/autoapi/templates/python/class.rst b/autoapi/templates/python/class.rst
index a791e3d6..df5edffb 100644
--- a/autoapi/templates/python/class.rst
+++ b/autoapi/templates/python/class.rst
@@ -32,6 +32,14 @@
{{ klass.render()|indent(3) }}
{% endfor %}
{% if "inherited-members" in autoapi_options %}
+ {% set visible_properties = obj.properties|selectattr("display")|list %}
+ {% else %}
+ {% set visible_properties = obj.properties|rejectattr("inherited")|selectattr("display")|list %}
+ {% endif %}
+ {% for property in visible_properties %}
+ {{ property.render()|indent(3) }}
+ {% endfor %}
+ {% if "inherited-members" in autoapi_options %}
{% set visible_attributes = obj.attributes|selectattr("display")|list %}
{% else %}
{% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %}
diff --git a/autoapi/templates/python/function.rst b/autoapi/templates/python/function.rst
index 6db8515c..b00d5c24 100644
--- a/autoapi/templates/python/function.rst
+++ b/autoapi/templates/python/function.rst
@@ -5,14 +5,11 @@
{{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}
{% endfor %}
- {% if sphinx_version >= (2, 1) %}
{% for property in obj.properties %}
:{{ property }}:
{% endfor %}
- {% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
- {% else %}
{% endif %}
{% endif %}
diff --git a/autoapi/templates/python/method.rst b/autoapi/templates/python/method.rst
index ff1b7767..723cb7bb 100644
--- a/autoapi/templates/python/method.rst
+++ b/autoapi/templates/python/method.rst
@@ -1,5 +1,4 @@
{%- if obj.display %}
-{% if sphinx_version >= (2, 1) %}
.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
{% for (args, return_annotation) in obj.overloads %}
@@ -14,13 +13,6 @@
{% else %}
{% endif %}
-{% else %}
-.. py:{{ obj.method_type }}:: {{ obj.short_name }}({{ obj.args }})
-{% for (args, return_annotation) in obj.overloads %}
- {{ " " * (obj.method_type | length) }} {{ obj.short_name }}({{ args }})
-{% endfor %}
-
-{% endif %}
{% if obj.docstring %}
{{ obj.docstring|indent(3) }}
{% endif %}
diff --git a/autoapi/templates/python/property.rst b/autoapi/templates/python/property.rst
new file mode 100644
index 00000000..70af2423
--- /dev/null
+++ b/autoapi/templates/python/property.rst
@@ -0,0 +1,15 @@
+{%- if obj.display %}
+.. py:property:: {{ obj.short_name }}
+ {% if obj.annotation %}
+ :type: {{ obj.annotation }}
+ {% endif %}
+ {% if obj.properties %}
+ {% for property in obj.properties %}
+ :{{ property }}:
+ {% endfor %}
+ {% endif %}
+
+ {% if obj.docstring %}
+ {{ obj.docstring|indent(3) }}
+ {% endif %}
+{% endif %}
diff --git a/docs/reference/templates.rst b/docs/reference/templates.rst
index 5439bedc..81884f1b 100644
--- a/docs/reference/templates.rst
+++ b/docs/reference/templates.rst
@@ -73,6 +73,10 @@ Python
:members:
:show-inheritance:
+.. autoapiclass:: autoapi.mappers.python.objects.PythonProperty
+ :members:
+ :show-inheritance:
+
.. autoapiclass:: autoapi.mappers.python.objects.PythonData
:members:
:show-inheritance:
diff --git a/setup.cfg b/setup.cfg
index 291c32c6..db508f73 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -36,7 +36,7 @@ install_requires =
astroid>=2.7
Jinja2
PyYAML
- sphinx>=3.0
+ sphinx>=4.0
unidecode
[options.extras_require]
diff --git a/tests/python/pyexample/autoapi/example/index.rst b/tests/python/pyexample/autoapi/example/index.rst
new file mode 100644
index 00000000..2d6946b9
--- /dev/null
+++ b/tests/python/pyexample/autoapi/example/index.rst
@@ -0,0 +1,185 @@
+:py:mod:`example`
+=================
+
+.. py:module:: example
+
+.. autoapi-nested-parse::
+
+ Example module
+
+ This is a description
+
+
+
+Module Contents
+---------------
+
+Classes
+~~~~~~~
+
+.. autoapisummary::
+
+ example.Foo
+ example.Bar
+ example.ClassWithNoInit
+ example.One
+ example.MultilineOne
+ example.Two
+
+
+
+Functions
+~~~~~~~~~
+
+.. autoapisummary::
+
+ example.decorator_okay
+ example.fn_with_long_sig
+
+
+
+.. py:class:: Foo(attr)
+
+ Bases: :py:obj:`object`
+
+ This is using custom filters.
+ .. py:class:: Meta
+
+ Bases: :py:obj:`object`
+
+ This is using custom filters.
+ .. py:method:: foo()
+ :classmethod:
+
+ The foo class method
+
+
+
+ .. py:property:: property_simple
+ :type: int
+
+ This property should parse okay.
+
+
+ .. py:attribute:: class_var
+ :annotation: = 42
+
+
+
+ .. py:attribute:: another_class_var
+ :annotation: = 42
+
+ Another class var docstring
+
+
+ .. py:attribute:: attr2
+
+
+ This is the docstring of an instance attribute.
+
+ :type: str
+
+
+ .. py:method:: method_okay(foo=None, bar=None)
+
+ This method should parse okay
+
+
+ .. py:method:: method_multiline(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
+
+
+ .. py:method:: method_tricky(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
+
+
+ .. py:method:: method_sphinx_docs(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
+
+
+ .. py:method:: method_google_docs(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.
+
+
+ .. py:method:: method_sphinx_unicode()
+
+ This docstring uses unicodé.
+
+ :returns: A string.
+ :rtype: str
+
+
+ .. py:method:: method_google_unicode()
+
+ This docstring uses unicodé.
+
+ Returns:
+ str: A string.
+
+
+
+.. py:function:: decorator_okay(func)
+
+ This decorator should parse okay.
+
+
+.. py:class:: Bar(attr)
+
+ Bases: :py:obj:`Foo`
+
+ This is using custom filters.
+ .. py:method:: method_okay(foo=None, bar=None)
+
+ This method should parse okay
+
+
+
+.. py:class:: ClassWithNoInit
+
+ This is using custom filters.
+
+.. py:class:: One
+
+ This is using custom filters.
+
+.. py:class:: MultilineOne
+
+ Bases: :py:obj:`One`
+
+ This is using custom filters.
+
+.. py:class:: Two
+
+ Bases: :py:obj:`One`
+
+ This is using custom filters.
+
+.. py:function:: fn_with_long_sig(this, *, function=None, has=True, quite=True, a, long, signature, many, keyword, arguments)
+
+ A function with a long signature.
+
+
diff --git a/tests/python/pyexample/autoapi/index.rst b/tests/python/pyexample/autoapi/index.rst
new file mode 100644
index 00000000..55f9ebe2
--- /dev/null
+++ b/tests/python/pyexample/autoapi/index.rst
@@ -0,0 +1,11 @@
+API Reference
+=============
+
+This page contains auto-generated API reference documentation [#f1]_.
+
+.. toctree::
+ :titlesonly:
+
+ /autoapi/example/index
+
+.. [#f1] Created with `sphinx-autoapi ~https://github.com/readthedocs/sphinx-autoapi>`_
\ No newline at end of file
diff --git a/tests/python/pyexample/conf.py b/tests/python/pyexample/conf.py
index 7a658e10..c85c83d8 100644
--- a/tests/python/pyexample/conf.py
+++ b/tests/python/pyexample/conf.py
@@ -19,3 +19,4 @@
autoapi_dirs = ["example"]
autoapi_file_pattern = "*.py"
autoapi_python_class_content = "both"
+autoapi_keep_files = True
diff --git a/tests/python/pyexample/example/example.py b/tests/python/pyexample/example/example.py
index 4b00265e..74cda18e 100644
--- a/tests/python/pyexample/example/example.py
+++ b/tests/python/pyexample/example/example.py
@@ -34,6 +34,11 @@ def __init__(self, attr):
:type: str
"""
+ @property
+ def property_simple(self) -> int:
+ """This property should parse okay."""
+ return 42
+
def method_okay(self, foo=None, bar=None):
"""This method should parse okay"""
return True
diff --git a/tests/python/pymovedconfpy/autoapi/example/index.rst b/tests/python/pymovedconfpy/autoapi/example/index.rst
new file mode 100644
index 00000000..0896e078
--- /dev/null
+++ b/tests/python/pymovedconfpy/autoapi/example/index.rst
@@ -0,0 +1,207 @@
+:py:mod:`example`
+=================
+
+.. py:module:: example
+
+.. autoapi-nested-parse::
+
+ Example module
+
+ This is a description
+
+
+
+Module Contents
+---------------
+
+Classes
+~~~~~~~
+
+.. autoapisummary::
+
+ example.Foo
+ example.Bar
+ example.ClassWithNoInit
+ example.One
+ example.MultilineOne
+ example.Two
+
+
+
+Functions
+~~~~~~~~~
+
+.. autoapisummary::
+
+ example.decorator_okay
+ example.fn_with_long_sig
+
+
+
+.. py:class:: Foo(attr)
+
+ Bases: :py:obj:`object`
+
+ Can we parse arguments from the class docstring?
+
+ :param attr: Set an attribute.
+ :type attr: str
+
+ Constructor docstring
+
+ .. py:class:: Meta
+
+ Bases: :py:obj:`object`
+
+ A nested class just to test things out
+
+ .. py:method:: foo()
+ :classmethod:
+
+ The foo class method
+
+
+
+ .. py:property:: property_simple
+ :type: int
+
+ This property should parse okay.
+
+
+ .. py:attribute:: class_var
+ :annotation: = 42
+
+
+
+ .. py:attribute:: another_class_var
+ :annotation: = 42
+
+ Another class var docstring
+
+
+ .. py:attribute:: attr2
+
+
+ This is the docstring of an instance attribute.
+
+ :type: str
+
+
+ .. py:method:: method_okay(foo=None, bar=None)
+
+ This method should parse okay
+
+
+ .. py:method:: method_multiline(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
+
+
+ .. py:method:: method_tricky(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
+
+
+ .. py:method:: method_sphinx_docs(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
+
+
+ .. py:method:: method_google_docs(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.
+
+
+ .. py:method:: method_sphinx_unicode()
+
+ This docstring uses unicodé.
+
+ :returns: A string.
+ :rtype: str
+
+
+ .. py:method:: method_google_unicode()
+
+ This docstring uses unicodé.
+
+ Returns:
+ str: A string.
+
+
+
+.. py:function:: decorator_okay(func)
+
+ This decorator should parse okay.
+
+
+.. py:class:: Bar(attr)
+
+ Bases: :py:obj:`Foo`
+
+ Can we parse arguments from the class docstring?
+
+ :param attr: Set an attribute.
+ :type attr: str
+
+ Constructor docstring
+
+ .. py:method:: method_okay(foo=None, bar=None)
+
+ This method should parse okay
+
+
+
+.. py:class:: ClassWithNoInit
+
+
+.. py:class:: One
+
+ One.
+
+ One __init__.
+
+
+.. py:class:: MultilineOne
+
+ Bases: :py:obj:`One`
+
+ This is a naughty summary line
+ that exists on two lines.
+
+ One __init__.
+
+
+.. py:class:: Two
+
+ Bases: :py:obj:`One`
+
+ Two.
+
+ One __init__.
+
+
+.. py:function:: fn_with_long_sig(this, *, function=None, has=True, quite=True, a, long, signature, many, keyword, arguments)
+
+ A function with a long signature.
+
+
diff --git a/tests/python/pymovedconfpy/autoapi/index.rst b/tests/python/pymovedconfpy/autoapi/index.rst
new file mode 100644
index 00000000..55f9ebe2
--- /dev/null
+++ b/tests/python/pymovedconfpy/autoapi/index.rst
@@ -0,0 +1,11 @@
+API Reference
+=============
+
+This page contains auto-generated API reference documentation [#f1]_.
+
+.. toctree::
+ :titlesonly:
+
+ /autoapi/example/index
+
+.. [#f1] Created with `sphinx-autoapi ~https://github.com/readthedocs/sphinx-autoapi>`_
\ No newline at end of file
diff --git a/tests/python/test_pyintegration.py b/tests/python/test_pyintegration.py
index 0fe759e6..0cc8b986 100644
--- a/tests/python/test_pyintegration.py
+++ b/tests/python/test_pyintegration.py
@@ -91,6 +91,9 @@ def check_integration(self, example_path):
# "self" should not be included in constructor arguments
assert "Foo(self" not in example_file
+ assert "property_simple" in example_file
+ assert "This property should parse okay." in example_file
+
# Overridden methods without their own docstring
# should inherit the parent's docstring
assert example_file.count("This method should parse okay") == 2