Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/usage/extensions/autodoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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 = []

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 27 additions & 2 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/roots/test-ext-autodoc/target/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions tests/roots/test-ext-autodoc/target/overload.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import enum
from typing import Any, overload


Expand Down Expand Up @@ -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
67 changes: 67 additions & 0 deletions tests/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 '<Color.RED:' not in signature_line


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_overload2(app):
options = {"members": None}
Expand Down Expand Up @@ -2543,3 +2558,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
51 changes: 50 additions & 1 deletion tests/test_util_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import ast
import datetime
import enum
import functools
import sys
import types
Expand All @@ -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():
Expand Down Expand Up @@ -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):
Expand Down