mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
[autodoc] fix rendering of enumerations inheriting from mixin and data types (#11596)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -8,7 +8,8 @@ import os
|
||||
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 @@ from sphinx.util.inspect import (
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,10 +4,14 @@ This tests mainly the Documenters; the auto directives are tested in a test
|
||||
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 @@ try:
|
||||
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 test_parse_name(app):
|
||||
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):
|
||||
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(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) == [
|
||||
'',
|
||||
'.. 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', '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 EnumClass
|
||||
actual = do_autodoc(app, 'attribute', 'target.enums.EnumCls.val1')
|
||||
# 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) == [
|
||||
'',
|
||||
'.. py:attribute:: EnumCls.val1',
|
||||
' :module: target.enums',
|
||||
' :value: 12',
|
||||
'',
|
||||
' doc for val1',
|
||||
'',
|
||||
*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_with_data_type(app, autodoc_enum_options):
|
||||
fmt = _EnumFormatter('EnumClassWithDataType')
|
||||
|
||||
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.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'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user