Merge pull request #7947 from tk0miya/1362_private_class_attributes

Fix #1362: autodoc: Support private class attributes
This commit is contained in:
Takeshi KOMIYA 2020-07-18 12:07:03 +09:00 committed by GitHub
commit 79d50b5a71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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',
'',
]