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 * #1810: Always copy static files when building, regardless of whether
any documents have changed since the previous build. any documents have changed since the previous build.
Patch by Adam Turner. 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 Testing
------- -------

View File

@ -986,10 +986,12 @@ There are also config values that you can set:
* ``'alphabetical'``: * ``'alphabetical'``:
Use alphabetical order. Use alphabetical order.
* ``'groupwise'``: order by member type. The order is: * ``'groupwise'``: order by member type. The order is:
* for modules, exceptions, classes, functions, data * 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. Members are ordered alphabetically within groups.

View File

@ -907,7 +907,7 @@ class Documenter:
members_check_module, members = self.get_object_members(want_all) members_check_module, members = self.get_object_members(want_all)
# document non-skipped members # 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): for mname, member, isattr in self.filter_members(members, want_all):
classes = [ classes = [
cls cls
@ -923,13 +923,27 @@ class Documenter:
# of inner classes can be documented # of inner classes can be documented
full_mname = f'{self.modname}::' + '.'.join((*self.objpath, mname)) full_mname = f'{self.modname}::' + '.'.join((*self.objpath, mname))
documenter = classes[-1](self.directive, full_mname, self.indent) 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 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: for documenter, isattr in member_documenters:
documenter.generate( 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, all_members=True,
real_modname=self.real_modname, real_modname=self.real_modname,
check_module=members_check_module and not isattr, check_module=members_check_module and not isattr,
@ -995,6 +1009,15 @@ class Documenter:
if not self.import_object(): if not self.import_object():
return 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. # 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 # The real module is used in the module analyzer to look up the module
# where the attribute documentation would actually be found in. # where the attribute documentation would actually be found in.
@ -2358,17 +2381,14 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return ret return ret
# to distinguish classmethod/staticmethod # to distinguish classmethod/staticmethod
obj = self.parent.__dict__.get(self.object_name) obj = self.parent.__dict__.get(self.object_name, self.object)
if obj is None: if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
obj = self.object # document static members before regular methods
self.member_order -= 1
obj_is_staticmethod = inspect.isstaticmethod( elif inspect.isclassmethod(obj):
obj, cls=self.parent, name=self.object_name # document class methods before static methods as
) # they usually behave as alternative constructors
if inspect.isclassmethod(obj) or obj_is_staticmethod: self.member_order -= 2
# document class and static members before ordinary ones
self.member_order = self.member_order - 1
return ret return ret
def format_args(self, **kwargs: Any) -> str: 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' 'moore', 9, 8, 7, docstring='moore(a, e, f) -> happiness'
) )
@staticmethod
def b_staticmeth():
pass
@staticmethod
def a_staticmeth():
pass
def __init__(self, arg): def __init__(self, arg):
self.inst_attr_inline = None #: an inline documented instance attr self.inst_attr_inline = None #: an inline documented instance attr
#: a documented instance attribute #: a documented instance attribute

View File

@ -728,7 +728,9 @@ def test_autodoc_undoc_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options) actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [ assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)', '.. py:class:: Class(arg)',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()', ' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_comment',
@ -750,7 +752,9 @@ def test_autodoc_undoc_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options) actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [ assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)', '.. py:class:: Class(arg)',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()', ' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_comment',
@ -921,7 +925,9 @@ def test_autodoc_special_members(app):
' .. py:method:: Class.__special1__()', ' .. py:method:: Class.__special1__()',
' .. py:method:: Class.__special2__()', ' .. py:method:: Class.__special2__()',
' .. py:attribute:: Class.__weakref__', ' .. py:attribute:: Class.__weakref__',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()', ' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_comment',
@ -1200,6 +1206,8 @@ def test_autodoc_member_order(app):
' .. py:attribute:: Class.mdocattr', ' .. py:attribute:: Class.mdocattr',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. 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.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_inline',
' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_comment',
' .. py:attribute:: Class.inst_attr_string', ' .. py:attribute:: Class.inst_attr_string',
@ -1216,10 +1224,15 @@ def test_autodoc_member_order(app):
actual = do_autodoc(app, 'class', 'target.Class', options) actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [ assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)', '.. py:class:: Class(arg)',
' .. py:method:: Class.excludemeth()', # class methods
' .. py:method:: Class.meth()',
' .. py:method:: Class.moore(a, e, f) -> happiness', ' .. py:method:: Class.moore(a, e, f) -> happiness',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)', ' .. 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.skipmeth()',
' .. py:method:: Class.undocmeth()', ' .. py:method:: Class.undocmeth()',
' .. py:attribute:: Class._private_inst_attr', ' .. py:attribute:: Class._private_inst_attr',
@ -1243,7 +1256,9 @@ def test_autodoc_member_order(app):
assert list(filter(lambda l: '::' in l, actual)) == [ assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)', '.. py:class:: Class(arg)',
' .. py:attribute:: Class._private_inst_attr', ' .. py:attribute:: Class._private_inst_attr',
' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr', ' .. py:attribute:: Class.attr',
' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr', ' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()', ' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment', ' .. py:attribute:: Class.inst_attr_comment',