Skip to content

Commit 1156555

Browse files
authored
Introduce _AutodocAttrGetter type (#13971)
1 parent ff3e1d2 commit 1156555

File tree

6 files changed

+75
-41
lines changed

6 files changed

+75
-41
lines changed

sphinx/ext/autodoc/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
ModuleDocumenter,
3636
PropertyDocumenter,
3737
TypeAliasDocumenter,
38-
autodoc_attrgetter,
3938
)
4039
from sphinx.ext.autodoc._event_listeners import between, cut_lines
4140
from sphinx.ext.autodoc._member_finder import ObjectMember, special_member_re
@@ -94,7 +93,6 @@
9493
'ObjectMember',
9594
'py_ext_sig_re',
9695
'special_member_re',
97-
'autodoc_attrgetter',
9896
'Documenter',
9997
)
10098

sphinx/ext/autodoc/_documenters.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
from sphinx.util.typing import restify, stringify_annotation
2727

2828
if TYPE_CHECKING:
29-
from collections.abc import Iterator
29+
from collections.abc import Callable, Iterator, Sequence
3030
from types import ModuleType
31-
from typing import Any, ClassVar, Final, Literal
31+
from typing import Any, ClassVar, Final, Literal, NoReturn
3232

3333
from sphinx.config import Config
3434
from sphinx.environment import BuildEnvironment, _CurrentDocument
@@ -43,7 +43,6 @@
4343
_TypeStatementProperties,
4444
)
4545
from sphinx.ext.autodoc.directive import DocumenterBridge
46-
from sphinx.registry import SphinxComponentRegistry
4746
from sphinx.util.typing import OptionSpec, _RestifyMode
4847

4948
logger = logging.getLogger('sphinx.ext.autodoc')
@@ -86,10 +85,6 @@ class Documenter:
8685
'noindex': bool_option,
8786
}
8887

89-
def get_attr(self, obj: Any, name: str, *defargs: Any) -> Any:
90-
"""getattr() override for types such as Zope interfaces."""
91-
return autodoc_attrgetter(obj, name, *defargs, registry=self.env._registry)
92-
9388
def __init__(
9489
self, directive: DocumenterBridge, name: str, indent: str = ''
9590
) -> None:
@@ -99,6 +94,7 @@ def __init__(
9994
self._current_document: _CurrentDocument = directive.env.current_document
10095
self._events: EventManager = directive.env.events
10196
self.options: _AutoDocumenterOptions = directive.genopt
97+
self.get_attr = directive.get_attr
10298
self.name = name
10399
self.indent: Final = indent
104100
self.module: ModuleType | None = None
@@ -618,12 +614,32 @@ class TypeAliasDocumenter(Documenter):
618614
}
619615

620616

621-
def autodoc_attrgetter(
622-
obj: Any, name: str, *defargs: Any, registry: SphinxComponentRegistry
623-
) -> Any:
624-
"""Alternative getattr() for types"""
625-
for typ, func in registry.autodoc_attrgetters.items():
626-
if isinstance(obj, typ):
627-
return func(obj, name, *defargs)
617+
class _AutodocAttrGetter:
618+
"""getattr() override for types such as Zope interfaces."""
619+
620+
_attr_getters: Sequence[tuple[type, Callable[[Any, str, Any], Any]]]
621+
622+
__slots__ = ('_attr_getters',)
623+
624+
def __init__(
625+
self, attr_getters: dict[type, Callable[[Any, str, Any], Any]], /
626+
) -> None:
627+
super().__setattr__('_attr_getters', tuple(attr_getters.items()))
628+
629+
def __call__(self, obj: Any, name: str, *defargs: Any) -> Any:
630+
for typ, func in self._attr_getters:
631+
if isinstance(obj, typ):
632+
return func(obj, name, *defargs)
633+
634+
return safe_getattr(obj, name, *defargs)
635+
636+
def __repr__(self) -> str:
637+
return f'_AutodocAttrGetter({dict(self._attr_getters)!r})'
638+
639+
def __setattr__(self, key: str, value: Any) -> NoReturn:
640+
msg = f'{self.__class__.__name__} is immutable'
641+
raise AttributeError(msg)
628642

629-
return safe_getattr(obj, name, *defargs)
643+
def __delattr__(self, key: str) -> NoReturn:
644+
msg = f'{self.__class__.__name__} is immutable'
645+
raise AttributeError(msg)

sphinx/ext/autodoc/directive.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_AutoDocumenterOptions,
1111
_process_documenter_options,
1212
)
13+
from sphinx.ext.autodoc._documenters import _AutodocAttrGetter
1314
from sphinx.util import logging
1415
from sphinx.util.docutils import SphinxDirective, switch_source_input
1516
from sphinx.util.parsing import nested_parse_to_nodes
@@ -25,6 +26,7 @@
2526
from sphinx.environment import BuildEnvironment
2627
from sphinx.ext.autodoc import Documenter
2728
from sphinx.ext.autodoc._directive_options import Options
29+
from sphinx.ext.autodoc.importer import _AttrGetter
2830

2931
logger = logging.getLogger(__name__)
3032

@@ -50,6 +52,7 @@ def __init__(
5052
options: _AutoDocumenterOptions,
5153
lineno: int,
5254
state: Any,
55+
get_attr: _AttrGetter,
5356
) -> None:
5457
self.env = env
5558
self._reporter = reporter
@@ -58,6 +61,7 @@ def __init__(
5861
self.record_dependencies: set[str] = set()
5962
self.result = StringList()
6063
self.state = state
64+
self.get_attr = get_attr
6165

6266

6367
def process_documenter_options(
@@ -136,8 +140,9 @@ def run(self) -> list[Node]:
136140
documenter_options._tab_width = self.state.document.settings.tab_width
137141

138142
# generate the output
143+
get_attr = _AutodocAttrGetter(self.env._registry.autodoc_attrgetters)
139144
params = DocumenterBridge(
140-
self.env, reporter, documenter_options, lineno, self.state
145+
self.env, reporter, documenter_options, lineno, self.state, get_attr
141146
)
142147
documenter = doccls(params, self.arguments[0])
143148
documenter.generate(more_content=self.content)

sphinx/ext/autosummary/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
from sphinx import addnodes
6868
from sphinx.errors import PycodeError
6969
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
70+
from sphinx.ext.autodoc._documenters import _AutodocAttrGetter
7071
from sphinx.ext.autodoc._member_finder import _best_object_type_for_member
7172
from sphinx.ext.autodoc._sentinels import INSTANCE_ATTR
7273
from sphinx.ext.autodoc.directive import DocumenterBridge
@@ -220,8 +221,14 @@ class Autosummary(SphinxDirective):
220221

221222
def run(self) -> list[Node]:
222223
opts = _AutoDocumenterOptions()
224+
get_attr = _AutodocAttrGetter(self.env._registry.autodoc_attrgetters)
223225
self.bridge = DocumenterBridge(
224-
self.env, self.state.document.reporter, opts, self.lineno, self.state
226+
self.env,
227+
self.state.document.reporter,
228+
opts,
229+
self.lineno,
230+
self.state,
231+
get_attr,
225232
)
226233

227234
names = [

tests/test_ext_autodoc/autodoc_util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# NEVER import those objects from sphinx.ext.autodoc directly
1212
from sphinx.ext.autodoc.directive import DocumenterBridge
1313
from sphinx.util.docutils import LoggingReporter
14+
from sphinx.util.inspect import safe_getattr
1415

1516
if TYPE_CHECKING:
1617
from typing import Any
@@ -38,7 +39,9 @@ def do_autodoc(
3839
)
3940
docoptions = _AutoDocumenterOptions.from_directive_options(opts)
4041
state = Mock()
41-
bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state)
42+
bridge = DocumenterBridge(
43+
app.env, LoggingReporter(''), docoptions, 1, state, safe_getattr
44+
)
4245
documenter = doccls(bridge, name)
4346
documenter.generate()
4447
return bridge.result

tests/test_ext_autodoc/test_ext_autodoc.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
inherited_members_option,
2121
)
2222
from sphinx.ext.autodoc._docstrings import _get_docstring_lines
23-
from sphinx.ext.autodoc._documenters import Documenter, autodoc_attrgetter
23+
from sphinx.ext.autodoc._documenters import Documenter, _AutodocAttrGetter
2424
from sphinx.ext.autodoc._property_types import (
2525
_ClassDefProperties,
2626
_FunctionDefProperties,
@@ -72,6 +72,7 @@ def make_directive_bridge(env: BuildEnvironment) -> DocumenterBridge:
7272
options=options,
7373
lineno=0,
7474
state=Mock(),
75+
get_attr=safe_getattr,
7576
)
7677

7778
return directive
@@ -133,9 +134,7 @@ def parse(objtype, name):
133134

134135

135136
def format_sig(obj_type, name, obj, *, app, args=None, retann=None):
136-
def get_attr(obj: Any, name: str, *defargs: Any) -> Any:
137-
return autodoc_attrgetter(obj, name, *defargs, registry=app.registry)
138-
137+
get_attr = _AutodocAttrGetter(app.registry.autodoc_attrgetters)
139138
options = _AutoDocumenterOptions(
140139
synopsis='',
141140
platform='',
@@ -424,9 +423,7 @@ def func(x: int, y: int) -> int: # type: ignore[empty-body]
424423
properties=frozenset(),
425424
)
426425

427-
def get_attr(obj: Any, name: str, *defargs: Any) -> Any:
428-
return autodoc_attrgetter(obj, name, *defargs, registry=app.registry)
429-
426+
get_attr = _AutodocAttrGetter(app.registry.autodoc_attrgetters)
430427
options = _AutoDocumenterOptions(
431428
synopsis='',
432429
platform='',
@@ -561,36 +558,44 @@ def test_new_documenter(app):
561558
]
562559

563560

561+
getattr_spy = []
562+
563+
564564
@pytest.mark.sphinx('html', testroot='ext-autodoc')
565565
def test_attrgetter_using(app):
566+
attrs = []
567+
568+
def _special_getattr(obj, attr_name, *defargs):
569+
if attr_name in attrs:
570+
getattr_spy.append((obj, attr_name))
571+
return None
572+
return getattr(obj, attr_name, *defargs)
573+
574+
app.add_autodoc_attrgetter(type, _special_getattr)
575+
566576
directive = make_directive_bridge(app.env)
577+
directive.get_attr = _AutodocAttrGetter(app.registry.autodoc_attrgetters)
567578
options = directive.genopt
568579
options.members = ALL
569580

570581
options.inherited_members = inherited_members_option(False)
582+
attrs[:] = ['meth']
571583
with catch_warnings(record=True):
572-
_assert_getter_works(app, directive, 'class', 'target.Class', {'meth'})
584+
_assert_getter_works(app, directive, 'class', 'target.Class', *attrs)
573585

574586
options.inherited_members = inherited_members_option(True)
587+
attrs[:] = ['inheritedmeth']
575588
with catch_warnings(record=True):
576589
_assert_getter_works(
577-
app, directive, 'class', 'target.inheritance.Derived', {'inheritedmeth'}
590+
app, directive, 'class', 'target.inheritance.Derived', *attrs
578591
)
579592

580593

581-
def _assert_getter_works(app, directive, objtype, name, attrs=(), **kw):
582-
getattr_spy = set()
583-
584-
def _special_getattr(obj, attr_name, *defargs):
585-
if attr_name in attrs:
586-
getattr_spy.add((obj, attr_name))
587-
return None
588-
return getattr(obj, attr_name, *defargs)
589-
590-
app.add_autodoc_attrgetter(type, _special_getattr)
591-
594+
def _assert_getter_works(app, directive, objtype, name, *attrs):
592595
getattr_spy.clear()
593-
app.registry.documenters[objtype](directive, name).generate(**kw)
596+
597+
doccls = app.registry.documenters[objtype](directive, name)
598+
doccls.generate()
594599

595600
hooked_members = {s[1] for s in getattr_spy}
596601
documented_members = {s[1] for s in processed_signatures}

0 commit comments

Comments
 (0)