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

Fix rendering of enumerations inheriting from mixin and data types. #11596

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
4767ad3
stash
picnixz May 28, 2023
b236d91
Merge remote-tracking branch 'upstream/master' into fix/11353-autodoc…
picnixz Aug 15, 2023
b711e6b
update tests
picnixz Aug 15, 2023
b09c0a3
add CHANGES
picnixz Aug 15, 2023
c87bbac
update tests
picnixz Aug 15, 2023
54a6ea4
Merge remote-tracking branch 'upstream/master' into fix/11353-autodoc…
picnixz Aug 16, 2023
d2fa387
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Aug 17, 2023
14ecd55
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Aug 25, 2023
11b2964
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Sep 19, 2023
d2d6301
Merge branch 'sphinx-doc:master' into fix/11353-autodoc-multiple-inhe…
picnixz Sep 21, 2023
3f2c08c
Merge branch 'sphinx-doc:master' into fix/11353-autodoc-multiple-inhe…
picnixz Sep 21, 2023
3e6c602
update CHANGES
picnixz Sep 21, 2023
be9d9d2
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Sep 21, 2023
9662baa
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Sep 26, 2023
9a359d2
update implementation
picnixz Sep 27, 2023
1a16021
fix lint
picnixz Sep 27, 2023
5485373
fix lint
picnixz Sep 27, 2023
09d8de7
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Oct 4, 2023
528ad8c
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Oct 5, 2023
06836e7
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Oct 9, 2023
1cfd9ee
Merge remote-tracking branch 'upstream/master' into fix/11353-autodoc…
picnixz Feb 3, 2024
ca770df
safe guard `__dict__` access
picnixz Feb 3, 2024
e4a7b7a
fix lint
picnixz Feb 3, 2024
cd41d46
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Feb 3, 2024
a6b29fc
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Feb 25, 2024
8961dd8
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 17, 2024
a03f7f9
Update test_ext_autodoc.py
picnixz Mar 17, 2024
098079a
Update test_ext_autodoc.py
picnixz Mar 17, 2024
d24392d
patch implementation
picnixz Mar 18, 2024
0336810
patch implementation
picnixz Mar 18, 2024
e82f358
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 18, 2024
4f6a020
use explicit default docstring for enum classes
picnixz Mar 18, 2024
0b3ad9e
cleanup
picnixz Mar 18, 2024
8d3a208
Fix class name
picnixz Mar 18, 2024
eda9645
fix for 3.12,3.13?
picnixz Mar 18, 2024
5e1f324
fixup python3.12
picnixz Mar 19, 2024
9d047fa
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 19, 2024
ad97b76
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 19, 2024
0e673dd
simplify logic
picnixz Mar 19, 2024
8e115be
cleanup
picnixz Mar 19, 2024
f30cce6
cleanup
picnixz Mar 19, 2024
89a2ae3
cleanup
picnixz Mar 19, 2024
55b0784
remove unnecessary private function
picnixz Mar 19, 2024
3425c1a
remove unnecessary private function
picnixz Mar 19, 2024
9ce5f4f
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 19, 2024
e1f9639
Handle mangled attributes.
picnixz Mar 20, 2024
d8c1dc8
Improve tests
picnixz Mar 20, 2024
901eaea
flake8
picnixz Mar 20, 2024
f06e5ab
mypy
picnixz Mar 20, 2024
e1d21df
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 20, 2024
9624d47
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 21, 2024
363fe3a
Merge branch 'master' into fix/11353-autodoc-multiple-inheritance-enum
picnixz Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ Bugs fixed
* #11925: Blacklist the ``sphinxprettysearchresults`` extension; the functionality
it provides was merged into Sphinx v2.0.0.
Patch by James Addison.
* #11353: Support enumeration classes inheriting from mixin or data types.
Patch by Bénédikt Tran.
* #11962: Fix target resolution when using ``:paramtype:`` fields.
Patch by Bénédikt Tran.
* #12008: Fix case-sensitive lookup of ``std:label`` names in intersphinx inventory.
Expand Down
132 changes: 100 additions & 32 deletions sphinx/ext/autodoc/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import sys
import traceback
import typing
from typing import TYPE_CHECKING, Any, Callable, NamedTuple
from enum import Enum
from typing import TYPE_CHECKING, NamedTuple

from sphinx.ext.autodoc.mock import ismock, undecorate
from sphinx.pycode import ModuleAnalyzer, PycodeError
Expand All @@ -20,16 +21,91 @@
isclass,
isenumclass,
safe_getattr,
unwrap_all,
)

if TYPE_CHECKING:
from collections.abc import Callable, Generator, Mapping
from types import ModuleType
from typing import Any

from sphinx.ext.autodoc import ObjectMember

logger = logging.getLogger(__name__)


def _filter_enum_dict(
enum_class: type[Enum],
attrgetter: Callable[[Any, str, Any], Any],
enum_class_dict: Mapping[str, object],
) -> Generator[tuple[str, type, Any], None, None]:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
"""Find the attributes to document of an enumeration class.

The output consists of triplets ``(attribute name, defining class, value)``
where the attribute name can appear more than once during the iteration
but with different defining class. The order of occurrence is guided by
the MRO of *enum_class*.
"""
# attributes that were found on a mixin type or the data type
candidate_in_mro: set[str] = set()
# sunder names that were picked up (and thereby allowed to be redefined)
# see: https://docs.python.org/3/howto/enum.html#supported-dunder-names
sunder_names = {'_name_', '_value_', '_missing_', '_order_', '_generate_next_value_'}
# attributes that can be picked up on a mixin type or the enum's data type
public_names = {'name', 'value', *object.__dict__, *sunder_names}
# names that are ignored by default
ignore_names = Enum.__dict__.keys() - public_names

def is_native_api(obj: object, name: str) -> bool:
"""Check whether *obj* is the same as ``Enum.__dict__[name]``."""
return unwrap_all(obj) is unwrap_all(Enum.__dict__[name])

def should_ignore(name: str, value: Any) -> bool:
if name in sunder_names:
return is_native_api(value, name)
return name in ignore_names

sentinel = object()

def query(name: str, defining_class: type) -> tuple[str, type, Any] | None:
value = attrgetter(enum_class, name, sentinel)
if value is not sentinel:
return (name, defining_class, value)
return None

# attributes defined on a parent type, possibly shadowed later by
# the attributes defined directly inside the enumeration class
for parent in enum_class.__mro__:
if parent in {enum_class, Enum, object}:
continue

parent_dict = attrgetter(parent, '__dict__', {})
for name, value in parent_dict.items():
if should_ignore(name, value):
continue

candidate_in_mro.add(name)
if (item := query(name, parent)) is not None:
yield item

# exclude members coming from the native Enum unless
# they were redefined on a mixin type or the data type
excluded_members = Enum.__dict__.keys() - candidate_in_mro
yield from filter(None, (query(name, enum_class) for name in enum_class_dict
if name not in excluded_members))

# check if allowed members from ``Enum`` were redefined at the enum level
special_names = sunder_names | public_names
special_names &= enum_class_dict.keys()
special_names &= Enum.__dict__.keys()
for name in special_names:
if (
not is_native_api(enum_class_dict[name], name)
and (item := query(name, enum_class)) is not None
):
yield item


def mangle(subject: Any, name: str) -> str:
"""Mangle the given name."""
try:
Expand Down Expand Up @@ -192,15 +268,11 @@ def get_object_members(

# enum members
if isenumclass(subject):
for name, value in subject.__members__.items():
if name not in members:
members[name] = Attribute(name, True, value)

superclass = subject.__mro__[1]
for name in obj_dict:
if name not in superclass.__dict__:
value = safe_getattr(subject, name)
members[name] = Attribute(name, True, value)
for name, defining_class, value in _filter_enum_dict(subject, attrgetter, obj_dict):
# the order of occurrence of *name* matches the subject's MRO,
# allowing inherited attributes to be shadowed correctly
if unmangled := unmangle(defining_class, name):
members[unmangled] = Attribute(unmangled, defining_class is subject, value)

# members in __slots__
try:
Expand All @@ -218,18 +290,18 @@ def get_object_members(
try:
value = attrgetter(subject, name)
directly_defined = name in obj_dict
name = unmangle(subject, name)
if name and name not in members:
members[name] = Attribute(name, directly_defined, value)
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
members[unmangled] = Attribute(unmangled, directly_defined, value)
except AttributeError:
continue

# annotation only member (ex. attr: int)
for i, cls in enumerate(getmro(subject)):
for cls in getmro(subject):
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
members[name] = Attribute(name, i == 0, INSTANCEATTR)
unmangled = unmangle(cls, name)
if unmangled and unmangled not in members:
members[unmangled] = Attribute(unmangled, cls is subject, INSTANCEATTR)

if analyzer:
# append instance attributes (cf. self.attr1) if analyzer knows
Expand All @@ -253,15 +325,11 @@ def get_class_members(subject: Any, objpath: Any, attrgetter: Callable,

# enum members
if isenumclass(subject):
for name, value in subject.__members__.items():
if name not in members:
members[name] = ObjectMember(name, value, class_=subject)

superclass = subject.__mro__[1]
for name in obj_dict:
if name not in superclass.__dict__:
value = safe_getattr(subject, name)
members[name] = ObjectMember(name, value, class_=subject)
for name, defining_class, value in _filter_enum_dict(subject, attrgetter, obj_dict):
# the order of occurrence of *name* matches the subject's MRO,
# allowing inherited attributes to be shadowed correctly
if unmangled := unmangle(defining_class, name):
members[unmangled] = ObjectMember(unmangled, value, class_=defining_class)

# members in __slots__
try:
Expand Down Expand Up @@ -306,15 +374,15 @@ def get_class_members(subject: Any, objpath: Any, attrgetter: Callable,

# annotation only member (ex. attr: int)
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
if analyzer and (qualname, name) in analyzer.attr_docs:
docstring = '\n'.join(analyzer.attr_docs[qualname, name])
unmangled = unmangle(cls, name)
if unmangled and unmangled not in members:
if analyzer and (qualname, unmangled) in analyzer.attr_docs:
docstring = '\n'.join(analyzer.attr_docs[qualname, unmangled])
else:
docstring = None

members[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
docstring=docstring)
members[unmangled] = ObjectMember(unmangled, INSTANCEATTR, class_=cls,
docstring=docstring)

# append or complete instance attributes (cf. self.attr1) if analyzer knows
if analyzer:
Expand Down
Loading