Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] PropertyDocumenter does not add annotation from type comment #11277

Closed
picnixz opened this issue Apr 1, 2023 · 0 comments · Fixed by #11298
Closed

[BUG] PropertyDocumenter does not add annotation from type comment #11277

picnixz opened this issue Apr 1, 2023 · 0 comments · Fixed by #11298

Comments

@picnixz
Copy link
Member

picnixz commented Apr 1, 2023

Describe the bug

Consider the following code:

class MyClass:
    """My class."""

    @property
    def foo(self) -> int:
      """The foo property."""
      return 0

    @property
    def bar(self):
      # type: () -> int
      """The bar property."""
      return 0

Currently, the sphinx.ext.autodoc.PropertyDocumenter does not fire the autodoc-before-process-signature event, and thus the sphinx.ext.autodoc.type_comments.update_annotations_using_type_comments event handler is never called for properties. Instead, this documenter solely relies on an existing return annotation, which may or may not exist

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

In particular, the bar property does not have a return annotation int because the type comment is never parsed. Note that PropertyDocumenter.add_directive_header is given as input the stringified signature of the object, but for properties, the signature does not include the return annotation. More precisely, when entering PropertyDocumenter.add_directive_header,

def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
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): # property
func = self.object.fget
elif safe_getattr(self.object, 'func', None): # cached_property
func = self.object.func
else:
func = None

the real function is extracted from the descriptor after the signature string has been computed. In particular, the signature being computed at

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:

is now the correct signature and is used to extract return annotation for the property type. However, this signature is never given the chance to be updated using type comments since the autodoc-before-process-signature event is never fired.

How to Fix

This issue can be easily fixed by one of the following method:

  • Fire the autodoc-before-process-signature event directly in PropertyDocumenter.import_object method in order to update the signature of the real object. I fix this in my project as follows:
class CustomPropertyDocumenter(PropertyDocumenter):
    option_spec: OptionSpec = PropertyDocumenter.option_spec.copy()

    def document_members(self, *args, **kwargs):
        return  # do not support members

    def import_object(self, raiseerror=None):
        rv = super().import_object(raiseerror)
        if rv:
            func = self._get_function()
            # update underlying property's annotations if needed
            self.app.emit('autodoc-before-process-signature', func, False)
        return rv

    def _get_function(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

I think it makes sense to fire this event at the earliest possible stage since the directive options (which are added early during the generation stage) depend on the signature of the member being documented. In particular, autodoc-before-process-signature should be indeed fired before calling Documenter.format_signature, namely inbetween Documenter.import_object1 and Documenter.format_signature2:


How to Reproduce

class.py

class MyClass:
    """My class."""

    @property
    def foo(self) -> int:
      """The foo property."""
      return 0

    @property
    def bar(self):
      # type: () -> int
      """The bar property."""
      return 0

index.rst

Foo
===

.. autoclass:: class.MyClass
   :members:

conf.py

import os
import sys

sys.path.insert(0, os.path.abspath('.'))

project = 'Foo'
copyright = '2023, picnixz'
author = 'picnixz'

extensions = ['sphinx.ext.autodoc']

include_patterns = ['index.rst', 'class.py']
templates_path = ['_templates']

master_doc = 'index'

exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

html_theme = 'alabaster'
html_static_path = ['_static']

Environment Information

Platform:              linux; (Linux-5.3.18-lp152.106-default-x86_64-with-glibc2.26)
Python version:        3.10.3 (main, Jan 31 2023, 10:47:25) [GCC 7.5.0])
Python implementation: CPython
Sphinx version:        6.1.3
Docutils version:      0.18.1
Jinja2 version:        3.1.2
Pygments version:      2.14.0

Sphinx extensions

['sphinx.ext.autodoc']

Additional context

Solving this issue may serve in solving #10396

Footnotes

  1. https://github.com/sphinx-doc/sphinx/blob/669bcc0a190ce9921451bad28431886fbaad170b/sphinx/ext/autodoc/__init__.py#L878

  2. https://github.com/sphinx-doc/sphinx/blob/669bcc0a190ce9921451bad28431886fbaad170b/sphinx/ext/autodoc/__init__.py#L930

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant