Skip to content

Commit 38d80c3

Browse files
committed
Close #9445: autodoc: Support class properties
Since python 3.9, `classmethod` starts to support creating a "class property". This supports to generate document for it.
1 parent 1205255 commit 38d80c3

File tree

6 files changed

+74
-7
lines changed

6 files changed

+74
-7
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Deprecated
1313
Features added
1414
--------------
1515

16+
* #9445: autodoc: Support class properties
1617
* #9445: py domain: ``:py:property:`` directive supports ``:classmethod:``
1718
option to describe the class property
1819

sphinx/ext/autodoc/__init__.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ def is_filtered_inherited_member(name: str, obj: Any) -> bool:
718718
isattr = False
719719

720720
doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings,
721-
self.parent, self.object_name)
721+
self.object, membername)
722722
if not isinstance(doc, str):
723723
# Ignore non-string __doc__
724724
doc = None
@@ -2661,7 +2661,32 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
26612661
@classmethod
26622662
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
26632663
) -> bool:
2664-
return inspect.isproperty(member) and isinstance(parent, ClassDocumenter)
2664+
if isinstance(parent, ClassDocumenter):
2665+
if inspect.isproperty(member):
2666+
return True
2667+
else:
2668+
__dict__ = safe_getattr(parent.object, '__dict__', {})
2669+
obj = __dict__.get(membername)
2670+
return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__)
2671+
else:
2672+
return False
2673+
2674+
def import_object(self, raiseerror: bool = False) -> bool:
2675+
"""Check the exisitence of uninitialized instance attribute when failed to import
2676+
the attribute."""
2677+
ret = super().import_object(raiseerror)
2678+
if ret and not inspect.isproperty(self.object):
2679+
__dict__ = safe_getattr(self.parent, '__dict__', {})
2680+
obj = __dict__.get(self.objpath[-1])
2681+
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
2682+
self.object = obj.__func__
2683+
self.isclassmethod = True
2684+
return True
2685+
else:
2686+
return False
2687+
2688+
self.isclassmethod = False
2689+
return ret
26652690

26662691
def document_members(self, all_members: bool = False) -> None:
26672692
pass
@@ -2675,6 +2700,8 @@ def add_directive_header(self, sig: str) -> None:
26752700
sourcename = self.get_sourcename()
26762701
if inspect.isabstractmethod(self.object):
26772702
self.add_line(' :abstractmethod:', sourcename)
2703+
if self.isclassmethod:
2704+
self.add_line(' :classmethod:', sourcename)
26782705

26792706
if safe_getattr(self.object, 'fget', None) and self.config.autodoc_typehints != 'none':
26802707
try:

sphinx/util/inspect.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,17 @@ def ispartial(obj: Any) -> bool:
245245
return isinstance(obj, (partial, partialmethod))
246246

247247

248-
def isclassmethod(obj: Any) -> bool:
248+
def isclassmethod(obj: Any, cls: Any = None, name: str = None) -> bool:
249249
"""Check if the object is classmethod."""
250250
if isinstance(obj, classmethod):
251251
return True
252252
elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__):
253253
return True
254+
elif cls and name:
255+
for basecls in getmro(cls):
256+
meth = basecls.__dict__.get(name)
257+
if meth:
258+
return isclassmethod(meth)
254259

255260
return False
256261

@@ -837,6 +842,12 @@ def getdoc(obj: Any, attrgetter: Callable = safe_getattr,
837842
* inherited docstring
838843
* inherited decorated methods
839844
"""
845+
if cls and name and isclassmethod(obj, cls, name):
846+
for basecls in getmro(cls):
847+
meth = basecls.__dict__.get(name)
848+
if meth:
849+
return getdoc(meth.__func__)
850+
840851
doc = attrgetter(obj, '__doc__', None)
841852
if ispartial(obj) and doc == obj.__class__.__doc__:
842853
return getdoc(obj.func)

tests/roots/test-ext-autodoc/target/properties.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@ class Foo:
22
"""docstring"""
33

44
@property
5-
def prop(self) -> int:
5+
def prop1(self) -> int:
6+
"""docstring"""
7+
8+
@classmethod
9+
@property
10+
def prop2(self) -> int:
611
"""docstring"""

tests/test_ext_autodoc_autoclass.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,20 @@ def test_properties(app):
212212
' docstring',
213213
'',
214214
'',
215-
' .. py:property:: Foo.prop',
215+
' .. py:property:: Foo.prop1',
216216
' :module: target.properties',
217217
' :type: int',
218218
'',
219219
' docstring',
220220
'',
221+
'',
222+
' .. py:property:: Foo.prop2',
223+
' :module: target.properties',
224+
' :classmethod:',
225+
' :type: int',
226+
'',
227+
' docstring',
228+
'',
221229
]
222230

223231

tests/test_ext_autodoc_autoproperty.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@
1616

1717
@pytest.mark.sphinx('html', testroot='ext-autodoc')
1818
def test_properties(app):
19-
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop')
19+
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1')
2020
assert list(actual) == [
2121
'',
22-
'.. py:property:: Foo.prop',
22+
'.. py:property:: Foo.prop1',
2323
' :module: target.properties',
2424
' :type: int',
2525
'',
2626
' docstring',
2727
'',
2828
]
29+
30+
31+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
32+
def test_class_properties(app):
33+
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop2')
34+
assert list(actual) == [
35+
'',
36+
'.. py:property:: Foo.prop2',
37+
' :module: target.properties',
38+
' :classmethod:',
39+
' :type: int',
40+
'',
41+
' docstring',
42+
'',
43+
]

0 commit comments

Comments
 (0)