From a3e9d7dfbff2464c093db4eee7a159ef7414ceef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:19:12 +0200 Subject: [PATCH] Add type comments support in PropertyDocumenter --- CHANGES | 2 + sphinx/ext/autodoc/__init__.py | 59 +++++++++++-------- .../test-ext-autodoc/target/properties.py | 11 ++++ tests/test_ext_autodoc_autoclass.py | 15 +++++ tests/test_ext_autodoc_autoproperty.py | 29 +++++++++ 5 files changed, 93 insertions(+), 23 deletions(-) diff --git a/CHANGES b/CHANGES index 0097c68e74b..e8f0b22d1dc 100644 --- a/CHANGES +++ b/CHANGES @@ -27,6 +27,8 @@ Deprecated Features added -------------- +* #11277: :rst:dir:`autoproperty` allows the return type to be specified as + a type comment (e.g., ``# type: () -> int``). Patch by Bénédikt Tran * #10811: Autosummary: extend ``__all__`` to imported members for template rendering when option ``autosummary_ignore_module_all`` is set to ``False``. Patch by Clement Pinard diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index f160721960c..0d16293265a 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2706,6 +2706,16 @@ def import_object(self, raiseerror: bool = False) -> bool: self.isclassmethod = False return ret + def format_args(self, **kwargs: Any) -> str | None: + func = self._get_property_getter() + if func is None: + return None + + # update the annotations of the property getter + self.env.app.emit('autodoc-before-process-signature', func, False) + # correctly format the arguments for a property + return super().format_args(**kwargs) + def document_members(self, all_members: bool = False) -> None: pass @@ -2721,30 +2731,33 @@ def add_directive_header(self, sig: str) -> None: if self.isclassmethod: self.add_line(' :classmethod:', sourcename) - if safe_getattr(self.object, 'fget', None): # property - func = self.object.fget - elif safe_getattr(self.object, 'func', None): # cached_property - func = self.object.func - else: - func = None + func = self._get_property_getter() + if func is None or self.config.autodoc_typehints == 'none': + return - if func and self.config.autodoc_typehints != 'none': - try: - signature = inspect.signature(func, - type_aliases=self.config.autodoc_type_aliases) - if signature.return_annotation is not Parameter.empty: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(signature.return_annotation, "smart") - else: - objrepr = stringify_annotation(signature.return_annotation, - "fully-qualified-except-typing") - self.add_line(' :type: ' + objrepr, sourcename) - except TypeError as exc: - logger.warning(__("Failed to get a function signature for %s: %s"), - self.fullname, exc) - pass - except ValueError: - pass + try: + signature = inspect.signature(func, + type_aliases=self.config.autodoc_type_aliases) + if signature.return_annotation is not Parameter.empty: + if self.config.autodoc_typehints_format == "short": + objrepr = stringify_annotation(signature.return_annotation, "smart") + else: + objrepr = stringify_annotation(signature.return_annotation, + "fully-qualified-except-typing") + self.add_line(' :type: ' + objrepr, sourcename) + except TypeError as exc: + logger.warning(__("Failed to get a function signature for %s: %s"), + self.fullname, exc) + pass + except ValueError: + pass + + def _get_property_getter(self): + if safe_getattr(self.object, 'fget', None): # property + return self.object.fget + if safe_getattr(self.object, 'func', None): # cached_property + return self.object.func + return None def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any: diff --git a/tests/roots/test-ext-autodoc/target/properties.py b/tests/roots/test-ext-autodoc/target/properties.py index 561daefb8fb..018f51ee45e 100644 --- a/tests/roots/test-ext-autodoc/target/properties.py +++ b/tests/roots/test-ext-autodoc/target/properties.py @@ -9,3 +9,14 @@ def prop1(self) -> int: @property def prop2(self) -> int: """docstring""" + + @property + def prop1_with_type_comment(self): + # type: () -> int + """docstring""" + + @classmethod + @property + def prop2_with_type_comment(self): + # type: () -> int + """docstring""" diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 2c70104ea88..fd4e4165bcf 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -213,6 +213,13 @@ def test_properties(app): ' docstring', '', '', + ' .. py:property:: Foo.prop1_with_type_comment', + ' :module: target.properties', + ' :type: int', + '', + ' docstring', + '', + '', ' .. py:property:: Foo.prop2', ' :module: target.properties', ' :classmethod:', @@ -220,6 +227,14 @@ def test_properties(app): '', ' docstring', '', + '', + ' .. py:property:: Foo.prop2_with_type_comment', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', ] diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_ext_autodoc_autoproperty.py index f982144a92d..b19a4ddffa2 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_ext_autodoc_autoproperty.py @@ -38,6 +38,35 @@ def test_class_properties(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_properties(app): + actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1_with_type_comment') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop1_with_type_comment', + ' :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_with_type_comment') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop2_with_type_comment', + ' :module: target.properties', + ' :classmethod:', + ' :type: int', + '', + ' docstring', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_cached_properties(app): actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop')