diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index c5347f36a7b..c5ddbd93f2b 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -659,6 +659,22 @@ There are also config values that you can set: .. __: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases .. versionadded:: 3.3 +.. confval:: autodoc_enum_default_rendering + + Controls how Enum default argument values are rendered in autodoc-generated + signatures. The supported values are: + + * ``'repr'`` (default) -- use :func:`repr` on the Enum member (the existing + behaviour). + * ``'name'`` -- show the member as ``EnumClass.member``. + * ``'qualified_name'`` -- include the full module path and qualified name. + * ``'value'`` -- render the underlying value of the Enum member. + + When :confval:`autodoc_preserve_defaults` is enabled, the original source + text is kept regardless of this setting. + + .. versionadded:: 4.1 + .. confval:: autodoc_preserve_defaults If True, the default argument values of functions will be not evaluated on diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ec1472e202c..ac1c4ba52ed 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -482,6 +482,9 @@ def format_name(self) -> str: return '.'.join(self.objpath) or self.modname def _call_format_args(self, **kwargs: Any) -> str: + if 'enum_default_rendering' not in kwargs: + kwargs['enum_default_rendering'] = self.config.autodoc_enum_default_rendering + if kwargs: try: return self.format_args(**kwargs) @@ -1287,6 +1290,8 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) + kwargs.setdefault('enum_default_rendering', + self.config.autodoc_enum_default_rendering) try: self.env.app.emit('autodoc-before-process-signature', self.object, False) @@ -1315,6 +1320,9 @@ def add_directive_header(self, sig: str) -> None: self.add_line(' :async:', sourcename) def format_signature(self, **kwargs: Any) -> str: + kwargs.setdefault('enum_default_rendering', + self.config.autodoc_enum_default_rendering) + sigs = [] if (self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads and @@ -1551,6 +1559,8 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) + kwargs.setdefault('enum_default_rendering', + self.config.autodoc_enum_default_rendering) try: self._signature_class, self._signature_method_name, sig = self._get_signature() @@ -1572,6 +1582,9 @@ def format_signature(self, **kwargs: Any) -> str: # do not show signatures return '' + kwargs.setdefault('enum_default_rendering', + self.config.autodoc_enum_default_rendering) + sig = super().format_signature() sigs = [] @@ -2089,6 +2102,8 @@ def import_object(self, raiseerror: bool = False) -> bool: def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints in ('none', 'description'): kwargs.setdefault('show_annotation', False) + kwargs.setdefault('enum_default_rendering', + self.config.autodoc_enum_default_rendering) try: if self.object == object.__init__ and self.parent != object: @@ -2139,6 +2154,9 @@ def document_members(self, all_members: bool = False) -> None: pass def format_signature(self, **kwargs: Any) -> str: + kwargs.setdefault('enum_default_rendering', + self.config.autodoc_enum_default_rendering) + sigs = [] if (self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads and @@ -2735,6 +2753,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: ENUM("signature", "description", "none", "both")) app.add_config_value('autodoc_typehints_description_target', 'all', True, ENUM('all', 'documented')) + app.add_config_value('autodoc_enum_default_rendering', 'repr', True, + ENUM('repr', 'name', 'qualified_name', 'value')) app.add_config_value('autodoc_type_aliases', {}, True) app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index a415a7074c8..cc9c1b9608e 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -706,11 +706,36 @@ def evaluate(annotation: Any, globalns: Dict, localns: Dict) -> Any: return sig.replace(parameters=parameters, return_annotation=return_annotation) +def stringify_default_value(value: Any, enum_default_rendering: Optional[str] = None) -> str: + """Stringify a default value for use in signatures.""" + + rendering = enum_default_rendering or 'repr' + + if isinstance(value, enum.Enum): + enum_type = value.__class__ + if rendering == 'name': + return f"{enum_type.__qualname__}.{value.name}" + if rendering == 'qualified_name': + return f"{enum_type.__module__}.{enum_type.__qualname__}.{value.name}" + if rendering == 'value': + try: + return object_description(value.value) + except Exception: + return object_description(value) + + try: + return object_description(value) + except Exception: + return repr(value) + + def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, - show_return_annotation: bool = True) -> str: + show_return_annotation: bool = True, + enum_default_rendering: Optional[str] = None) -> str: """Stringify a Signature object. :param show_annotation: Show annotation in result + :param enum_default_rendering: Rendering style for Enum defaults """ args = [] last_kind = None @@ -740,7 +765,7 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, arg.write(' = ') else: arg.write('=') - arg.write(object_description(param.default)) + arg.write(stringify_default_value(param.default, enum_default_rendering)) args.append(arg.getvalue()) last_kind = param.kind diff --git a/tests/roots/test-ext-autodoc/target/enums.py b/tests/roots/test-ext-autodoc/target/enums.py index c69455fb7fb..2b942a62602 100644 --- a/tests/roots/test-ext-autodoc/target/enums.py +++ b/tests/roots/test-ext-autodoc/target/enums.py @@ -21,3 +21,25 @@ def say_hello(self): def say_goodbye(cls): """a classmethod says good-bye to you.""" pass + + +class IntEnumCls(enum.IntEnum): + """An IntEnum for autodoc enum default tests.""" + + level1 = 1 + level2 = 2 + + +def enum_function(color: EnumCls = EnumCls.val1): + """Function with an Enum default value.""" + return color + + +def int_enum_function(level: IntEnumCls = IntEnumCls.level1): + """Function with an IntEnum default value.""" + return level + + +def mixed_defaults(color: EnumCls = EnumCls.val2, label: str = 'enum'): + """Function combining Enum and non-Enum defaults.""" + return color, label diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py index 1b395ee5b34..9710c1c1211 100644 --- a/tests/roots/test-ext-autodoc/target/overload.py +++ b/tests/roots/test-ext-autodoc/target/overload.py @@ -1,3 +1,4 @@ +import enum from typing import Any, overload @@ -86,3 +87,17 @@ def __call__(cls, x, y): class Baz(metaclass=Meta): """docstring""" + + +class Color(enum.Enum): + RED = 1 + BLUE = 2 + + +class EnumOverload: + @overload + def choose(self, color: Color = Color.RED) -> Color: + ... + + def choose(self, color: Color = Color.RED) -> Color: + return color diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 4c16886b3fc..c78ff63fbb9 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -45,6 +45,10 @@ def do_autodoc(app, objtype, name, options=None): return bridge.result +def _signature_line(result): + return list(result)[1] + + def make_directive_bridge(env): options = Options( inherited_members = False, @@ -2283,6 +2287,17 @@ def test_overload(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_enum_default_rendering': 'name'}) +def test_overload_enum_default_rendering(app): + actual = do_autodoc(app, 'method', 'target.overload.EnumOverload.choose') + signature_line = _signature_line(actual) + + assert signature_line.startswith('.. py:method:: EnumOverload.choose(') + assert '= Color.RED' in signature_line + assert ' None: + return None + + sig = inspect.signature(func) + + enum_module = Color.__module__ + enum_qual = Color.__qualname__ + enum_name = Color.__name__ + int_enum_module = Level.__module__ + int_enum_qual = Level.__qualname__ + int_enum_name = Level.__name__ + + expected_repr = ( + f"(color: {enum_module}.{enum_qual} = <{enum_name}.{Color.RED.name}: {Color.RED.value}>, " + f"level: {int_enum_module}.{int_enum_qual} = <{int_enum_name}.{Level.LOW.name}: {Level.LOW.value}>, " + "text: str = 'x') -> None" + ) + expected_name = ( + f"(color: {enum_module}.{enum_qual} = {enum_qual}.{Color.RED.name}, " + f"level: {int_enum_module}.{int_enum_qual} = {int_enum_qual}.{Level.LOW.name}, " + "text: str = 'x') -> None" + ) + expected_qualified = ( + f"(color: {enum_module}.{enum_qual} = {enum_module}.{enum_qual}.{Color.RED.name}, " + f"level: {int_enum_module}.{int_enum_qual} = {int_enum_module}.{int_enum_qual}.{Level.LOW.name}, " + "text: str = 'x') -> None" + ) + expected_value = ( + f"(color: {enum_module}.{enum_qual} = {object_description(Color.RED.value)}, " + f"level: {int_enum_module}.{int_enum_qual} = {object_description(Level.LOW.value)}, " + "text: str = 'x') -> None" + ) + + assert stringify_signature(sig) == expected_repr + assert stringify_signature(sig, enum_default_rendering='repr') == expected_repr + assert stringify_signature(sig, enum_default_rendering='name') == expected_name + assert stringify_signature(sig, enum_default_rendering='qualified_name') == expected_qualified + assert stringify_signature(sig, enum_default_rendering='value') == expected_value + + def test_signature_methods(): class Foo: def meth1(self, arg1, **kwargs):