Improve various autodoc tests (#13958)

This commit is contained in:
Adam Turner authored and GitHub committed 2025-10-16 21:30:21 +01:00
1 parent e281d26b5e
commit a8f2b9629b
1 file changed
+167 -134
+167 -134
View File
@@ -27,10 +27,9 @@ from sphinx.ext.autodoc._property_types import (
_ItemProperties,
)
from sphinx.ext.autodoc._sentinels import ALL
# NEVER import these objects from sphinx.ext.autodoc directly
from sphinx.ext.autodoc.directive import DocumenterBridge
from sphinx.ext.autodoc.importer import _format_signatures, _parse_name
from sphinx.util.inspect import safe_getattr
from tests.test_ext_autodoc.autodoc_util import do_autodoc
@@ -133,74 +132,79 @@ def test_parse_name(app):
assert parsed == ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)
def format_sig(obj_type, name, obj, *, app, args=None, retann=None):
def get_attr(obj: Any, name: str, *defargs: Any) -> Any:
return autodoc_attrgetter(obj, name, *defargs, registry=app.registry)
options = _AutoDocumenterOptions(
synopsis='',
platform='',
members=[],
member_order='alphabetical',
exclude_members=set(),
)
parent = object # dummy
props = _ClassDefProperties(
obj_type=obj_type,
module_name='',
parts=(name,),
docstring_lines=(),
bases=getattr(obj, '__bases__', None),
_obj=obj,
_obj___module__=getattr(obj, '__module__', None),
_obj___qualname__=getattr(obj, '__qualname__', None),
_obj___name__=name,
_obj_bases=(),
_obj_is_new_type=False,
_obj_is_typevar=False,
)
props._docstrings = _get_docstring_lines(
props,
class_doc_from=app.config.autoclass_content,
get_attr=get_attr,
inherit_docstrings=app.config.autodoc_inherit_docstrings,
parent=parent,
tab_width=8,
)
signatures = _format_signatures(
config=app.config,
events=app.events,
get_attr=get_attr,
options=options,
parent=parent,
props=props,
args=args,
retann=retann,
)
if not signatures:
return ()
assert len(signatures) == 1
return signatures[0]
@pytest.mark.sphinx('html', testroot='root')
def test_format_signature(app):
def process_signature(app, what, name, obj, options, args, retann):
def test_format_signatures(app):
def process_signature(_app, what, name, _obj, _options, _args, _retann):
processed_signatures.append((what, name))
if name == '.bar':
return '42', None
return None
def skip_member(app, what, name, obj, skip, options):
def skip_member(_app, _what, name, _obj, skip, _options):
if name in {'__special1__', '__special2__'}:
return skip
if name.startswith('__'):
return True
if name == 'skipmeth':
if name == ('skipmeth', ''):
return True
return None
app.connect('autodoc-process-signature', process_signature)
app.connect('autodoc-skip-member', skip_member)
def get_attr(obj: Any, name: str, *defargs: Any) -> Any:
return autodoc_attrgetter(obj, name, *defargs, registry=app.registry)
options = make_directive_bridge(app.env).genopt
def formatsig(objtype, name, obj, args, retann):
parent = object # dummy
props = _ClassDefProperties(
obj_type=objtype,
module_name='',
parts=(name,),
docstring_lines=(),
bases=getattr(obj, '__bases__', None),
_obj=obj,
_obj___module__=getattr(obj, '__module__', None),
_obj___qualname__=getattr(obj, '__qualname__', None),
_obj___name__=name,
_obj_bases=(),
_obj_is_new_type=False,
_obj_is_typevar=False,
)
props._docstrings = _get_docstring_lines(
props,
class_doc_from=app.config.autoclass_content,
get_attr=get_attr,
inherit_docstrings=app.config.autodoc_inherit_docstrings,
parent=parent,
tab_width=8,
)
signatures = _format_signatures(
config=app.config,
events=app.events,
get_attr=get_attr,
options=options,
parent=parent,
props=props,
args=args,
retann=retann,
)
res = '\n'.join(
f'{args} -> {retann}' if retann else str(args)
for args, retann in signatures
)
print(res)
return res
# no signatures for modules
assert formatsig('module', 'test', None, None, None) == ''
assert format_sig('module', 'test', None, app=app) == ()
# test for functions
def f(a, b, c=1, **d):
@@ -209,9 +213,12 @@ def test_format_signature(app):
def g(a='\n'):
pass
assert formatsig('function', 'f', f, None, None) == '(a, b, c=1, **d)'
assert formatsig('function', 'f', f, '(a, b, c, d)', None) == '(a, b, c, d)'
assert formatsig('function', 'g', g, None, None) == r"(a='\n')"
assert format_sig('function', 'f', f, app=app) == ('(a, b, c=1, **d)', '')
assert format_sig('function', 'f', f, app=app, args='(a, b, c, d)') == (
'(a, b, c, d)',
'',
)
assert format_sig('function', 'g', g, app=app) == (r"(a='\n')", '')
if sys.version_info >= (3, 12):
for params, expect in [
@@ -222,15 +229,25 @@ def test_format_signature(app):
ns = {}
exec(f'def f[T]{params}: pass', ns) # NoQA: S102
f = ns['f']
assert formatsig('function', 'f', f, None, None) == expect
assert formatsig('function', 'f', f, '(...)', None) == '(...)'
assert formatsig('function', 'f', f, '(...)', '...') == '(...) -> ...'
assert format_sig('function', 'f', f, app=app) == (expect, '')
assert format_sig('function', 'f', f, app=app, args='(...)') == (
'(...)',
'',
)
assert format_sig(
'function', 'f', f, app=app, args='(...)', retann='...'
) == ('(...)', '...')
exec(f'def f[T]{params} -> list[T]: return []', ns) # NoQA: S102
f = ns['f']
assert formatsig('function', 'f', f, None, None) == f'{expect} -> list[T]'
assert formatsig('function', 'f', f, '(...)', None) == '(...)'
assert formatsig('function', 'f', f, '(...)', '...') == '(...) -> ...'
assert format_sig('function', 'f', f, app=app) == (expect, 'list[T]')
assert format_sig('function', 'f', f, app=app, args='(...)') == (
'(...)',
'',
)
assert format_sig(
'function', 'f', f, app=app, args='(...)', retann='...'
) == ('(...)', '...')
# TODO(picnixz): add more test cases for PEP-695 classes as well (though
# complex cases are less likely to appear and are painful to test).
@@ -245,7 +262,7 @@ def test_format_signature(app):
# an empty init and no init are the same
for C in (D, E):
assert formatsig('class', 'D', C, None, None) == '()'
assert format_sig('class', 'D', C, app=app) == ('()', '')
class SomeMeta(type):
def __call__(cls, a, b=None):
@@ -275,24 +292,30 @@ def test_format_signature(app):
# subclasses inherit
for C in (F, FNew, FMeta, G, GNew, GMeta):
assert formatsig('class', 'C', C, None, None) == '(a, b=None)'
assert formatsig('class', 'C', D, '(a, b)', 'X') == '(a, b) -> X'
assert format_sig('class', 'C', C, app=app) == ('(a, b=None)', '')
assert format_sig('class', 'C', D, app=app, args='(a, b)', retann='X') == (
'(a, b)',
'X',
)
class ListSubclass(list): # NoQA: FURB189
pass
# only supported if the python implementation decides to document it
if getattr(list, '__text_signature__', None) is not None:
assert formatsig('class', 'C', ListSubclass, None, None) == '(iterable=(), /)'
assert format_sig('class', 'C', ListSubclass, app=app) == (
'(iterable=(), /)',
'',
)
else:
assert formatsig('class', 'C', ListSubclass, None, None) == ''
assert format_sig('class', 'C', ListSubclass, app=app) == ()
class ExceptionSubclass(Exception):
pass
# Exception has no __text_signature__ at least in Python 3.11
if getattr(Exception, '__text_signature__', None) is None:
assert formatsig('class', 'C', ExceptionSubclass, None, None) == ''
assert format_sig('class', 'C', ExceptionSubclass, app=app) == ()
# __init__ have signature at first line of docstring
app.config.autoclass_content = 'both'
@@ -310,8 +333,14 @@ def test_format_signature(app):
class G2(F2):
pass
assert formatsig('class', 'F2', F2, None, None) == '(a1, a2, kw1=True, kw2=False)'
assert formatsig('class', 'G2', G2, None, None) == '(a1, a2, kw1=True, kw2=False)'
assert format_sig('class', 'F2', F2, app=app) == (
'(a1, a2, kw1=True, kw2=False)',
'',
)
assert format_sig('class', 'G2', G2, app=app) == (
'(a1, a2, kw1=True, kw2=False)',
'',
)
# test for methods
class H:
@@ -324,35 +353,36 @@ def test_format_signature(app):
def foo3(self, d='\n'):
pass
assert formatsig('method', 'H.foo', H.foo1, None, None) == '(b, *c)'
assert formatsig('method', 'H.foo', H.foo1, '(a)', None) == '(a)'
assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)'
assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\n')"
assert format_sig('method', 'H.foo', H.foo1, app=app) == ('(b, *c)', '')
assert format_sig('method', 'H.foo', H.foo1, app=app, args='(a)') == ('(a)', '')
assert format_sig('method', 'H.foo', H.foo2, app=app) == ('(*c)', '')
assert format_sig('method', 'H.foo', H.foo3, app=app) == (r"(d='\n')", '')
# test bound methods interpreted as functions
assert formatsig('function', 'foo', H().foo1, None, None) == '(b, *c)'
assert formatsig('function', 'foo', H().foo2, None, None) == '(*c)'
assert formatsig('function', 'foo', H().foo3, None, None) == r"(d='\n')"
assert format_sig('function', 'foo', H().foo1, app=app) == ('(b, *c)', '')
assert format_sig('function', 'foo', H().foo2, app=app) == ('(*c)', '')
assert format_sig('function', 'foo', H().foo3, app=app) == (r"(d='\n')", '')
# test exception handling (exception is caught and args is '')
app.config.autodoc_docstring_signature = False
assert formatsig('function', 'int', int, None, None) == ''
assert format_sig('function', 'int', int, app=app) == ()
# test processing by event handler
assert formatsig('method', 'bar', H.foo1, None, None) == '42'
assert format_sig('method', 'bar', H.foo1, app=app) == ('42', '')
# test functions created via functools.partial
from functools import partial
curried1 = partial(lambda a, b, c: None, 'A')
assert formatsig('function', 'curried1', curried1, None, None) == '(b, c)'
assert format_sig('function', 'curried1', curried1, app=app) == ('(b, c)', '')
curried2 = partial(lambda a, b, c=42: None, 'A')
assert formatsig('function', 'curried2', curried2, None, None) == '(b, c=42)'
assert format_sig('function', 'curried2', curried2, app=app) == ('(b, c=42)', '')
curried3 = partial(lambda a, b, *c: None, 'A')
assert formatsig('function', 'curried3', curried3, None, None) == '(b, *c)'
assert format_sig('function', 'curried3', curried3, app=app) == ('(b, *c)', '')
curried4 = partial(lambda a, b, c=42, *d, **e: None, 'A')
assert (
formatsig('function', 'curried4', curried4, None, None) == '(b, c=42, *d, **e)'
assert format_sig('function', 'curried4', curried4, app=app) == (
'(b, c=42, *d, **e)',
'',
)
@@ -397,7 +427,13 @@ def test_autodoc_process_signature_typehints(app):
def get_attr(obj: Any, name: str, *defargs: Any) -> Any:
return autodoc_attrgetter(obj, name, *defargs, registry=app.registry)
options = make_directive_bridge(app.env).genopt
options = _AutoDocumenterOptions(
synopsis='',
platform='',
members=[],
member_order='alphabetical',
exclude_members=set(),
)
_format_signatures(
config=app.config,
events=app.events,
@@ -411,62 +447,53 @@ def test_autodoc_process_signature_typehints(app):
]
def get_docstring_lines(obj_type, obj, *, config):
parent = object # dummy
props = _ItemProperties(
obj_type=obj_type,
module_name='',
parts=(obj.__name__,),
docstring_lines=(),
_obj=obj,
_obj___module__=getattr(obj, '__module__', None),
)
ds = _get_docstring_lines(
props,
class_doc_from=config.autoclass_content,
get_attr=safe_getattr,
inherit_docstrings=config.autodoc_inherit_docstrings,
parent=parent,
tab_width=8,
)
# for testing purposes, concat them and strip the empty line at the end
res = list(itertools.chain.from_iterable(ds or ()))
if res:
res.pop()
return tuple(res)
@pytest.mark.sphinx('html', testroot='root')
def test_get_doc(app):
def get_attr(obj: Any, name: str, *defargs: Any) -> Any:
return autodoc_attrgetter(obj, name, *defargs, registry=app.registry)
options = make_directive_bridge(app.env).genopt
def getdocl(objtype, obj):
parent = object # dummy
props = _ItemProperties(
obj_type=objtype,
module_name='',
parts=(obj.__name__,),
docstring_lines=(),
_obj=obj,
_obj___module__=getattr(obj, '__module__', None),
)
props._docstrings = _get_docstring_lines(
props,
class_doc_from=app.config.autoclass_content,
get_attr=get_attr,
inherit_docstrings=app.config.autodoc_inherit_docstrings,
parent=parent,
tab_width=8,
)
_format_signatures( # handle docstring signatures!
config=app.config,
events=app.events,
get_attr=get_attr,
options=options,
parent=parent,
props=props,
)
ds = props._docstrings
# for testing purposes, concat them and strip the empty line at the end
res = list(itertools.chain.from_iterable(ds))[:-1]
print(res)
return res
def test_get_docstring_lines(app):
config = app.config
# objects without docstring
def f():
pass
assert getdocl('function', f) == []
assert get_docstring_lines('function', f, config=config) == ()
# standard function, diverse docstring styles...
def f():
"""Docstring"""
def g():
assert get_docstring_lines('function', f, config=config) == ('Docstring',)
def f():
"""
Docstring
""" # NoQA: D212
for func in (f, g):
assert getdocl('function', func) == ['Docstring']
assert get_docstring_lines('function', f, config=config) == ('Docstring',)
# first line vs. other lines indentation
def f():
@@ -476,13 +503,18 @@ def test_get_doc(app):
lines
"""
assert getdocl('function', f) == ['First line', '', 'Other', ' lines']
assert get_docstring_lines('function', f, config=config) == (
'First line',
'',
'Other',
' lines',
)
# charset guessing (this module is encoded in utf-8)
def f():
"""Döcstring"""
assert getdocl('function', f) == ['Döcstring']
assert get_docstring_lines('function', f, config=config) == ('Döcstring',)
# verify that method docstrings get extracted in both normal case
# and in case of bound method posing as a function
@@ -490,8 +522,9 @@ def test_get_doc(app):
def foo(self):
"""Method docstring"""
assert getdocl('method', J.foo) == ['Method docstring']
assert getdocl('function', J().foo) == ['Method docstring']
expected = ('Method docstring',)
assert get_docstring_lines('method', J.foo, config=config) == expected
assert get_docstring_lines('function', J().foo, config=config) == expected
class _MyDocumenter(ModuleLevelDocumenter):
@@ -539,21 +572,21 @@ def test_attrgetter_using(app):
options.inherited_members = inherited_members_option(False)
with catch_warnings(record=True):
_assert_getter_works(app, directive, 'class', 'target.Class', ['meth'])
_assert_getter_works(app, directive, 'class', 'target.Class', {'meth'})
options.inherited_members = inherited_members_option(True)
with catch_warnings(record=True):
_assert_getter_works(
app, directive, 'class', 'target.inheritance.Derived', ['inheritedmeth']
app, directive, 'class', 'target.inheritance.Derived', {'inheritedmeth'}
)
def _assert_getter_works(app, directive, objtype, name, attrs=(), **kw):
getattr_spy = []
getattr_spy = set()
def _special_getattr(obj, attr_name, *defargs):
if attr_name in attrs:
getattr_spy.append((obj, attr_name))
getattr_spy.add((obj, attr_name))
return None
return getattr(obj, attr_name, *defargs)