From fc02b3f6ada152e38c7683662a1361227dbc7b70 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 26 Dec 2025 19:01:03 +0000 Subject: [PATCH 1/2] feat(autodoc): add enum default rendering option --- doc/usage/extensions/autodoc.rst | 16 ++++++ sphinx/ext/autodoc/__init__.py | 17 ++++++ sphinx/util/inspect.py | 29 +++++++++- tests/roots/test-ext-autodoc/target/enums.py | 22 ++++++++ tests/test_ext_autodoc.py | 56 ++++++++++++++++++++ tests/test_util_inspect.py | 51 +++++++++++++++++- 6 files changed, 188 insertions(+), 3 deletions(-) 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..6d63ac497e4 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: @@ -2735,6 +2750,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/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 4c16886b3fc..385526ee296 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, @@ -2543,3 +2547,55 @@ def test_canonical(app): ' docstring', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_enum_default_rendering': 'name'}) +def test_autodoc_enum_default_rendering_name(app): + enum_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.enum_function')) + int_enum_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.int_enum_function')) + mixed_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.mixed_defaults')) + + assert enum_signature.startswith('.. py:function:: enum_function(') + assert '= EnumCls.val1' in enum_signature + assert int_enum_signature.startswith('.. py:function:: int_enum_function(') + assert '= IntEnumCls.level1' in int_enum_signature + assert mixed_signature.startswith('.. py:function:: mixed_defaults(') + assert '= EnumCls.val2' in mixed_signature + assert "label: str = 'enum'" in mixed_signature + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_enum_default_rendering': 'qualified_name'}) +def test_autodoc_enum_default_rendering_qualified_name(app): + enum_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.enum_function')) + int_enum_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.int_enum_function')) + mixed_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.mixed_defaults')) + + assert "= target.enums.EnumCls.val1" in enum_signature + assert "= target.enums.IntEnumCls.level1" in int_enum_signature + assert "= target.enums.EnumCls.val2" in mixed_signature + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_enum_default_rendering': 'value'}) +def test_autodoc_enum_default_rendering_value(app): + enum_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.enum_function')) + int_enum_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.int_enum_function')) + mixed_signature = _signature_line(do_autodoc(app, 'function', 'target.enums.mixed_defaults')) + + assert '= 12' in enum_signature + assert '= 1' in int_enum_signature + assert "label: str = 'enum'" in mixed_signature + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={ + 'autodoc_enum_default_rendering': 'qualified_name', + 'autodoc_preserve_defaults': True, +}) +def test_autodoc_enum_default_rendering_preserve_defaults(app): + signature = _signature_line(do_autodoc(app, 'function', 'target.enums.mixed_defaults')) + + assert '= EnumCls.val2' in signature + assert 'target.enums.EnumCls.val2' not in signature + assert "label: str = 'enum'" in signature diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index de4ad92366b..b0ce659b4a1 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -10,6 +10,7 @@ import ast import datetime +import enum import functools import sys import types @@ -19,7 +20,8 @@ import pytest from sphinx.util import inspect -from sphinx.util.inspect import TypeAliasNamespace, stringify_signature +from sphinx.util.inspect import (TypeAliasNamespace, object_description, + stringify_signature) def test_TypeAliasNamespace(): @@ -75,6 +77,53 @@ def fun(a, b, c=1, d=2): assert stringify_signature(sig) == '(b, *, c=11, d=2)' +def test_signature_enum_defaults(): + class Color(enum.Enum): + RED = 10 + + class Level(enum.IntEnum): + LOW = 1 + + def func(color: Color = Color.RED, level: Level = Level.LOW, text: str = 'x') -> 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): From 4e3e2ea78958e3afa8c147e26abda037eea798cf Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 26 Dec 2025 19:11:34 +0000 Subject: [PATCH 2/2] fix(autodoc): honor enum defaults for overloads [skip ci] --- sphinx/ext/autodoc/__init__.py | 3 +++ tests/roots/test-ext-autodoc/target/overload.py | 15 +++++++++++++++ tests/test_ext_autodoc.py | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 6d63ac497e4..ac1c4ba52ed 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2154,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 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 385526ee296..c78ff63fbb9 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -2287,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 '