sphinx-doc / sphinx

The Sphinx documentation generator
https://www.sphinx-doc.org/
Other
6.45k stars 2.1k forks source link

disabled 'inherited-members' shows type-annotated parent class' instance attribute when it has no default value #10934

Open martinpakosch opened 1 year ago

martinpakosch commented 1 year ago

Describe the bug

Hey guys,

this is my first issue here. :D I've noticed a strange behavior of inherited-members option of Sphinx 5.3.2 and 5.3.0 running Python 3.7 on Windows 10.

When I have a public type-annotated class instance attribute at class level context: Mapping[str, Any], without default value as it is set in __init__ later on and should not be a class variable, then Sphinx autodoc documents this attribute as inherited member in subclasses although 'inherited-members' is disabled in conf.py. As soon as I set a default value at class level context: Mapping[str, Any] = {}, making it unintentionally a class variable, it is hidden in the subclass as expected. And it is also hidden as expected at the second subclass level in any case, so for the SubSubClass. :D See my stripped code how to reproduce. Maybe related to or a side effect of #741, #8548, #8587 ? Just guessing... ;-)

Oh, and by the way the member-order does not respect the 'bysource' in this case, or is it that __init__ is always above any attributes? Usually I have my attributes/properties above any methods and so does sphinx for other classes. Give me a hint if this is also worth an issue.

The related environment details of Sphinx. As you can see I am using apidoc for pre-processing my API. The generated rst files correctly contain only automodule directives with :members:, no other options.

Is that issue a bug or do I have a mistake somewhere?

# partial conf.py

extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.autosectionlabel',
    'sphinx.ext.duration',
    'sphinx.ext.intersphinx',
    'sphinx.ext.napoleon',
    'sphinx.ext.viewcode',
]
autodoc_default_options = {
    'members': True,
    'member-order': 'bysource',
    'special-members': '__init__',
    'show-inheritance': True,
}
html_theme = 'sphinx_rtd_theme'

import os
from sphinx.ext import apidoc  # noqa: E402
os.environ['SPHINX_APIDOC_OPTIONS'] = 'members'
apidoc.main([
    '--force',
    '--maxdepth', '3',
    '--module-first',
    '--output-dir', 'api',
    '--no-toc',
    '--separate',
    '../../src/mylib',
])

How to Reproduce

from abc import ABCMeta
from typing import Generic, Mapping, TypeVar

DataObject = TypeVar('DataObject')

class BaseClass(Generic[DataObject], metaclass=ABCMeta):
    """class docstring"""

    _data: DataObject
    """attribute docstring"""

    # with additional default value notation " = {}" hidden by sphinx, otherwise exposed to first-level subclasses although 'inherited-members' is disabled.
    context: Mapping[str, Any]  
    """attribute docstring"""

   def __init__(self, data: DataObject, *, context: Optional[Mapping[str, Any]] = None):
        """Initialize the instance."""
        self._data = data
        self.context = context if context is not None else {}

class SomeSubclass(BaseClass[Dict[str, Any]]):
    """class docstring"""

    ...

Environment Information

source URL not working, HTTP 404

Sphinx extensions

No response

Additional context

No response

martinpakosch commented 1 year ago

Hey guys,

while doing some research on best-practices to document class and instance attributes, I came across some details, that let me struggle about the posted "bug" and so my issue needs some more clarification - at least for me. Maybe I should also mention, that I am quite new to type annotations and even more to attribute docstrings.

However, according to the autoattribute example in the Sphinx docs attributes can be fully autodocumented, if it is "done correctly". ;-) And while reading the linked example and the PEP 526 on class and instance attribute annotations it seems, that it is more "pythonic" - or at least can/should be best-practice - to strictly separate class attributes (at class level) and instance attributes (at init method) - what I always did in my pre-annotation past to be honest. But somehow the typing.ClassVar annotation let me somehow to the conclusion, I should define all attributes at class level for the sake of correct type annotation.

So, I retried the described scenario and when I define and annotate context only at __init__(), everything seems to work regarding the inherited-members option. So now the important question is: Does Sphinx indeed have an issue here as described above OR is my example simply a broken convention where I should not define/annotate instance attributes at class level. I think it is still the first as this issue should also NOT happen for real class attributes. But on the other hand: class attributes without default values are pretty useless, aren't they? And with a default value it would be technically a class attribute, where also everything seems to work as intended.

Hope that helps understanding my thoughts on this potential issue while classifying it.

Cheers.