autodoc: fix ordering of class and static methods for groupwise order (#13201)

Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
This commit is contained in:
Bénédikt Tran 2025-01-20 16:19:05 +01:00 committed by GitHub
parent f4a802cce7
commit 5b9fb9e060
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 69 additions and 19 deletions

View File

@ -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
-------

View File

@ -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.

View File

@ -907,7 +907,7 @@ class Documenter:
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
@ -923,13 +923,27 @@ class Documenter:
# 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)
# 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 memberdocumenters:
documenter.generate(
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,
@ -995,6 +1009,15 @@ class Documenter:
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.
@ -2358,17 +2381,14 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
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:

View File

@ -72,6 +72,14 @@ class Class:
'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

View File

@ -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',
@ -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',
@ -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',
@ -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',
@ -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',
@ -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',