Fix #1362: autodoc: Support private class attributes

So far, autodoc treats a "private" class attribute as a mere attribute.
But its name is mangled by python interpreter. This make it unmangled
name to be documented expectedly.
This commit is contained in:
Takeshi KOMIYA 2020-07-18 03:36:03 +09:00
parent 8c8943f6c0
commit 488a173904
4 changed files with 94 additions and 4 deletions

View File

@ -44,6 +44,7 @@ Bugs fixed
parameter having ``inspect._empty`` as its default value
* #7901: autodoc: type annotations for overloaded functions are not resolved
* #904: autodoc: An instance attribute cause a crash of autofunction directive
* #1362: autodoc: ``private-members`` option does not work for class attributes
* #7839: autosummary: cannot handle umlauts in function names
* #7865: autosummary: Failed to extract summary line when abbreviations found
* #7866: autosummary: Failed to extract correct summary line when docstring

View File

@ -11,7 +11,7 @@
import importlib
import traceback
import warnings
from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Tuple
from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tuple
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.pycode import ModuleAnalyzer
@ -21,6 +21,36 @@ from sphinx.util.inspect import isclass, isenumclass, safe_getattr
logger = logging.getLogger(__name__)
def mangle(subject: Any, name: str) -> str:
"""mangle the given name."""
try:
if isclass(subject) and name.startswith('__') and not name.endswith('__'):
return "_%s%s" % (subject.__name__, name)
except AttributeError:
pass
return name
def unmangle(subject: Any, name: str) -> Optional[str]:
"""unmangle the given name."""
try:
if isclass(subject) and not name.endswith('__'):
prefix = "_%s__" % subject.__name__
if name.startswith(prefix):
return name.replace(prefix, "__", 1)
else:
for cls in subject.__mro__:
prefix = "_%s__" % cls.__name__
if name.startswith(prefix):
# mangled attribute defined in parent class
return None
except AttributeError:
pass
return name
def import_module(modname: str, warningiserror: bool = False) -> Any:
"""
Call importlib.import_module(modname), convert exceptions to ImportError
@ -68,7 +98,8 @@ def import_object(modname: str, objpath: List[str], objtype: str = '',
for attrname in objpath:
parent = obj
logger.debug('[autodoc] getattr(_, %r)', attrname)
obj = attrgetter(obj, attrname)
mangled_name = mangle(obj, attrname)
obj = attrgetter(obj, mangled_name)
logger.debug('[autodoc] => %r', obj)
object_name = attrname
return [module, parent, object_name, obj]
@ -161,7 +192,8 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
try:
value = attrgetter(subject, name)
directly_defined = name in obj_dict
if name not in members:
name = unmangle(subject, name)
if name and name not in members:
members[name] = Attribute(name, directly_defined, value)
except AttributeError:
continue
@ -169,7 +201,8 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
# annotation only member (ex. attr: int)
if hasattr(subject, '__annotations__') and isinstance(subject.__annotations__, Mapping):
for name in subject.__annotations__:
if name not in members:
name = unmangle(subject, name)
if name and name not in members:
members[name] = Attribute(name, True, INSTANCEATTR)
if analyzer:

View File

@ -0,0 +1,11 @@
class Foo:
#: name of Foo
__name = None
__age = None
class Bar(Foo):
__address = None
#: a member having mangled-like name
_Baz__email = None

View File

@ -1973,3 +1973,48 @@ def test_name_conflict(app):
' docstring of target.name_conflict.foo::bar.',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_name_mangling(app):
options = {"members": None,
"undoc-members": None,
"private-members": None}
actual = do_autodoc(app, 'module', 'target.name_mangling', options)
assert list(actual) == [
'',
'.. py:module:: target.name_mangling',
'',
'',
'.. py:class:: Bar()',
' :module: target.name_mangling',
'',
'',
' .. py:attribute:: Bar._Baz__email',
' :module: target.name_mangling',
' :value: None',
'',
' a member having mangled-like name',
'',
'',
' .. py:attribute:: Bar.__address',
' :module: target.name_mangling',
' :value: None',
'',
'',
'.. py:class:: Foo()',
' :module: target.name_mangling',
'',
'',
' .. py:attribute:: Foo.__age',
' :module: target.name_mangling',
' :value: None',
'',
'',
' .. py:attribute:: Foo.__name',
' :module: target.name_mangling',
' :value: None',
'',
' name of Foo',
'',
]