Skip to content

Commit

Permalink
autodoc: fix ordering of class and static methods for groupwise ord…
Browse files Browse the repository at this point in the history
…er (#13201)

Co-authored-by: Adam Turner <[email protected]>
  • Loading branch information
picnixz and AA-Turner authored Jan 20, 2025
1 parent f4a802c commit 5b9fb9e
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ Bugs fixed
* #1810: Always copy static files when building, regardless of whether
any documents have changed since the previous build.
Patch by Adam Turner.
* #13201: autodoc: fix ordering of members when using ``groupwise``
for :confval:`autodoc_member_order`. Class methods are now rendered
before static methods, which themselves are rendered before regular
methods and attributes.
Patch by Bénédikt Tran.

Testing
-------
Expand Down
4 changes: 3 additions & 1 deletion doc/usage/extensions/autodoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -986,10 +986,12 @@ There are also config values that you can set:

* ``'alphabetical'``:
Use alphabetical order.

* ``'groupwise'``: order by member type. The order is:

* for modules, exceptions, classes, functions, data
* for classes: methods, then properties and attributes
* for classes: class methods, static methods, methods,
and properties/attributes

Members are ordered alphabetically within groups.

Expand Down
54 changes: 37 additions & 17 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@ def document_members(self, all_members: bool = False) -> None:
members_check_module, members = self.get_object_members(want_all)

# document non-skipped members
memberdocumenters: list[tuple[Documenter, bool]] = []
member_documenters: list[tuple[Documenter, bool]] = []
for mname, member, isattr in self.filter_members(members, want_all):
classes = [
cls
Expand All @@ -923,13 +923,27 @@ def document_members(self, all_members: bool = False) -> None:
# of inner classes can be documented
full_mname = f'{self.modname}::' + '.'.join((*self.objpath, mname))
documenter = classes[-1](self.directive, full_mname, self.indent)
memberdocumenters.append((documenter, isattr))
member_documenters.append((documenter, isattr))

member_order = self.options.member_order or self.config.autodoc_member_order
memberdocumenters = self.sort_members(memberdocumenters, member_order)

for documenter, isattr in memberdocumenters:
documenter.generate(
# We now try to import all objects before ordering them. This is to
# avoid possible circular imports if we were to import objects after
# their associated documenters have been sorted.
member_documenters = [
(documenter, isattr)
for documenter, isattr in member_documenters
if documenter.parse_name() and documenter.import_object()
]
member_documenters = self.sort_members(member_documenters, member_order)

for documenter, isattr in member_documenters:
assert documenter.modname
# We can directly call ._generate() since the documenters
# already called parse_name() and import_object() before.
#
# Note that those two methods above do not emit events, so
# whatever objects we deduced should not have changed.
documenter._generate(
all_members=True,
real_modname=self.real_modname,
check_module=members_check_module and not isattr,
Expand Down Expand Up @@ -995,6 +1009,15 @@ def generate(
if not self.import_object():
return

self._generate(more_content, real_modname, check_module, all_members)

def _generate(
self,
more_content: StringList | None = None,
real_modname: str | None = None,
check_module: bool = False,
all_members: bool = False,
) -> None:
# If there is no real module defined, figure out which to use.
# The real module is used in the module analyzer to look up the module
# where the attribute documentation would actually be found in.
Expand Down Expand Up @@ -2358,17 +2381,14 @@ def import_object(self, raiseerror: bool = False) -> bool:
return ret

# to distinguish classmethod/staticmethod
obj = self.parent.__dict__.get(self.object_name)
if obj is None:
obj = self.object

obj_is_staticmethod = inspect.isstaticmethod(
obj, cls=self.parent, name=self.object_name
)
if inspect.isclassmethod(obj) or obj_is_staticmethod:
# document class and static members before ordinary ones
self.member_order = self.member_order - 1

obj = self.parent.__dict__.get(self.object_name, self.object)
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
# document static members before regular methods
self.member_order -= 1
elif inspect.isclassmethod(obj):
# document class methods before static methods as
# they usually behave as alternative constructors
self.member_order -= 2
return ret

def format_args(self, **kwargs: Any) -> str:
Expand Down
8 changes: 8 additions & 0 deletions tests/roots/test-ext-autodoc/target/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ def excludemeth(self):
'moore', 9, 8, 7, docstring='moore(a, e, f) -> happiness'
)

@staticmethod
def b_staticmeth():
pass

@staticmethod
def a_staticmeth():
pass

def __init__(self, arg):
self.inst_attr_inline = None #: an inline documented instance attr
#: a documented instance attribute
Expand Down
19 changes: 17 additions & 2 deletions tests/test_extensions/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,9 @@ def test_autodoc_undoc_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
Expand All @@ -750,7 +752,9 @@ def test_autodoc_undoc_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
Expand Down Expand Up @@ -921,7 +925,9 @@ def test_autodoc_special_members(app):
' .. py:method:: Class.__special1__()',
' .. py:method:: Class.__special2__()',
' .. py:attribute:: Class.__weakref__',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
Expand Down Expand Up @@ -1200,6 +1206,8 @@ def test_autodoc_member_order(app):
' .. py:attribute:: Class.mdocattr',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
' .. py:method:: Class.moore(a, e, f) -> happiness',
' .. py:method:: Class.b_staticmeth()',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.inst_attr_inline',
' .. py:attribute:: Class.inst_attr_comment',
' .. py:attribute:: Class.inst_attr_string',
Expand All @@ -1216,10 +1224,15 @@ def test_autodoc_member_order(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
' .. py:method:: Class.excludemeth()',
' .. py:method:: Class.meth()',
# class methods
' .. py:method:: Class.moore(a, e, f) -> happiness',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
# static methods
' .. py:method:: Class.a_staticmeth()',
' .. py:method:: Class.b_staticmeth()',
# regular methods
' .. py:method:: Class.excludemeth()',
' .. py:method:: Class.meth()',
' .. py:method:: Class.skipmeth()',
' .. py:method:: Class.undocmeth()',
' .. py:attribute:: Class._private_inst_attr',
Expand All @@ -1243,7 +1256,9 @@ def test_autodoc_member_order(app):
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
' .. py:attribute:: Class._private_inst_attr',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
Expand Down

0 comments on commit 5b9fb9e

Please sign in to comment.