Skip to content

Commit

Permalink
Render Python properties with the property directive
Browse files Browse the repository at this point in the history
Fixes #352.
  • Loading branch information
AWhetter committed Sep 27, 2022
1 parent d91c7aa commit a23e2ff
Show file tree
Hide file tree
Showing 19 changed files with 530 additions and 45 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,21 @@ Changelog

Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.<patch>``).

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.
Expand Down
32 changes: 21 additions & 11 deletions autoapi/documenters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
PythonFunction,
PythonClass,
PythonMethod,
PythonProperty,
PythonData,
PythonAttribute,
PythonException,
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions autoapi/mappers/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PythonModule,
PythonMethod,
PythonPackage,
PythonProperty,
PythonAttribute,
PythonData,
PythonException,
Expand Down
5 changes: 3 additions & 2 deletions autoapi/mappers/python/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
PythonModule,
PythonMethod,
PythonPackage,
PythonProperty,
PythonAttribute,
PythonData,
PythonException,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
44 changes: 32 additions & 12 deletions autoapi/mappers/python/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -272,7 +288,7 @@ class PythonAttribute(PythonData):
"""An object/class level attribute."""

type = "attribute"
member_order = 10
member_order = 60


class TopLevelPythonPythonMapper(PythonPythonMapper):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -446,4 +466,4 @@ class PythonException(PythonClass):
"""The representation of an exception class."""

type = "exception"
member_order = 20
member_order = 10
16 changes: 9 additions & 7 deletions autoapi/mappers/python/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_,
Expand All @@ -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__":
Expand Down
8 changes: 8 additions & 0 deletions autoapi/templates/python/class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
3 changes: 0 additions & 3 deletions autoapi/templates/python/function.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
8 changes: 0 additions & 8 deletions autoapi/templates/python/method.rst
Original file line number Diff line number Diff line change
@@ -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 %}
Expand All @@ -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 %}
Expand Down
15 changes: 15 additions & 0 deletions autoapi/templates/python/property.rst
Original file line number Diff line number Diff line change
@@ -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 %}
4 changes: 4 additions & 0 deletions docs/reference/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ install_requires =
astroid>=2.7
Jinja2
PyYAML
sphinx>=3.0
sphinx>=4.0
unidecode

[options.extras_require]
Expand Down
Loading

0 comments on commit a23e2ff

Please sign in to comment.