diff --git a/CHANGES b/CHANGES index d4fc4d2115..8289c5eb9b 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Deprecated Features added -------------- +* #9445: autodoc: Support class properties * #9445: py domain: ``:py:property:`` directive supports ``:classmethod:`` option to describe the class property diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 1cecb1f797..eafea97483 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2661,7 +2661,32 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: - return inspect.isproperty(member) and isinstance(parent, ClassDocumenter) + if isinstance(parent, ClassDocumenter): + if inspect.isproperty(member): + return True + else: + __dict__ = safe_getattr(parent.object, '__dict__', {}) + obj = __dict__.get(membername) + return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__) + else: + return False + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the exisitence of uninitialized instance attribute when failed to import + the attribute.""" + ret = super().import_object(raiseerror) + if ret and not inspect.isproperty(self.object): + __dict__ = safe_getattr(self.parent, '__dict__', {}) + obj = __dict__.get(self.objpath[-1]) + if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__): + self.object = obj.__func__ + self.isclassmethod = True + return True + else: + return False + + self.isclassmethod = False + return ret def document_members(self, all_members: bool = False) -> None: pass @@ -2675,6 +2700,8 @@ def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() if inspect.isabstractmethod(self.object): self.add_line(' :abstractmethod:', sourcename) + if self.isclassmethod: + self.add_line(' :classmethod:', sourcename) if safe_getattr(self.object, 'fget', None) and self.config.autodoc_typehints != 'none': try: diff --git a/tests/roots/test-ext-autodoc/target/properties.py b/tests/roots/test-ext-autodoc/target/properties.py index 409fc2b5d6..561daefb8f 100644 --- a/tests/roots/test-ext-autodoc/target/properties.py +++ b/tests/roots/test-ext-autodoc/target/properties.py @@ -2,5 +2,10 @@ class Foo: """docstring""" @property - def prop(self) -> int: + def prop1(self) -> int: + """docstring""" + + @classmethod + @property + def prop2(self) -> int: """docstring""" diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 59ee1a9488..24617bf0a5 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -212,12 +212,20 @@ def test_properties(app): ' docstring', '', '', - ' .. py:property:: Foo.prop', + ' .. py:property:: Foo.prop1', ' :module: target.properties', ' :type: int', '', ' docstring', '', + '', + ' .. py:property:: Foo.prop2', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', ] diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_ext_autodoc_autoproperty.py index ee25aa8b71..2b4e5c12a9 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_ext_autodoc_autoproperty.py @@ -16,13 +16,28 @@ @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_properties(app): - actual = do_autodoc(app, 'property', 'target.properties.Foo.prop') + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1') assert list(actual) == [ '', - '.. py:property:: Foo.prop', + '.. py:property:: Foo.prop1', ' :module: target.properties', ' :type: int', '', ' docstring', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_properties(app): + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop2') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop2', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', + ]