Repository: sphinx-doc/sphinx

Issue:
Bad computation of enum superclass in `sphinx.ext.autodoc.importer.get_class_members()`
### Describe the bug

The computation for the super-class in [`sphinx.ext.autodoc.importer.get_class_members()`](https://github.com/sphinx-doc/sphinx/blob/v6.1.3/sphinx/ext/autodoc/importer.py#L227) is:
```python
superclass = subject.__mro__[1]
```

The version above does not check the super-class taken into account is actually the expected base enum type.

As a consequence, when the super-class is wrong:
- the method does not filter out a couple of members it's used to filter-out for enums: `_generate_next_value_`, `__weakref__`, `_member_names_`, `_member_map_`, `_member_type_`, `_value2member_map_`
- the method filters out other members it should not: `__str__()` for instance.

The computation below fixes the thing:
```python
superclass = list(filter(sphinx.util.inspect.isenumclass, subject.__mro__))[1]
```

### How to Reproduce

```bash
git clone https://github.com/alxroyer/scenario/
cd scenario/
python -m sphinx -T -E -b html -D language=en ./tools/conf/sphinx/ ./doc/html/
```

The problem occurs for the [`StrEnum`](https://github.com/alxroyer/scenario/blob/v0.2.2/src/scenario/enumutils.py#L25) class.

### Environment Information

```text
Sphinx version in use: 5.3.0, but the wrong line is still the same with 6.1.3.
```


### Sphinx extensions

```python
Base `sphinx.ext.autodoc` should be enough.
```


### Additional context

_No response_

Discussion:
picnixz: @alxroyer Could you check if this patch would fix your issue?
picnixz: Thanks to the new tests, I observed that there were issues. So it's still WIP
chrisjsewell: > Thanks to the new tests, I observed that there were issues. So it's still WIP

Maybe convert to draft then 😄 ?
picnixz: > Maybe convert to draft then 😄 ?

I fixed it actually, now I'm just cleaning it up.
picnixz: So there is something with docutils but I don't know what.

@jayaddison can you investigate this one please?
picnixz: There's definitely something. Maybe it's my fix that is causing the issue because it's really weird.
chrisjsewell: > There's definitely something wrong with docutils HEAD. Maybe it's my fix that is causing the issue because it's really weird.

Lokks like `tests/test_builders/test_build_linkcheck.py::test_defaults` is flaky, because I had the same failure on my PR (just re-run and it dissapeared)
picnixz: > Lokks like tests/test_builders/test_build_linkcheck.py::test_defaults is flaky, because I had the same failure on my PR (just re-run and it dissapeared)

It's funny because every time I rerun the test, it failed. So I am quite unlucky :(
picnixz: I am really unlucky. I clearly don't know why this one became flaky now...

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 951d228fd..df4caaef8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -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.
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index cdc357a26..60457c530 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -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
@@ -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]:
+    """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:
@@ -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:
@@ -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
@@ -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:
@@ -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:


Test patch:
diff --git a/tests/roots/test-ext-autodoc/target/enums.py b/tests/roots/test-ext-autodoc/target/enums.py
index c69455fb7..6b2731672 100644
--- a/tests/roots/test-ext-autodoc/target/enums.py
+++ b/tests/roots/test-ext-autodoc/target/enums.py
@@ -1,10 +1,46 @@
+# ruff: NoQA: D403, PIE796
 import enum
+from typing import final
+
+
+class MemberType:
+    """Custom data type with a simple API."""
+
+    # this mangled attribute will never be shown on subclasses
+    # even if :inherited-members: and :private-members: are set
+    __slots__ = ('__data',)
+
+    def __new__(cls, value):
+        self = object.__new__(cls)
+        self.__data = value
+        return self
+
+    def __str__(self):
+        """inherited"""
+        return self.__data
+
+    def __repr__(self):
+        return repr(self.__data)
+
+    def __reduce__(self):
+        # data types must be pickable, otherwise enum classes using this data
+        # type will be forced to be non-pickable and have their __module__ set
+        # to '<unknown>' instead of, for instance, '__main__'
+        return self.__class__, (self.__data,)
+
+    @final
+    @property
+    def dtype(self):
+        """docstring"""
+        return 'str'
+
+    def isupper(self):
+        """inherited"""
+        return self.__data.isupper()
 
 
 class EnumCls(enum.Enum):
-    """
-    this is enum class
-    """
+    """this is enum class"""
 
     #: doc for val1
     val1 = 12
@@ -15,9 +51,194 @@ class EnumCls(enum.Enum):
 
     def say_hello(self):
         """a method says hello to you."""
-        pass
 
     @classmethod
     def say_goodbye(cls):
         """a classmethod says good-bye to you."""
-        pass
+
+
+class EnumClassWithDataType(MemberType, enum.Enum):
+    """this is enum class"""
+
+    x = 'x'
+
+    def say_hello(self):
+        """docstring"""
+
+    @classmethod
+    def say_goodbye(cls):
+        """docstring"""
+
+
+class ToUpperCase:  # not inheriting from enum.Enum
+    @property
+    def value(self):  # bypass enum.Enum.value
+        """uppercased"""
+        return str(self._value_).upper()  # type: ignore[attr-defined]
+
+
+class Greeter:
+    def say_hello(self):
+        """inherited"""
+
+    @classmethod
+    def say_goodbye(cls):
+        """inherited"""
+
+
+class EnumClassWithMixinType(ToUpperCase, enum.Enum):
+    """this is enum class"""
+
+    x = 'x'
+
+    def say_hello(self):
+        """docstring"""
+
+    @classmethod
+    def say_goodbye(cls):
+        """docstring"""
+
+
+class EnumClassWithMixinTypeInherit(Greeter, ToUpperCase, enum.Enum):
+    """this is enum class"""
+
+    x = 'x'
+
+
+class Overridden(enum.Enum):
+    def override(self):
+        """inherited"""
+        return 1
+
+
+class EnumClassWithMixinEnumType(Greeter, Overridden, enum.Enum):
+    """this is enum class"""
+
+    x = 'x'
+
+    def override(self):
+        """overridden"""
+        return 2
+
+
+class EnumClassWithMixinAndDataType(Greeter, ToUpperCase, MemberType, enum.Enum):
+    """this is enum class"""
+
+    x = 'x'
+
+    def say_hello(self):
+        """overridden"""
+
+    @classmethod
+    def say_goodbye(cls):
+        """overridden"""
+
+    def isupper(self):
+        """overridden"""
+        return False
+
+    def __str__(self):
+        """overridden"""
+        return super().__str__()
+
+
+class _ParentEnum(Greeter, Overridden, enum.Enum):
+    """docstring"""
+
+
+class EnumClassWithParentEnum(ToUpperCase, MemberType, _ParentEnum, enum.Enum):
+    """this is enum class"""
+
+    x = 'x'
+
+    def isupper(self):
+        """overridden"""
+        return False
+
+    def __str__(self):
+        """overridden"""
+        return super().__str__()
+
+
+class _SunderMissingInNonEnumMixin:
+    @classmethod
+    def _missing_(cls, value):
+        """inherited"""
+        return super()._missing_(value)  # type: ignore[misc]
+
+
+class _SunderMissingInEnumMixin(enum.Enum):
+    @classmethod
+    def _missing_(cls, value):
+        """inherited"""
+        return super()._missing_(value)
+
+
+class _SunderMissingInDataType(MemberType):
+    @classmethod
+    def _missing_(cls, value):
+        """inherited"""
+        return super()._missing_(value)  # type: ignore[misc]
+
+
+class EnumSunderMissingInNonEnumMixin(_SunderMissingInNonEnumMixin, enum.Enum):
+    """this is enum class"""
+
+
+class EnumSunderMissingInEnumMixin(_SunderMissingInEnumMixin, enum.Enum):
+    """this is enum class"""
+
+
+class EnumSunderMissingInDataType(_SunderMissingInDataType, enum.Enum):
+    """this is enum class"""
+
+
+class EnumSunderMissingInClass(enum.Enum):
+    """this is enum class"""
+
+    @classmethod
+    def _missing_(cls, value):
+        """docstring"""
+        return super()._missing_(value)
+
+
+class _NamePropertyInNonEnumMixin:
+    @property
+    def name(self):
+        """inherited"""
+        return super().name  # type: ignore[misc]
+
+
+class _NamePropertyInEnumMixin(enum.Enum):
+    @property
+    def name(self):
+        """inherited"""
+        return super().name
+
+
+class _NamePropertyInDataType(MemberType):
+    @property
+    def name(self):
+        """inherited"""
+        return super().name  # type: ignore[misc]
+
+
+class EnumNamePropertyInNonEnumMixin(_NamePropertyInNonEnumMixin, enum.Enum):
+    """this is enum class"""
+
+
+class EnumNamePropertyInEnumMixin(_NamePropertyInEnumMixin, enum.Enum):
+    """this is enum class"""
+
+
+class EnumNamePropertyInDataType(_NamePropertyInDataType, enum.Enum):
+    """this is enum class"""
+
+
+class EnumNamePropertyInClass(enum.Enum):
+    """this is enum class"""
+
+    @property
+    def name(self):
+        """docstring"""
+        return super().name
diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py
index 203e5b439..723e91dfb 100644
--- a/tests/test_extensions/test_ext_autodoc.py
+++ b/tests/test_extensions/test_ext_autodoc.py
@@ -4,10 +4,14 @@
 source file translated by test_build.
 """
 
+from __future__ import annotations
+
 import functools
+import itertools
 import operator
 import sys
 from types import SimpleNamespace
+from typing import TYPE_CHECKING
 from unittest.mock import Mock
 from warnings import catch_warnings
 
@@ -26,10 +30,12 @@
 except ImportError:
     pyximport = None
 
+if TYPE_CHECKING:
+    from typing import Any
+
 
 def do_autodoc(app, objtype, name, options=None):
-    if options is None:
-        options = {}
+    options = {} if options is None else options.copy()
     app.env.temp_data.setdefault('docname', 'index')  # set dummy docname
     doccls = app.registry.documenters[objtype]
     docoptions = process_documenter_options(doccls, app.config, options)
@@ -105,7 +111,7 @@ def verify(objtype, name, result):
     verify('module', 'test_ext_autodoc', ('test_ext_autodoc', [], None, None))
     verify('module', 'test.test_ext_autodoc', ('test.test_ext_autodoc', [], None, None))
     verify('module', 'test(arg)', ('test', [], 'arg', None))
-    assert 'signature arguments' in app._warning.getvalue()
+    assert 'signature arguments' in app.warning.getvalue()
 
     # for functions/classes
     verify('function', 'test_ext_autodoc.raises',
@@ -1407,76 +1413,411 @@ def test_slots(app):
     ]
 
 
+class _EnumFormatter:
+    def __init__(self, name: str, *, module: str = 'target.enums') -> None:
+        self.name = name
+        self.module = module
+
+    @property
+    def target(self) -> str:
+        """The autodoc target class."""
+        return f'{self.module}.{self.name}'
+
+    def subtarget(self, name: str) -> str:
+        """The autodoc sub-target (an attribute, method, etc)."""
+        return f'{self.target}.{name}'
+
+    def _node(
+        self, role: str, name: str, doc: str, *, args: str, indent: int, **options: Any,
+    ) -> list[str]:
+        prefix = indent * ' '
+        tab = ' ' * 3
+
+        def rst_option(name: str, value: Any) -> str:
+            value = '' if value in {1, True} else value
+            return f'{prefix}{tab}:{name}: {value!s}'.rstrip()
+
+        lines = [
+            '',
+            f'{prefix}.. py:{role}:: {name}{args}',
+            f'{prefix}{tab}:module: {self.module}',
+            *itertools.starmap(rst_option, options.items()),
+        ]
+        if doc:
+            lines.extend(['', f'{prefix}{tab}{doc}'])
+        lines.append('')
+        return lines
+
+    def entry(
+        self,
+        entry_name: str,
+        doc: str = '',
+        *,
+        role: str,
+        args: str = '',
+        indent: int = 3,
+        **rst_options: Any,
+    ) -> list[str]:
+        """Get the RST lines for a named attribute, method, etc."""
+        qualname = f'{self.name}.{entry_name}'
+        return self._node(role, qualname, doc, args=args, indent=indent, **rst_options)
+
+    def brief(self, doc: str, *, indent: int = 0, **options: Any) -> list[str]:
+        """Generate the brief part of the class being documented."""
+        assert doc, f'enumeration class {self.target!r} should have an explicit docstring'
+
+        if sys.version_info[:2] >= (3, 13):
+            args = ('(value, names=<not given>, *values, module=None, '
+                    'qualname=None, type=None, start=1, boundary=None)')
+        elif sys.version_info[:2] >= (3, 12):
+            args = ('(value, names=None, *values, module=None, '
+                    'qualname=None, type=None, start=1, boundary=None)')
+        elif sys.version_info[:2] >= (3, 11):
+            args = ('(value, names=None, *, module=None, qualname=None, '
+                    'type=None, start=1, boundary=None)')
+        else:
+            args = '(value)'
+
+        return self._node('class', self.name, doc, args=args, indent=indent, **options)
+
+    def method(
+        self, name: str, doc: str, *flags: str, args: str = '()', indent: int = 3,
+    ) -> list[str]:
+        rst_options = dict.fromkeys(flags, '')
+        return self.entry(name, doc, role='method', args=args, indent=indent, **rst_options)
+
+    def member(self, name: str, value: Any, doc: str, *, indent: int = 3) -> list[str]:
+        rst_options = {'value': repr(value)}
+        return self.entry(name, doc, role='attribute', indent=indent, **rst_options)
+
+
+@pytest.fixture()
+def autodoc_enum_options() -> dict[str, object]:
+    """Default autodoc options to use when testing enum's documentation."""
+    return {"members": None, "undoc-members": None}
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_class(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumCls')
+    options = autodoc_enum_options | {'private-members': None}
+
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('say_goodbye', 'a classmethod says good-bye to you.', 'classmethod'),
+        *fmt.method('say_hello', 'a method says hello to you.'),
+        *fmt.member('val1', 12, 'doc for val1'),
+        *fmt.member('val2', 23, 'doc for val2'),
+        *fmt.member('val3', 34, 'doc for val3'),
+        *fmt.member('val4', 34, ''),  # val4 is alias of val3
+    ]
+
+    # Inherited members exclude the native Enum API (in particular
+    # the 'name' and 'value' properties), unless they were explicitly
+    # redefined by the user in one of the bases.
+    actual = do_autodoc(app, 'class', fmt.target, options | {'inherited-members': None})
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('say_goodbye', 'a classmethod says good-bye to you.', 'classmethod'),
+        *fmt.method('say_hello', 'a method says hello to you.'),
+        *fmt.member('val1', 12, 'doc for val1'),
+        *fmt.member('val2', 23, 'doc for val2'),
+        *fmt.member('val3', 34, 'doc for val3'),
+        *fmt.member('val4', 34, ''),  # val4 is alias of val3
+    ]
+
+    # checks for an attribute of EnumCls
+    actual = do_autodoc(app, 'attribute', fmt.subtarget('val1'))
+    assert list(actual) == fmt.member('val1', 12, 'doc for val1', indent=0)
+
+
 @pytest.mark.sphinx('html', testroot='ext-autodoc')
-def test_enum_class(app):
-    options = {"members": None}
-    actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options)
-
-    if sys.version_info[:2] >= (3, 13):
-        args = ('(value, names=<not given>, *values, module=None, '
-                'qualname=None, type=None, start=1, boundary=None)')
-    elif sys.version_info[:2] >= (3, 12):
-        args = ('(value, names=None, *values, module=None, '
-                'qualname=None, type=None, start=1, boundary=None)')
-    elif sys.version_info[:2] >= (3, 11):
-        args = ('(value, names=None, *, module=None, qualname=None, '
-                'type=None, start=1, boundary=None)')
-    else:
-        args = '(value)'
+def test_enum_class_with_data_type(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumClassWithDataType')
 
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
     assert list(actual) == [
-        '',
-        '.. py:class:: EnumCls' + args,
-        '   :module: target.enums',
-        '',
-        '   this is enum class',
-        '',
-        '',
-        '   .. py:method:: EnumCls.say_goodbye()',
-        '      :module: target.enums',
-        '      :classmethod:',
-        '',
-        '      a classmethod says good-bye to you.',
-        '',
-        '',
-        '   .. py:method:: EnumCls.say_hello()',
-        '      :module: target.enums',
-        '',
-        '      a method says hello to you.',
-        '',
-        '',
-        '   .. py:attribute:: EnumCls.val1',
-        '      :module: target.enums',
-        '      :value: 12',
-        '',
-        '      doc for val1',
-        '',
-        '',
-        '   .. py:attribute:: EnumCls.val2',
-        '      :module: target.enums',
-        '      :value: 23',
-        '',
-        '      doc for val2',
-        '',
-        '',
-        '   .. py:attribute:: EnumCls.val3',
-        '      :module: target.enums',
-        '      :value: 34',
-        '',
-        '      doc for val3',
-        '',
+        *fmt.brief('this is enum class'),
+        *fmt.method('say_goodbye', 'docstring', 'classmethod'),
+        *fmt.method('say_hello', 'docstring'),
+        *fmt.member('x', 'x', ''),
     ]
 
-    # checks for an attribute of EnumClass
-    actual = do_autodoc(app, 'attribute', 'target.enums.EnumCls.val1')
+    options = autodoc_enum_options | {'inherited-members': None}
+    actual = do_autodoc(app, 'class', fmt.target, options)
     assert list(actual) == [
-        '',
-        '.. py:attribute:: EnumCls.val1',
-        '   :module: target.enums',
-        '   :value: 12',
-        '',
-        '   doc for val1',
-        '',
+        *fmt.brief('this is enum class'),
+        *fmt.entry('dtype', 'docstring', role='property'),
+        *fmt.method('isupper', 'inherited'),
+        *fmt.method('say_goodbye', 'docstring', 'classmethod'),
+        *fmt.method('say_hello', 'docstring'),
+        *fmt.member('x', 'x', ''),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_class_with_mixin_type(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumClassWithMixinType')
+
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('say_goodbye', 'docstring', 'classmethod'),
+        *fmt.method('say_hello', 'docstring'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+    options = autodoc_enum_options | {'inherited-members': None}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('say_goodbye', 'docstring', 'classmethod'),
+        *fmt.method('say_hello', 'docstring'),
+        *fmt.entry('value', 'uppercased', role='property'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_class_with_mixin_type_and_inheritence(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumClassWithMixinTypeInherit')
+
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+    options = autodoc_enum_options | {'inherited-members': None}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('say_goodbye', 'inherited', 'classmethod'),
+        *fmt.method('say_hello', 'inherited'),
+        *fmt.entry('value', 'uppercased', role='property'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_class_with_mixin_enum_type(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumClassWithMixinEnumType')
+
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        # override() is overridden at the class level so it should be rendered
+        *fmt.method('override', 'overridden'),
+        # say_goodbye() and say_hello() are not rendered since they are inherited
+        *fmt.member('x', 'x', ''),
+    ]
+
+    options = autodoc_enum_options | {'inherited-members': None}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('override', 'overridden'),
+        *fmt.method('say_goodbye', 'inherited', 'classmethod'),
+        *fmt.method('say_hello', 'inherited'),
+        *fmt.member('x', 'x', ''),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_class_with_mixin_and_data_type(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumClassWithMixinAndDataType')
+
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('isupper', 'overridden'),
+        *fmt.method('say_goodbye', 'overridden', 'classmethod'),
+        *fmt.method('say_hello', 'overridden'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+    # add the special member __str__ (but not the inherited members)
+    options = autodoc_enum_options | {'special-members': '__str__'}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('__str__', 'overridden'),
+        *fmt.method('isupper', 'overridden'),
+        *fmt.method('say_goodbye', 'overridden', 'classmethod'),
+        *fmt.method('say_hello', 'overridden'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+    options = autodoc_enum_options | {'inherited-members': None}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('dtype', 'docstring', role='property'),
+        *fmt.method('isupper', 'overridden'),
+        *fmt.method('say_goodbye', 'overridden', 'classmethod'),
+        *fmt.method('say_hello', 'overridden'),
+        *fmt.entry('value', 'uppercased', role='property'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_with_parent_enum(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumClassWithParentEnum')
+
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('isupper', 'overridden'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+    # add the special member __str__ (but not the inherited members)
+    options = autodoc_enum_options | {'special-members': '__str__'}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('__str__', 'overridden'),
+        *fmt.method('isupper', 'overridden'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+    options = autodoc_enum_options | {'inherited-members': None}
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('dtype', 'docstring', role='property'),
+        *fmt.method('isupper', 'overridden'),
+        *fmt.method('override', 'inherited'),
+        *fmt.method('say_goodbye', 'inherited', 'classmethod'),
+        *fmt.method('say_hello', 'inherited'),
+        *fmt.entry('value', 'uppercased', role='property'),
+        *fmt.member('x', 'X', ''),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_sunder_method(app, autodoc_enum_options):
+    PRIVATE = {'private-members': None}  # sunder methods are recognized as private
+
+    fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+
+    fmt = _EnumFormatter('EnumSunderMissingInEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+
+    fmt = _EnumFormatter('EnumSunderMissingInDataType')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+
+    fmt = _EnumFormatter('EnumSunderMissingInClass')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_inherited_sunder_method(app, autodoc_enum_options):
+    options = autodoc_enum_options | {'private-members': None, 'inherited-members': None}
+
+    fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'),
+    ]
+
+    fmt = _EnumFormatter('EnumSunderMissingInEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'),
+    ]
+
+    fmt = _EnumFormatter('EnumSunderMissingInDataType')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'),
+        *fmt.entry('dtype', 'docstring', role='property'),
+        *fmt.method('isupper', 'inherited'),
+    ]
+
+    fmt = _EnumFormatter('EnumSunderMissingInClass')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_custom_name_property(app, autodoc_enum_options):
+    fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+
+    fmt = _EnumFormatter('EnumNamePropertyInEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+
+    fmt = _EnumFormatter('EnumNamePropertyInDataType')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [*fmt.brief('this is enum class')]
+
+    fmt = _EnumFormatter('EnumNamePropertyInClass')
+    actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('name', 'docstring', role='property'),
+    ]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_enum_inherited_custom_name_property(app, autodoc_enum_options):
+    options = autodoc_enum_options | {"inherited-members": None}
+
+    fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('name', 'inherited', role='property'),
+    ]
+
+    fmt = _EnumFormatter('EnumNamePropertyInEnumMixin')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('name', 'inherited', role='property'),
+    ]
+
+    fmt = _EnumFormatter('EnumNamePropertyInDataType')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('dtype', 'docstring', role='property'),
+        *fmt.method('isupper', 'inherited'),
+        *fmt.entry('name', 'inherited', role='property'),
+    ]
+
+    fmt = _EnumFormatter('EnumNamePropertyInClass')
+    actual = do_autodoc(app, 'class', fmt.target, options)
+    assert list(actual) == [
+        *fmt.brief('this is enum class'),
+        *fmt.entry('name', 'docstring', role='property'),
     ]