From aba392d87f5b7cc9954aa2666805a80cd593b2eb Mon Sep 17 00:00:00 2001 From: picnixz <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Apr 2023 23:56:17 +0200 Subject: [PATCH] Support type comments in ``PropertyDocumenter`` (#11298) --- CHANGES | 2 + sphinx/ext/autodoc/__init__.py | 59 +++++++++++-------- .../target/cached_property.py | 5 ++ .../test-ext-autodoc/target/properties.py | 11 ++++ tests/test_ext_autodoc.py | 5 ++ tests/test_ext_autodoc_autoclass.py | 15 +++++ tests/test_ext_autodoc_autoproperty.py | 41 +++++++++++++ 7 files changed, 115 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..0bb5cb6a8fe 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/cached_property.py b/tests/roots/test-ext-autodoc/target/cached_property.py index 63ec09f8eee..712d1d917e7 100644 --- a/tests/roots/test-ext-autodoc/target/cached_property.py +++ b/tests/roots/test-ext-autodoc/target/cached_property.py @@ -5,3 +5,8 @@ class Foo: @cached_property def prop(self) -> int: return 1 + + @cached_property + def prop_with_type_comment(self): + # type: () -> int + return 1 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.py b/tests/test_ext_autodoc.py index 64347bbc6f3..1023323aa6a 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1089,6 +1089,11 @@ def test_autodoc_cached_property(app): ' :module: target.cached_property', ' :type: int', '', + '', + ' .. py:property:: Foo.prop_with_type_comment', + ' :module: target.cached_property', + ' :type: int', + '', ] 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..ca8b981d383 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_with_type_comment(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_with_type_comment(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') @@ -48,3 +77,15 @@ def test_cached_properties(app): ' :type: int', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_cached_properties_with_type_comment(app): + actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop_with_type_comment') + assert list(actual) == [ + '', + '.. py:property:: Foo.prop_with_type_comment', + ' :module: target.cached_property', + ' :type: int', + '', + ]