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

Bad computation of enum superclass in sphinx.ext.autodoc.importer.get_class_members() #11353

Closed
alxroyer opened this issue Apr 24, 2023 · 4 comments · Fixed by #11596
Closed

Comments

@alxroyer
Copy link

alxroyer commented Apr 24, 2023

Describe the bug

The computation for the super-class in sphinx.ext.autodoc.importer.get_class_members() is:

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:

superclass = list(filter(sphinx.util.inspect.isenumclass, subject.__mro__))[1]

How to Reproduce

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 class.

Environment Information

Sphinx version in use: 5.3.0, but the wrong line is still the same with 6.1.3.

Sphinx extensions

Base `sphinx.ext.autodoc` should be enough.

Additional context

No response

@AA-Turner
Copy link
Member

Please provide a minimal reproducer, ideally a single module file, a single index.rst file, and a single conf.py file.

A

@alxroyer
Copy link
Author

3 files, in the same directory:

  • 'conf.py':
    extensions = ["sphinx.ext.autodoc"]
  • 'index.rst':
    .. automodule:: strenum
       :members:
       :undoc-members:
       :private-members:
  • strenum.py
    import enum
    
    
    class RegularEnum(enum.Enum):
        """
        Regular enum.
    
        ``enum.Enum`` inherited attributes are filtered-out.
        """
    
    
    class StrEnum(str, enum.Enum):
        """
        Inspired from ``enum.IntEnum``.
    
        ``enum.Enum`` inherited attributes are not filtered-out.
        """
        def __str__(self):
            return self.value

Build command, from the same directory:

python -m sphinx -b html . out/html

Output:
image

Hope that helps. Let me know.

@picnixz
Copy link
Member

picnixz commented Apr 29, 2023

The computation below fixes the thing:
superclass = list(filter(sphinx.util.inspect.isenumclass, subject.__mro__))[1]

An enumeration is instantiated as1:

EnumName([mixin_type, ...] [data_type,] enum_type)

Instead, it is better to use superclass = subject.__mro__[-2] because this would always target the correct enumeration type. Also, enumerations cannot be extended with other enumerations, unless the enumeration super class has no members:2

class EnumMixin(enum.Enum):
    pass

class MyEnum(EnumMixin, Enum):
    a = 1

class Invalid(MyEnum, Enum):  # failure
    b = 1 

The reason why __str__ is not included is because it is first a special member. I agree that there is an issue. This is not due to

for name in obj_dict:
if name not in superclass.__dict__:
value = safe_getattr(subject, name)
members[name] = ObjectMember(name, value, class_=subject)
but due to
# other members
for name in dir(subject):
try:
value = attrgetter(subject, name)
if ismock(value):
value = undecorate(value)
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
if name in obj_dict:
members[unmangled] = ObjectMember(unmangled, value, class_=subject)
else:
members[unmangled] = ObjectMember(unmangled, value)
except AttributeError:
continue

Enumerations override __dir__ and only return public members and some pre-defined special methods. In particular, __str__ is not part of the public API for enumerations and Sphinx will never be able to find it. This is not the case for regular classes since __str__ will be found when using dir(subject).

As such, I suggest one of the following:

  • Either you document the behaviour of what __str__ in the enumeration docstring directly (which would probably be more readable in general).
  • Or we entirely change the logic of finding members for an enumeration (this would also affect
    def get_object_members(
    subject: Any,
    objpath: list[str],
    attrgetter: Callable,
    analyzer: ModuleAnalyzer | None = None
    ) -> dict[str, Attribute]:
    """Get members and attributes of target object."""
    from sphinx.ext.autodoc import INSTANCEATTR
    where enumerations are handled differently as well.

Footnotes

  1. https://github.com/python/cpython/blob/9fbb614c4ed0b9181db1c1b858dfb93587662d6b/Lib/enum.py#L955-L957

  2. https://github.com/python/cpython/blob/9fbb614c4ed0b9181db1c1b858dfb93587662d6b/Lib/enum.py#L929-L937

@AA-Turner AA-Turner added this to the some future version milestone Apr 29, 2023
@alxroyer
Copy link
Author

alxroyer commented May 2, 2023

Thanks a lot @picnixz for the explanation.

@picnixz picnixz self-assigned this Aug 9, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants