Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage_docbuild/ext/sage_autodoc.py
6378 views
1
"""
2
Sage autodoc extension
3
4
This is :mod:`sphinx.ext.autodoc` extension modified for Sage objects.
5
6
The original header of :mod:`sphinx.ext.autodoc`:
7
8
Extension to create automatic documentation from code docstrings.
9
10
Automatically insert docstrings for functions, classes or whole modules into
11
the doctree, thus avoiding duplication between docstrings and documentation
12
for those who like elaborate docstrings.
13
14
This module is currently based on :mod:`sphinx.ext.autodoc` from Sphinx version
15
8.0.2. Compare (do diff) with the upstream source file
16
`sphinx/ext/autodoc/__init__.py
17
<https://github.com/sphinx-doc/sphinx/blob/v7.2.6/sphinx/ext/autodoc/__init__.py>`_.
18
19
In the source file of this module, major modifications are delimited by a pair
20
of comment dividers. To lessen maintenance burden, we aim at reducing the modifications.
21
22
AUTHORS:
23
24
- ? (before 2011): initial version derived from sphinx.ext.autodoc
25
26
- Johan S. R. Nielsen: support for _sage_argspec_
27
28
- Simon King (2011-04): use sageinspect; include public cython attributes
29
only in the documentation if they have a docstring
30
31
- Kwankyu Lee (2018-12-26, 2022-11-08): rebased on the latest sphinx.ext.autodoc
32
33
- Kwankyu Lee (2024-02-14): rebased on Sphinx 7.2.6
34
35
- François Bissey (2024-08-24): rebased on Sphinx 8.0.2
36
37
- François Bissey (2024-09-10): Tweaks to support python 3.9 (and older sphinx) as well
38
39
- François Bissey (2024-11-12): rebased on Sphinx 8.1.3 (while trying to keep python 3.9 compatibility)
40
41
- François Bissey (2025-02-24): Remove python 3.9 support hacks, making us closer to upstream
42
43
- François Bissey (2025-03-18): rebased on sphinx 8.2.3
44
"""
45
46
from __future__ import annotations
47
48
import functools
49
import operator
50
import re
51
from inspect import Parameter, Signature
52
from typing import TYPE_CHECKING, Any, NewType, TypeVar
53
54
import sphinx
55
from docutils.statemachine import StringList
56
from sphinx.config import ENUM
57
from sphinx.errors import PycodeError
58
from sphinx.ext.autodoc.importer import get_class_members, import_module, import_object
59
from sphinx.ext.autodoc.mock import ismock, mock, undecorate
60
from sphinx.locale import _, __
61
from sphinx.pycode import ModuleAnalyzer
62
from sphinx.util import inspect, logging
63
from sphinx.util.docstrings import prepare_docstring, separate_metadata
64
from sphinx.util.inspect import (
65
evaluate_signature,
66
getdoc,
67
object_description,
68
safe_getattr,
69
stringify_signature,
70
)
71
from sphinx.util.typing import get_type_hints, restify, stringify_annotation
72
73
# ------------------------------------------------------------------
74
from sage.misc.sageinspect import (
75
is_function_or_cython_function,
76
isclassinstance,
77
sage_formatargspec,
78
sage_getargspec,
79
sage_getdoc_original,
80
)
81
82
_getdoc = getdoc
83
84
85
def getdoc(obj, *args, **kwargs):
86
return sage_getdoc_original(obj)
87
88
89
# ------------------------------------------------------------------
90
91
if TYPE_CHECKING:
92
from collections.abc import Callable, Iterator, Sequence
93
from types import ModuleType
94
from typing import ClassVar, Literal, TypeAlias
95
96
from sphinx.application import Sphinx
97
from sphinx.config import Config
98
from sphinx.environment import BuildEnvironment, _CurrentDocument
99
from sphinx.events import EventManager
100
from sphinx.ext.autodoc.directive import DocumenterBridge
101
from sphinx.registry import SphinxComponentRegistry
102
from sphinx.util.typing import ExtensionMetadata, OptionSpec, _RestifyMode
103
104
_AutodocObjType = Literal[
105
'module', 'class', 'exception', 'function', 'method', 'attribute'
106
]
107
_AutodocProcessDocstringListener: TypeAlias = Callable[
108
[Sphinx, _AutodocObjType, str, Any, dict[str, bool], list[str]], None
109
]
110
111
logger = logging.getLogger(__name__)
112
113
114
# This type isn't exposed directly in any modules, but can be found
115
# here in most Python versions
116
MethodDescriptorType = type(type.__subclasses__)
117
118
# ------------------------------------------------------------------------
119
# As of Sphinx 7.2.6, Sphinx is confused with unquoted "::" in the comment
120
# below.
121
# ------------------------------------------------------------------------
122
#: extended signature RE: with explicit module name separated by "::"
123
py_ext_sig_re = re.compile(
124
r"""^ ([\w.]+::)? # explicit module name
125
([\w.]+\.)? # module and/or class name(s)
126
(\w+) \s* # thing name
127
(?: \[\s*(.*?)\s*])? # optional: type parameters list
128
(?: \((.*)\) # optional: arguments
129
(?:\s* -> \s* (.*))? # return annotation
130
)? $ # and nothing more
131
""",
132
re.VERBOSE,
133
)
134
special_member_re = re.compile(r'^__\S+__$')
135
136
137
def _get_render_mode(
138
typehints_format: Literal['fully-qualified', 'short'],
139
) -> _RestifyMode:
140
if typehints_format == 'short':
141
return 'smart'
142
return 'fully-qualified-except-typing'
143
144
145
def identity(x: Any) -> Any:
146
return x
147
148
149
class _All:
150
"""A special value for :*-members: that matches to any member."""
151
152
def __contains__(self, item: Any) -> bool:
153
return True
154
155
def append(self, item: Any) -> None:
156
pass # nothing
157
158
159
class _Empty:
160
"""A special value for :exclude-members: that never matches to any member."""
161
162
def __contains__(self, item: Any) -> bool:
163
return False
164
165
166
ALL = _All()
167
EMPTY = _Empty()
168
UNINITIALIZED_ATTR = object()
169
INSTANCEATTR = object()
170
SLOTSATTR = object()
171
172
173
def members_option(arg: Any) -> object | list[str]:
174
"""Used to convert the :members: option to auto directives."""
175
if arg in {None, True}:
176
return ALL
177
elif arg is False:
178
return None
179
else:
180
return [x.strip() for x in arg.split(',') if x.strip()]
181
182
183
def exclude_members_option(arg: Any) -> object | set[str]:
184
"""Used to convert the :exclude-members: option."""
185
if arg in {None, True}:
186
return EMPTY
187
return {x.strip() for x in arg.split(',') if x.strip()}
188
189
190
def inherited_members_option(arg: Any) -> set[str]:
191
"""Used to convert the :inherited-members: option to auto directives."""
192
if arg in {None, True}:
193
return {'object'}
194
elif arg:
195
return {x.strip() for x in arg.split(',')}
196
else:
197
return set()
198
199
200
def member_order_option(arg: Any) -> str | None:
201
"""Used to convert the :member-order: option to auto directives."""
202
if arg in {None, True}:
203
return None
204
elif arg in {'alphabetical', 'bysource', 'groupwise'}:
205
return arg
206
else:
207
raise ValueError(__('invalid value for member-order option: %s') % arg)
208
209
210
def class_doc_from_option(arg: Any) -> str | None:
211
"""Used to convert the :class-doc-from: option to autoclass directives."""
212
if arg in {'both', 'class', 'init'}:
213
return arg
214
else:
215
raise ValueError(__('invalid value for class-doc-from option: %s') % arg)
216
217
218
SUPPRESS = object()
219
220
221
def annotation_option(arg: Any) -> Any:
222
if arg in {None, True}:
223
# suppress showing the representation of the object
224
return SUPPRESS
225
else:
226
return arg
227
228
229
def bool_option(arg: Any) -> bool:
230
"""Used to convert flag options to auto directives. (Instead of
231
directives.flag(), which returns None).
232
"""
233
return True
234
235
236
def merge_members_option(options: dict[str, Any]) -> None:
237
"""Merge :private-members: and :special-members: options to the
238
:members: option.
239
"""
240
if options.get('members') is ALL:
241
# merging is not needed when members: ALL
242
return
243
244
members = options.setdefault('members', [])
245
for key in ('private-members', 'special-members'):
246
other_members = options.get(key)
247
if other_members is not None and other_members is not ALL:
248
for member in other_members:
249
if member not in members:
250
members.append(member)
251
252
253
# Some useful event listener factories for autodoc-process-docstring.
254
255
256
def cut_lines(
257
pre: int, post: int = 0, what: Sequence[str] | None = None
258
) -> _AutodocProcessDocstringListener:
259
"""Return a listener that removes the first *pre* and last *post*
260
lines of every docstring. If *what* is a sequence of strings,
261
only docstrings of a type in *what* will be processed.
262
263
Use like this (e.g. in the ``setup()`` function of :file:`conf.py`)::
264
265
from sphinx.ext.autodoc import cut_lines
266
267
app.connect('autodoc-process-docstring', cut_lines(4, what={'module'}))
268
269
This can (and should) be used in place of ``automodule_skip_lines``.
270
"""
271
# -------------------------------------------------------------------------
272
# Sphinx in Sage does not use 'sphinx_toolbox.confval' extension, and hence
273
# does not know the role ":confval:". Use of the role in this module
274
# results in failure of building the reference manual.
275
#
276
# In the above docstring,
277
#
278
# ... in place of :confval:`automodule_skip_lines`.
279
#
280
# was changed to
281
#
282
# ... in place of ``automodule_skip_lines``.
283
# -------------------------------------------------------------------------
284
if not what:
285
what_unique: frozenset[str] = frozenset()
286
elif isinstance(what, str): # strongly discouraged
287
what_unique = frozenset({what})
288
else:
289
what_unique = frozenset(what)
290
291
def process(
292
app: Sphinx,
293
what_: _AutodocObjType,
294
name: str,
295
obj: Any,
296
options: dict[str, bool],
297
lines: list[str],
298
) -> None:
299
if what_unique and what_ not in what_unique:
300
return
301
del lines[:pre]
302
if post:
303
# remove one trailing blank line.
304
if lines and not lines[-1]:
305
lines.pop(-1)
306
del lines[-post:]
307
# make sure there is a blank line at the end
308
if lines and lines[-1]:
309
lines.append('')
310
311
return process
312
313
314
def between(
315
marker: str,
316
what: Sequence[str] | None = None,
317
keepempty: bool = False,
318
exclude: bool = False,
319
) -> _AutodocProcessDocstringListener:
320
"""Return a listener that either keeps, or if *exclude* is True excludes,
321
lines between lines that match the *marker* regular expression. If no line
322
matches, the resulting docstring would be empty, so no change will be made
323
unless *keepempty* is true.
324
325
If *what* is a sequence of strings, only docstrings of a type in *what* will
326
be processed.
327
"""
328
marker_re = re.compile(marker)
329
330
def process(
331
app: Sphinx,
332
what_: _AutodocObjType,
333
name: str,
334
obj: Any,
335
options: dict[str, bool],
336
lines: list[str],
337
) -> None:
338
if what and what_ not in what:
339
return
340
deleted = 0
341
delete = not exclude
342
orig_lines = lines.copy()
343
for i, line in enumerate(orig_lines):
344
if delete:
345
lines.pop(i - deleted)
346
deleted += 1
347
if marker_re.match(line):
348
delete = not delete
349
if delete:
350
lines.pop(i - deleted)
351
deleted += 1
352
if not lines and not keepempty:
353
lines[:] = orig_lines
354
# make sure there is a blank line at the end
355
if lines and lines[-1]:
356
lines.append('')
357
358
return process
359
360
361
# This class is used only in ``sphinx.ext.autodoc.directive``,
362
# But we define this class here to keep compatibility
363
# See: https://github.com/sphinx-doc/sphinx/issues/4538
364
class Options(dict[str, Any]): # NoQA: FURB189
365
"""A dict/attribute hybrid that returns None on nonexisting keys."""
366
367
def copy(self) -> Options:
368
return Options(super().copy())
369
370
def __getattr__(self, name: str) -> Any:
371
try:
372
return self[name.replace('_', '-')]
373
except KeyError:
374
return None
375
376
377
class ObjectMember:
378
"""A member of object.
379
380
This is used for the result of `Documenter.get_module_members()` to
381
represent each member of the object.
382
"""
383
384
__slots__ = '__name__', 'object', 'docstring', 'class_', 'skipped'
385
386
__name__: str
387
object: Any
388
docstring: str | None
389
class_: Any
390
skipped: bool
391
392
def __init__(
393
self,
394
name: str,
395
obj: Any,
396
*,
397
docstring: str | None = None,
398
class_: Any = None,
399
skipped: bool = False,
400
) -> None:
401
self.__name__ = name
402
self.object = obj
403
self.docstring = docstring
404
self.class_ = class_
405
self.skipped = skipped
406
407
def __repr__(self) -> str:
408
return (
409
f'ObjectMember('
410
f'name={self.__name__!r}, '
411
f'obj={self.object!r}, '
412
f'docstring={self.docstring!r}, '
413
f'class_={self.class_!r}, '
414
f'skipped={self.skipped!r}'
415
f')'
416
)
417
418
419
class Documenter:
420
"""A Documenter knows how to autodocument a single object type. When
421
registered with the AutoDirective, it will be used to document objects
422
of that type when needed by autodoc.
423
424
Its *objtype* attribute selects what auto directive it is assigned to
425
(the directive name is 'auto' + objtype), and what directive it generates
426
by default, though that can be overridden by an attribute called
427
*directivetype*.
428
429
A Documenter has an *option_spec* that works like a docutils directive's;
430
in fact, it will be used to parse an auto directive's options that matches
431
the Documenter.
432
"""
433
434
#: name by which the directive is called (auto...) and the default
435
#: generated directive name
436
objtype = 'object'
437
#: indentation by which to indent the directive content
438
content_indent = ' '
439
#: priority if multiple documenters return True from can_document_member
440
priority = 0
441
#: order if autodoc_member_order is set to 'groupwise'
442
member_order = 0
443
#: true if the generated content may contain titles
444
titles_allowed = True
445
446
option_spec: ClassVar[OptionSpec] = {
447
'no-index': bool_option,
448
'no-index-entry': bool_option,
449
'noindex': bool_option,
450
}
451
452
def get_attr(self, obj: Any, name: str, *defargs: Any) -> Any:
453
"""getattr() override for types such as Zope interfaces."""
454
return autodoc_attrgetter(obj, name, *defargs, registry=self.env._registry)
455
456
@classmethod
457
def can_document_member(
458
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
459
) -> bool:
460
"""Called to see if a member can be documented by this Documenter."""
461
msg = 'must be implemented in subclasses'
462
raise NotImplementedError(msg)
463
464
def __init__(
465
self, directive: DocumenterBridge, name: str, indent: str = ''
466
) -> None:
467
self.directive = directive
468
self.config: Config = directive.env.config
469
self.env: BuildEnvironment = directive.env
470
self._current_document: _CurrentDocument = directive.env.current_document
471
self._events: EventManager = directive.env.events
472
self.options = directive.genopt
473
self.name = name
474
self.indent = indent
475
# the module and object path within the module, and the fully
476
# qualified name (all set after resolve_name succeeds)
477
self.modname: str = ''
478
self.module: ModuleType | None = None
479
self.objpath: list[str] = []
480
self.fullname = ''
481
# extra signature items (arguments and return annotation,
482
# also set after resolve_name succeeds)
483
self.args: str | None = None
484
self.retann: str = ''
485
# the object to document (set after import_object succeeds)
486
self.object: Any = None
487
self.object_name = ''
488
# the parent/owner of the object to document
489
self.parent: Any = None
490
# the module analyzer to get at attribute docs, or None
491
self.analyzer: ModuleAnalyzer | None = None
492
493
@property
494
def documenters(self) -> dict[str, type[Documenter]]:
495
"""Returns registered Documenter classes"""
496
return self.env._registry.documenters
497
498
def add_line(self, line: str, source: str, *lineno: int) -> None:
499
"""Append one line of generated reST to the output."""
500
if line.strip(): # not a blank line
501
self.directive.result.append(self.indent + line, source, *lineno)
502
else:
503
self.directive.result.append('', source, *lineno)
504
505
def resolve_name(
506
self, modname: str | None, parents: Any, path: str, base: str
507
) -> tuple[str | None, list[str]]:
508
"""Resolve the module and name of the object to document given by the
509
arguments and the current module/class.
510
511
Must return a pair of the module name and a chain of attributes; for
512
example, it would return ``('zipfile', ['ZipFile', 'open'])`` for the
513
``zipfile.ZipFile.open`` method.
514
"""
515
msg = 'must be implemented in subclasses'
516
raise NotImplementedError(msg)
517
518
def parse_name(self) -> bool:
519
"""Determine what module to import and what attribute to document.
520
521
Returns True and sets *self.modname*, *self.objpath*, *self.fullname*,
522
*self.args* and *self.retann* if parsing and resolving was successful.
523
"""
524
# first, parse the definition -- auto directives for classes and
525
# functions can contain a signature which is then used instead of
526
# an autogenerated one
527
matched = py_ext_sig_re.match(self.name)
528
if matched is None:
529
logger.warning(
530
__('invalid signature for auto%s (%r)'),
531
self.objtype,
532
self.name,
533
type='autodoc',
534
)
535
return False
536
explicit_modname, path, base, tp_list, args, retann = matched.groups()
537
538
# support explicit module and class name separation via ::
539
if explicit_modname is not None:
540
modname = explicit_modname[:-2]
541
parents = path.rstrip('.').split('.') if path else []
542
else:
543
modname = None
544
parents = []
545
546
with mock(self.config.autodoc_mock_imports):
547
modname, self.objpath = self.resolve_name(modname, parents, path, base)
548
549
if not modname:
550
return False
551
552
self.modname = modname
553
self.args = args
554
self.retann = retann
555
self.fullname = '.'.join((self.modname or '', *self.objpath))
556
return True
557
558
def import_object(self, raiseerror: bool = False) -> bool:
559
"""Import the object given by *self.modname* and *self.objpath* and set
560
it as *self.object*.
561
562
Returns True if successful, False if an error occurred.
563
"""
564
with mock(self.config.autodoc_mock_imports):
565
try:
566
ret = import_object(
567
self.modname, self.objpath, self.objtype, attrgetter=self.get_attr
568
)
569
self.module, self.parent, self.object_name, self.object = ret
570
if ismock(self.object):
571
self.object = undecorate(self.object)
572
return True
573
except ImportError as exc:
574
if raiseerror:
575
raise
576
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
577
self.env.note_reread()
578
return False
579
580
def get_real_modname(self) -> str:
581
"""Get the real module name of an object to document.
582
583
It can differ from the name of the module through which the object was
584
imported.
585
"""
586
return self.get_attr(self.object, '__module__', None) or self.modname
587
588
def check_module(self) -> bool:
589
"""Check if *self.object* is really defined in the module given by
590
*self.modname*.
591
"""
592
if self.options.imported_members:
593
return True
594
595
subject = inspect.unpartial(self.object)
596
modname = self.get_attr(subject, '__module__', None)
597
return not modname or modname == self.modname
598
599
def format_args(self, **kwargs: Any) -> str:
600
"""Format the argument signature of *self.object*.
601
602
Should return None if the object does not have a signature.
603
"""
604
return ''
605
606
def format_name(self) -> str:
607
"""Format the name of *self.object*.
608
609
This normally should be something that can be parsed by the generated
610
directive, but doesn't need to be (Sphinx will display it unparsed
611
then).
612
"""
613
# normally the name doesn't contain the module (except for module
614
# directives of course)
615
return '.'.join(self.objpath) or self.modname
616
617
def _call_format_args(self, **kwargs: Any) -> str:
618
if kwargs:
619
try:
620
return self.format_args(**kwargs)
621
except TypeError:
622
# avoid chaining exceptions, by putting nothing here
623
pass
624
625
# retry without arguments for old documenters
626
return self.format_args()
627
628
def format_signature(self, **kwargs: Any) -> str:
629
"""Format the signature (arguments and return annotation) of the object.
630
631
Let the user process it via the ``autodoc-process-signature`` event.
632
"""
633
if self.args is not None:
634
# signature given explicitly
635
args = f'({self.args})'
636
retann = self.retann
637
else:
638
# try to introspect the signature
639
try:
640
retann = None
641
args = self._call_format_args(**kwargs)
642
if args:
643
matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args)
644
if matched:
645
args = matched.group(1)
646
retann = matched.group(2)
647
except Exception as exc:
648
logger.warning(
649
__('error while formatting arguments for %s: %s'),
650
self.fullname,
651
exc,
652
type='autodoc',
653
)
654
args = None
655
656
result = self._events.emit_firstresult(
657
'autodoc-process-signature',
658
self.objtype,
659
self.fullname,
660
self.object,
661
self.options,
662
args,
663
retann,
664
)
665
if result:
666
args, retann = result
667
668
if args is not None:
669
return args + ((' -> %s' % retann) if retann else '')
670
else:
671
return ''
672
673
def add_directive_header(self, sig: str) -> None:
674
"""Add the directive header and options to the generated content."""
675
domain = getattr(self, 'domain', 'py')
676
directive = getattr(self, 'directivetype', self.objtype)
677
name = self.format_name()
678
sourcename = self.get_sourcename()
679
680
# one signature per line, indented by column
681
prefix = f'.. {domain}:{directive}:: '
682
for i, sig_line in enumerate(sig.split('\n')):
683
self.add_line(f'{prefix}{name}{sig_line}', sourcename)
684
if i == 0:
685
prefix = ' ' * len(prefix)
686
687
if self.options.no_index or self.options.noindex:
688
self.add_line(' :no-index:', sourcename)
689
if self.options.no_index_entry:
690
self.add_line(' :no-index-entry:', sourcename)
691
if self.objpath:
692
# Be explicit about the module, this is necessary since .. class::
693
# etc. don't support a prepended module name
694
self.add_line(' :module: %s' % self.modname, sourcename)
695
696
def get_doc(self) -> list[list[str]] | None:
697
"""Decode and return lines of the docstring(s) for the object.
698
699
When it returns None, autodoc-process-docstring will not be called for this
700
object.
701
"""
702
docstring = getdoc(
703
self.object,
704
self.get_attr,
705
self.config.autodoc_inherit_docstrings,
706
self.parent,
707
self.object_name,
708
)
709
if docstring:
710
tab_width = self.directive.state.document.settings.tab_width
711
return [prepare_docstring(docstring, tab_width)]
712
return []
713
714
def process_doc(self, docstrings: list[list[str]]) -> Iterator[str]:
715
"""Let the user process the docstrings before adding them."""
716
for docstringlines in docstrings:
717
if self._events is not None:
718
# let extensions preprocess docstrings
719
self._events.emit(
720
'autodoc-process-docstring',
721
self.objtype,
722
self.fullname,
723
self.object,
724
self.options,
725
docstringlines,
726
)
727
728
if docstringlines and docstringlines[-1]:
729
# append a blank line to the end of the docstring
730
docstringlines.append('')
731
732
yield from docstringlines
733
734
def get_sourcename(self) -> str:
735
obj_module = inspect.safe_getattr(self.object, '__module__', None)
736
obj_qualname = inspect.safe_getattr(self.object, '__qualname__', None)
737
if obj_module and obj_qualname:
738
# Get the correct location of docstring from self.object
739
# to support inherited methods
740
fullname = f'{self.object.__module__}.{self.object.__qualname__}'
741
else:
742
fullname = self.fullname
743
744
if self.analyzer:
745
return f'{self.analyzer.srcname}:docstring of {fullname}'
746
else:
747
return 'docstring of %s' % fullname
748
749
def add_content(self, more_content: StringList | None) -> None:
750
"""Add content from docstrings, attribute documentation and user."""
751
docstring = True
752
753
# set sourcename and add content from attribute documentation
754
sourcename = self.get_sourcename()
755
if self.analyzer:
756
attr_docs = self.analyzer.find_attr_docs()
757
if self.objpath:
758
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
759
if key in attr_docs:
760
docstring = False
761
# make a copy of docstring for attributes to avoid cache
762
# the change of autodoc-process-docstring event.
763
attribute_docstrings = [list(attr_docs[key])]
764
765
for i, line in enumerate(self.process_doc(attribute_docstrings)):
766
self.add_line(line, sourcename, i)
767
768
# add content from docstrings
769
if docstring:
770
docstrings = self.get_doc()
771
if docstrings is None:
772
# Do not call autodoc-process-docstring on get_doc() returns None.
773
pass
774
else:
775
if not docstrings:
776
# append at least a dummy docstring, so that the event
777
# autodoc-process-docstring is fired and can add some
778
# content if desired
779
docstrings.append([])
780
for i, line in enumerate(self.process_doc(docstrings)):
781
self.add_line(line, sourcename, i)
782
783
# add additional content (e.g. from document), if present
784
if more_content:
785
for line, src in zip(more_content.data, more_content.items, strict=True):
786
self.add_line(line, src[0], src[1])
787
788
def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]:
789
"""Return `(members_check_module, members)` where `members` is a
790
list of `(membername, member)` pairs of the members of *self.object*.
791
792
If *want_all* is True, return all members. Else, only return those
793
members given by *self.options.members* (which may also be None).
794
"""
795
msg = 'must be implemented in subclasses'
796
raise NotImplementedError(msg)
797
798
def filter_members(
799
self, members: list[ObjectMember], want_all: bool
800
) -> list[tuple[str, Any, bool]]:
801
"""Filter the given member list.
802
803
Members are skipped if
804
805
- they are private (except if given explicitly or the private-members
806
option is set)
807
- they are special methods (except if given explicitly or the
808
special-members option is set)
809
- they are undocumented (except if the undoc-members option is set)
810
811
The user can override the skipping decision by connecting to the
812
``autodoc-skip-member`` event.
813
"""
814
815
def is_filtered_inherited_member(name: str, obj: Any) -> bool:
816
inherited_members = self.options.inherited_members or set()
817
seen = set()
818
819
if inspect.isclass(self.object):
820
for cls in self.object.__mro__:
821
if name in cls.__dict__:
822
seen.add(cls)
823
if (
824
cls.__name__ in inherited_members
825
and cls != self.object
826
and any(
827
issubclass(potential_child, cls) for potential_child in seen
828
)
829
):
830
# given member is a member of specified *super class*
831
return True
832
if name in cls.__dict__:
833
return False
834
if name in self.get_attr(cls, '__annotations__', {}):
835
return False
836
if isinstance(obj, ObjectMember) and obj.class_ is cls:
837
return False
838
839
return False
840
841
ret = []
842
843
# search for members in source code too
844
namespace = '.'.join(self.objpath) # will be empty for modules
845
846
if self.analyzer:
847
attr_docs = self.analyzer.find_attr_docs()
848
else:
849
attr_docs = {}
850
851
# process members and determine which to skip
852
for obj in members:
853
membername = obj.__name__
854
member = obj.object
855
856
# ---------------------------------------------------
857
# Issue #17455: Immediately skip lazy imports to avoid
858
# deprecation messages.
859
from sage.misc.lazy_import import LazyImport
860
if isinstance(member, LazyImport):
861
continue
862
# ---------------------------------------------------
863
864
# if isattr is True, the member is documented as an attribute
865
isattr = member is INSTANCEATTR or (namespace, membername) in attr_docs
866
867
try:
868
doc = getdoc(
869
member,
870
self.get_attr,
871
self.config.autodoc_inherit_docstrings,
872
self.object,
873
membername,
874
)
875
if not isinstance(doc, str):
876
# Ignore non-string __doc__
877
doc = None
878
879
# if the member __doc__ is the same as self's __doc__, it's just
880
# inherited and therefore not the member's doc
881
cls = self.get_attr(member, '__class__', None)
882
if cls:
883
cls_doc = self.get_attr(cls, '__doc__', None)
884
if cls_doc == doc:
885
doc = None
886
887
if isinstance(obj, ObjectMember) and obj.docstring:
888
# hack for ClassDocumenter to inject docstring via ObjectMember
889
doc = obj.docstring
890
891
doc, metadata = separate_metadata(doc)
892
has_doc = bool(doc)
893
894
if 'private' in metadata:
895
# consider a member private if docstring has "private" metadata
896
isprivate = True
897
elif 'public' in metadata:
898
# consider a member public if docstring has "public" metadata
899
isprivate = False
900
else:
901
isprivate = membername.startswith('_')
902
903
keep = False
904
if ismock(member) and (namespace, membername) not in attr_docs:
905
# mocked module or object
906
pass
907
elif (
908
self.options.exclude_members
909
and membername in self.options.exclude_members
910
):
911
# remove members given by exclude-members
912
keep = False
913
elif want_all and special_member_re.match(membername):
914
# special __methods__
915
if (
916
self.options.special_members
917
and membername in self.options.special_members
918
):
919
if membername == '__doc__': # NoQA: SIM114
920
keep = False
921
elif is_filtered_inherited_member(membername, obj):
922
keep = False
923
else:
924
keep = has_doc or self.options.undoc_members
925
else:
926
keep = False
927
elif (namespace, membername) in attr_docs:
928
if want_all and isprivate:
929
if self.options.private_members is None:
930
keep = False
931
else:
932
keep = membername in self.options.private_members
933
else:
934
# keep documented attributes
935
keep = True
936
elif want_all and isprivate:
937
if has_doc or self.options.undoc_members:
938
if self.options.private_members is None: # NoQA: SIM114
939
keep = False
940
elif is_filtered_inherited_member(membername, obj):
941
keep = False
942
else:
943
keep = membername in self.options.private_members
944
else:
945
keep = False
946
elif self.options.members is ALL and is_filtered_inherited_member(
947
membername, obj
948
):
949
keep = False
950
else:
951
# ignore undocumented members if :undoc-members: is not given
952
keep = has_doc or self.options.undoc_members
953
954
if isinstance(obj, ObjectMember) and obj.skipped:
955
# forcedly skipped member (ex. a module attribute not defined in __all__)
956
keep = False
957
958
# give the user a chance to decide whether this member
959
# should be skipped
960
if self._events is not None:
961
# let extensions preprocess docstrings
962
skip_user = self._events.emit_firstresult(
963
'autodoc-skip-member',
964
self.objtype,
965
membername,
966
member,
967
not keep,
968
self.options,
969
)
970
if skip_user is not None:
971
keep = not skip_user
972
except Exception as exc:
973
logger.warning(
974
__(
975
'autodoc: failed to determine %s.%s (%r) to be documented, '
976
'the following exception was raised:\n%s'
977
),
978
self.name,
979
membername,
980
member,
981
exc,
982
type='autodoc',
983
)
984
keep = False
985
986
if keep:
987
ret.append((membername, member, isattr))
988
989
return ret
990
991
def document_members(self, all_members: bool = False) -> None:
992
"""Generate reST for member documentation.
993
994
If *all_members* is True, document all members, else those given by
995
*self.options.members*.
996
"""
997
# set current namespace for finding members
998
self._current_document.autodoc_module = self.modname
999
if self.objpath:
1000
self._current_document.autodoc_class = self.objpath[0]
1001
1002
want_all = (
1003
all_members or self.options.inherited_members or self.options.members is ALL
1004
)
1005
# find out which members are documentable
1006
members_check_module, members = self.get_object_members(want_all)
1007
1008
# document non-skipped members
1009
member_documenters: list[tuple[Documenter, bool]] = []
1010
for mname, member, isattr in self.filter_members(members, want_all):
1011
classes = [
1012
cls
1013
for cls in self.documenters.values()
1014
if cls.can_document_member(member, mname, isattr, self)
1015
]
1016
if not classes:
1017
# don't know how to document this member
1018
continue
1019
# prefer the documenter with the highest priority
1020
classes.sort(key=lambda cls: cls.priority)
1021
# give explicitly separated module name, so that members
1022
# of inner classes can be documented
1023
full_mname = f'{self.modname}::' + '.'.join((*self.objpath, mname))
1024
documenter = classes[-1](self.directive, full_mname, self.indent)
1025
member_documenters.append((documenter, isattr))
1026
1027
member_order = self.options.member_order or self.config.autodoc_member_order
1028
# We now try to import all objects before ordering them. This is to
1029
# avoid possible circular imports if we were to import objects after
1030
# their associated documenters have been sorted.
1031
member_documenters = [
1032
(documenter, isattr)
1033
for documenter, isattr in member_documenters
1034
if documenter.parse_name() and documenter.import_object()
1035
]
1036
member_documenters = self.sort_members(member_documenters, member_order)
1037
1038
for documenter, isattr in member_documenters:
1039
assert documenter.modname
1040
# We can directly call ._generate() since the documenters
1041
# already called parse_name() and import_object() before.
1042
#
1043
# Note that those two methods above do not emit events, so
1044
# whatever objects we deduced should not have changed.
1045
documenter._generate(
1046
all_members=True,
1047
real_modname=self.real_modname,
1048
check_module=members_check_module and not isattr,
1049
)
1050
1051
# reset current objects
1052
self._current_document.autodoc_module = ''
1053
self._current_document.autodoc_class = ''
1054
1055
def sort_members(
1056
self, documenters: list[tuple[Documenter, bool]], order: str
1057
) -> list[tuple[Documenter, bool]]:
1058
"""Sort the given member list."""
1059
if order == 'groupwise':
1060
# sort by group; alphabetically within groups
1061
documenters.sort(key=lambda e: (e[0].member_order, e[0].name))
1062
elif order == 'bysource':
1063
# By default, member discovery order matches source order,
1064
# as dicts are insertion-ordered.
1065
if self.analyzer:
1066
# sort by source order, by virtue of the module analyzer
1067
tagorder = self.analyzer.tagorder
1068
1069
def keyfunc(entry: tuple[Documenter, bool]) -> int:
1070
fullname = entry[0].name.split('::')[1]
1071
return tagorder.get(fullname, len(tagorder))
1072
1073
documenters.sort(key=keyfunc)
1074
else: # alphabetical
1075
documenters.sort(key=lambda e: e[0].name)
1076
1077
return documenters
1078
1079
def generate(
1080
self,
1081
more_content: StringList | None = None,
1082
real_modname: str | None = None,
1083
check_module: bool = False,
1084
all_members: bool = False,
1085
) -> None:
1086
"""Generate reST for the object given by *self.name*, and possibly for
1087
its members.
1088
1089
If *more_content* is given, include that content. If *real_modname* is
1090
given, use that module name to find attribute docs. If *check_module* is
1091
True, only generate if the object is defined in the module name it is
1092
imported from. If *all_members* is True, document all members.
1093
"""
1094
if not self.parse_name():
1095
# need a module to import
1096
logger.warning(
1097
__(
1098
"don't know which module to import for autodocumenting "
1099
'%r (try placing a "module" or "currentmodule" directive '
1100
'in the document, or giving an explicit module name)'
1101
),
1102
self.name,
1103
type='autodoc',
1104
)
1105
return
1106
1107
# now, import the module and get object to document
1108
if not self.import_object():
1109
return
1110
1111
self._generate(more_content, real_modname, check_module, all_members)
1112
1113
def _generate(
1114
self,
1115
more_content: StringList | None = None,
1116
real_modname: str | None = None,
1117
check_module: bool = False,
1118
all_members: bool = False,
1119
) -> None:
1120
# If there is no real module defined, figure out which to use.
1121
# The real module is used in the module analyzer to look up the module
1122
# where the attribute documentation would actually be found in.
1123
# This is used for situations where you have a module that collects the
1124
# functions and classes of internal submodules.
1125
guess_modname = self.get_real_modname()
1126
self.real_modname: str = real_modname or guess_modname
1127
1128
# try to also get a source code analyzer for attribute docs
1129
try:
1130
self.analyzer = ModuleAnalyzer.for_module(self.real_modname)
1131
# parse right now, to get PycodeErrors on parsing (results will
1132
# be cached anyway)
1133
self.analyzer.find_attr_docs()
1134
except PycodeError as exc:
1135
logger.debug('[autodoc] module analyzer failed: %s', exc)
1136
# no source file -- e.g. for builtin and C modules
1137
self.analyzer = None
1138
# at least add the module.__file__ as a dependency
1139
if module___file__ := getattr(self.module, '__file__', ''):
1140
self.directive.record_dependencies.add(module___file__)
1141
else:
1142
self.directive.record_dependencies.add(self.analyzer.srcname)
1143
1144
if self.real_modname != guess_modname:
1145
# Add module to dependency list if target object is defined in other module.
1146
try:
1147
analyzer = ModuleAnalyzer.for_module(guess_modname)
1148
self.directive.record_dependencies.add(analyzer.srcname)
1149
except PycodeError:
1150
pass
1151
1152
docstrings: list[str] = functools.reduce(
1153
operator.iadd, self.get_doc() or [], []
1154
)
1155
if ismock(self.object) and not docstrings:
1156
logger.warning(
1157
__('A mocked object is detected: %r'),
1158
self.name,
1159
type='autodoc',
1160
subtype='mocked_object',
1161
)
1162
1163
# check __module__ of object (for members not given explicitly)
1164
if check_module:
1165
if not self.check_module():
1166
return
1167
1168
sourcename = self.get_sourcename()
1169
1170
# make sure that the result starts with an empty line. This is
1171
# necessary for some situations where another directive preprocesses
1172
# reST and no starting newline is present
1173
self.add_line('', sourcename)
1174
1175
# format the object's signature, if any
1176
try:
1177
sig = self.format_signature()
1178
except Exception as exc:
1179
logger.warning(
1180
__('error while formatting signature for %s: %s'),
1181
self.fullname,
1182
exc,
1183
type='autodoc',
1184
)
1185
return
1186
1187
# generate the directive header and options, if applicable
1188
self.add_directive_header(sig)
1189
self.add_line('', sourcename)
1190
1191
# e.g. the module directive doesn't have content
1192
self.indent += self.content_indent
1193
1194
# add all content (from docstrings, attribute docs etc.)
1195
self.add_content(more_content)
1196
1197
# document members, if possible
1198
self.document_members(all_members)
1199
1200
1201
class ModuleDocumenter(Documenter):
1202
"""Specialized Documenter subclass for modules."""
1203
1204
objtype = 'module'
1205
content_indent = ''
1206
_extra_indent = ' '
1207
1208
option_spec: ClassVar[OptionSpec] = {
1209
'members': members_option,
1210
'undoc-members': bool_option,
1211
'no-index': bool_option,
1212
'no-index-entry': bool_option,
1213
'inherited-members': inherited_members_option,
1214
'show-inheritance': bool_option,
1215
'synopsis': identity,
1216
'platform': identity,
1217
'deprecated': bool_option,
1218
'member-order': member_order_option,
1219
'exclude-members': exclude_members_option,
1220
'private-members': members_option,
1221
'special-members': members_option,
1222
'imported-members': bool_option,
1223
'ignore-module-all': bool_option,
1224
'no-value': bool_option,
1225
'noindex': bool_option,
1226
}
1227
1228
def __init__(self, *args: Any) -> None:
1229
super().__init__(*args)
1230
merge_members_option(self.options)
1231
self.__all__: Sequence[str] | None = None
1232
1233
def add_content(self, more_content: StringList | None) -> None:
1234
old_indent = self.indent
1235
self.indent += self._extra_indent
1236
super().add_content(None)
1237
self.indent = old_indent
1238
if more_content:
1239
for line, src in zip(more_content.data, more_content.items, strict=True):
1240
self.add_line(line, src[0], src[1])
1241
1242
@classmethod
1243
def can_document_member(
1244
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
1245
) -> bool:
1246
# don't document submodules automatically
1247
return False
1248
1249
def resolve_name(
1250
self, modname: str | None, parents: Any, path: str, base: str
1251
) -> tuple[str | None, list[str]]:
1252
if modname is not None:
1253
logger.warning(
1254
__('"::" in automodule name doesn\'t make sense'), type='autodoc'
1255
)
1256
return (path or '') + base, []
1257
1258
def parse_name(self) -> bool:
1259
ret = super().parse_name()
1260
if self.args or self.retann:
1261
logger.warning(
1262
__('signature arguments or return annotation given for automodule %s'),
1263
self.fullname,
1264
type='autodoc',
1265
)
1266
return ret
1267
1268
def import_object(self, raiseerror: bool = False) -> bool:
1269
ret = super().import_object(raiseerror)
1270
1271
try:
1272
if not self.options.ignore_module_all:
1273
self.__all__ = inspect.getall(self.object)
1274
except ValueError as exc:
1275
# invalid __all__ found.
1276
logger.warning(
1277
__(
1278
'__all__ should be a list of strings, not %r '
1279
'(in module %s) -- ignoring __all__'
1280
),
1281
exc.args[0],
1282
self.fullname,
1283
type='autodoc',
1284
)
1285
1286
return ret
1287
1288
def add_directive_header(self, sig: str) -> None:
1289
Documenter.add_directive_header(self, sig)
1290
1291
sourcename = self.get_sourcename()
1292
1293
# add some module-specific options
1294
if self.options.synopsis:
1295
self.add_line(' :synopsis: ' + self.options.synopsis, sourcename)
1296
if self.options.platform:
1297
self.add_line(' :platform: ' + self.options.platform, sourcename)
1298
if self.options.deprecated:
1299
self.add_line(' :deprecated:', sourcename)
1300
if self.options.no_index_entry:
1301
self.add_line(' :no-index-entry:', sourcename)
1302
1303
def get_module_members(self) -> dict[str, ObjectMember]:
1304
"""Get members of target module."""
1305
if self.analyzer:
1306
attr_docs = self.analyzer.attr_docs
1307
else:
1308
attr_docs = {}
1309
1310
members: dict[str, ObjectMember] = {}
1311
for name in dir(self.object):
1312
try:
1313
value = safe_getattr(self.object, name, None)
1314
if ismock(value):
1315
value = undecorate(value)
1316
docstring = attr_docs.get(('', name), [])
1317
members[name] = ObjectMember(
1318
name, value, docstring='\n'.join(docstring)
1319
)
1320
except AttributeError:
1321
continue
1322
1323
# annotation only member (ex. attr: int)
1324
for name in inspect.getannotations(self.object):
1325
if name not in members:
1326
docstring = attr_docs.get(('', name), [])
1327
members[name] = ObjectMember(
1328
name, INSTANCEATTR, docstring='\n'.join(docstring)
1329
)
1330
1331
return members
1332
1333
def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]:
1334
members = self.get_module_members()
1335
if want_all:
1336
if self.__all__ is None:
1337
# for implicit module members, check __module__ to avoid
1338
# documenting imported objects
1339
return True, list(members.values())
1340
else:
1341
for member in members.values():
1342
if member.__name__ not in self.__all__:
1343
member.skipped = True
1344
1345
return False, list(members.values())
1346
else:
1347
memberlist = self.options.members or []
1348
ret = []
1349
for name in memberlist:
1350
if name in members:
1351
ret.append(members[name])
1352
else:
1353
logger.warning(
1354
__(
1355
'missing attribute mentioned in :members: option: '
1356
'module %s, attribute %s'
1357
),
1358
safe_getattr(self.object, '__name__', '???'),
1359
name,
1360
type='autodoc',
1361
)
1362
return False, ret
1363
1364
def sort_members(
1365
self, documenters: list[tuple[Documenter, bool]], order: str
1366
) -> list[tuple[Documenter, bool]]:
1367
if order == 'bysource' and self.__all__:
1368
assert self.__all__ is not None
1369
module_all = self.__all__
1370
module_all_set = set(module_all)
1371
module_all_len = len(module_all)
1372
1373
# Sort alphabetically first (for members not listed on the __all__)
1374
documenters.sort(key=lambda e: e[0].name)
1375
1376
# Sort by __all__
1377
def keyfunc(entry: tuple[Documenter, bool]) -> int:
1378
name = entry[0].name.split('::')[1]
1379
if name in module_all_set:
1380
return module_all.index(name)
1381
else:
1382
return module_all_len
1383
1384
documenters.sort(key=keyfunc)
1385
1386
return documenters
1387
else:
1388
return super().sort_members(documenters, order)
1389
1390
1391
class ModuleLevelDocumenter(Documenter):
1392
"""Specialized Documenter subclass for objects on module level (functions,
1393
classes, data/constants).
1394
"""
1395
1396
def resolve_name(
1397
self, modname: str | None, parents: Any, path: str, base: str
1398
) -> tuple[str | None, list[str]]:
1399
if modname is not None:
1400
return modname, [*parents, base]
1401
if path:
1402
modname = path.rstrip('.')
1403
return modname, [*parents, base]
1404
1405
# if documenting a toplevel object without explicit module,
1406
# it can be contained in another auto directive ...
1407
modname = self._current_document.autodoc_module
1408
# ... or in the scope of a module directive
1409
if not modname:
1410
modname = self.env.ref_context.get('py:module')
1411
# ... else, it stays None, which means invalid
1412
return modname, [*parents, base]
1413
1414
1415
class ClassLevelDocumenter(Documenter):
1416
"""Specialized Documenter subclass for objects on class level (methods,
1417
attributes).
1418
"""
1419
1420
def resolve_name(
1421
self, modname: str | None, parents: Any, path: str, base: str
1422
) -> tuple[str | None, list[str]]:
1423
if modname is not None:
1424
return modname, [*parents, base]
1425
1426
if path:
1427
mod_cls = path.rstrip('.')
1428
else:
1429
# if documenting a class-level object without path,
1430
# there must be a current class, either from a parent
1431
# auto directive ...
1432
mod_cls = self._current_document.autodoc_class
1433
# ... or from a class directive
1434
if not mod_cls:
1435
mod_cls = self.env.ref_context.get('py:class', '')
1436
# ... if still falsy, there's no way to know
1437
if not mod_cls:
1438
return None, []
1439
modname, sep, cls = mod_cls.rpartition('.')
1440
parents = [cls]
1441
# if the module name is still missing, get it like above
1442
if not modname:
1443
modname = self._current_document.autodoc_module
1444
if not modname:
1445
modname = self.env.ref_context.get('py:module')
1446
# ... else, it stays None, which means invalid
1447
return modname, [*parents, base]
1448
1449
1450
class DocstringSignatureMixin:
1451
"""Mixin for FunctionDocumenter and MethodDocumenter to provide the
1452
feature of reading the signature from the docstring.
1453
"""
1454
1455
_new_docstrings: list[list[str]] | None = None
1456
_signatures: list[str] = []
1457
1458
def _find_signature(self) -> tuple[str | None, str | None] | None:
1459
# candidates of the object name
1460
valid_names = [self.objpath[-1]] # type: ignore[attr-defined]
1461
if isinstance(self, ClassDocumenter):
1462
valid_names.append('__init__')
1463
if hasattr(self.object, '__mro__'):
1464
valid_names.extend(cls.__name__ for cls in self.object.__mro__)
1465
1466
docstrings = self.get_doc()
1467
if docstrings is None:
1468
return None, None
1469
self._new_docstrings = docstrings[:]
1470
self._signatures = []
1471
result = None
1472
for i, doclines in enumerate(docstrings):
1473
for j, line in enumerate(doclines):
1474
if not line:
1475
# no lines in docstring, no match
1476
break
1477
1478
if line.endswith('\\'):
1479
line = line.rstrip('\\').rstrip()
1480
1481
# match first line of docstring against signature RE
1482
match = py_ext_sig_re.match(line)
1483
if not match:
1484
break
1485
exmod, path, base, tp_list, args, retann = match.groups()
1486
1487
# the base name must match ours
1488
if base not in valid_names:
1489
break
1490
1491
# re-prepare docstring to ignore more leading indentation
1492
directive = self.directive # type: ignore[attr-defined]
1493
tab_width = directive.state.document.settings.tab_width
1494
self._new_docstrings[i] = prepare_docstring(
1495
'\n'.join(doclines[j + 1 :]), tab_width
1496
)
1497
1498
if result is None:
1499
# first signature
1500
result = args, retann
1501
else:
1502
# subsequent signatures
1503
self._signatures.append(f'({args}) -> {retann}')
1504
1505
if result is not None:
1506
# finish the loop when signature found
1507
break
1508
1509
return result
1510
1511
def get_doc(self) -> list[list[str]] | None:
1512
if self._new_docstrings is not None:
1513
return self._new_docstrings
1514
return super().get_doc() # type: ignore[misc]
1515
1516
def format_signature(self, **kwargs: Any) -> str:
1517
self.args: str | None
1518
if self.args is None and self.config.autodoc_docstring_signature: # type: ignore[attr-defined]
1519
# only act if a signature is not explicitly given already, and if
1520
# the feature is enabled
1521
result = self._find_signature()
1522
if result is not None:
1523
self.args, self.retann = result
1524
sig = super().format_signature(**kwargs) # type: ignore[misc]
1525
if self._signatures:
1526
return '\n'.join((sig, *self._signatures))
1527
else:
1528
return sig
1529
1530
1531
class DocstringStripSignatureMixin(DocstringSignatureMixin):
1532
"""Mixin for AttributeDocumenter to provide the
1533
feature of stripping any function signature from the docstring.
1534
"""
1535
1536
def format_signature(self, **kwargs: Any) -> str:
1537
if self.args is None and self.config.autodoc_docstring_signature: # type: ignore[attr-defined]
1538
# only act if a signature is not explicitly given already, and if
1539
# the feature is enabled
1540
result = self._find_signature()
1541
if result is not None:
1542
# Discarding _args is a only difference with
1543
# DocstringSignatureMixin.format_signature.
1544
# Documenter.format_signature use self.args value to format.
1545
_args, self.retann = result
1546
return super().format_signature(**kwargs)
1547
1548
1549
class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore[misc]
1550
"""Specialized Documenter subclass for functions."""
1551
1552
objtype = 'function'
1553
member_order = 30
1554
1555
@classmethod
1556
def can_document_member(
1557
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
1558
) -> bool:
1559
# --------------------------------------------------------------------
1560
# supports functions, builtins but, unlike Sphinx' autodoc,
1561
# does not support bound methods exported at the module level
1562
if is_function_or_cython_function(member) or inspect.isbuiltin(member):
1563
return True
1564
# Issue #9976: It can be documented if it is a genuine function.
1565
# Often, a class instance has the same documentation as its class,
1566
# and then we typically want to document the class and not the
1567
# instance. However, there is an exception: CachedFunction(f) returns
1568
# a class instance, whose doc string coincides with that of f and is
1569
# thus different from that of the class CachedFunction. In that
1570
# situation, we want that f is documented.
1571
return (isclassinstance(member) and
1572
sage_getdoc_original(member) != sage_getdoc_original(member.__class__))
1573
# --------------------------------------------------------------------
1574
1575
def format_args(self, **kwargs: Any) -> str:
1576
if self.config.autodoc_typehints in {'none', 'description'}:
1577
kwargs.setdefault('show_annotation', False)
1578
if self.config.autodoc_typehints_format == 'short':
1579
kwargs.setdefault('unqualified_typehints', True)
1580
if self.config.python_display_short_literal_types:
1581
kwargs.setdefault('short_literals', True)
1582
1583
try:
1584
self._events.emit('autodoc-before-process-signature', self.object, False)
1585
# ----------------------------------------------------------------
1586
# Issue #9976: Support the _sage_argspec_ attribute which makes it
1587
# possible to get argument specification of decorated callables in
1588
# documentation correct. See e.g. sage.misc.decorators.sage_wraps
1589
obj = self.object
1590
1591
if hasattr(obj, "_sage_argspec_"):
1592
argspec = obj._sage_argspec_()
1593
if inspect.isbuiltin(obj) or \
1594
inspect.ismethoddescriptor(obj):
1595
# cannot introspect arguments of a C function or method
1596
# unless a function to do so is supplied
1597
argspec = sage_getargspec(obj)
1598
argspec = sage_getargspec(obj)
1599
if isclassinstance(obj) or inspect.isclass(obj):
1600
# if a class should be documented as function, we try
1601
# to use the constructor signature as function
1602
# signature without the first argument.
1603
if argspec is not None and argspec[0]:
1604
del argspec[0][0]
1605
1606
if argspec is None:
1607
return None
1608
args = sage_formatargspec(*argspec)
1609
# ----------------------------------------------------------------
1610
except TypeError as exc:
1611
logger.warning(
1612
__('Failed to get a function signature for %s: %s'), self.fullname, exc
1613
)
1614
return ''
1615
except ValueError:
1616
args = ''
1617
1618
if self.config.strip_signature_backslash:
1619
# escape backslashes for reST
1620
args = args.replace('\\', '\\\\')
1621
return args
1622
1623
def document_members(self, all_members: bool = False) -> None:
1624
pass
1625
1626
def add_directive_header(self, sig: str) -> None:
1627
sourcename = self.get_sourcename()
1628
super().add_directive_header(sig)
1629
1630
is_coro = inspect.iscoroutinefunction(self.object)
1631
is_acoro = inspect.isasyncgenfunction(self.object)
1632
if is_coro or is_acoro:
1633
self.add_line(' :async:', sourcename)
1634
1635
def format_signature(self, **kwargs: Any) -> str:
1636
if self.config.autodoc_typehints_format == 'short':
1637
kwargs.setdefault('unqualified_typehints', True)
1638
if self.config.python_display_short_literal_types:
1639
kwargs.setdefault('short_literals', True)
1640
1641
sigs = []
1642
if (
1643
self.analyzer
1644
and '.'.join(self.objpath) in self.analyzer.overloads
1645
and self.config.autodoc_typehints != 'none'
1646
):
1647
# Use signatures for overloaded functions instead of the implementation function.
1648
overloaded = True
1649
else:
1650
overloaded = False
1651
sig = super().format_signature(**kwargs)
1652
sigs.append(sig)
1653
1654
if inspect.is_singledispatch_function(self.object):
1655
# append signature of singledispatch'ed functions
1656
for typ, func in self.object.registry.items():
1657
if typ is object:
1658
pass # default implementation. skipped.
1659
else:
1660
dispatchfunc = self.annotate_to_first_argument(func, typ)
1661
if dispatchfunc:
1662
documenter = FunctionDocumenter(self.directive, '')
1663
documenter.object = dispatchfunc
1664
documenter.objpath = ['']
1665
sigs.append(documenter.format_signature())
1666
if overloaded and self.analyzer is not None:
1667
actual = inspect.signature(
1668
self.object, type_aliases=self.config.autodoc_type_aliases
1669
)
1670
__globals__ = safe_getattr(self.object, '__globals__', {})
1671
for overload in self.analyzer.overloads['.'.join(self.objpath)]:
1672
overload = self.merge_default_value(actual, overload)
1673
overload = evaluate_signature(
1674
overload, __globals__, self.config.autodoc_type_aliases
1675
)
1676
1677
sig = stringify_signature(overload, **kwargs)
1678
sigs.append(sig)
1679
1680
return '\n'.join(sigs)
1681
1682
def merge_default_value(self, actual: Signature, overload: Signature) -> Signature:
1683
"""Merge default values of actual implementation to the overload variants."""
1684
parameters = list(overload.parameters.values())
1685
for i, param in enumerate(parameters):
1686
actual_param = actual.parameters.get(param.name)
1687
if actual_param and param.default == '...':
1688
parameters[i] = param.replace(default=actual_param.default)
1689
1690
return overload.replace(parameters=parameters)
1691
1692
def annotate_to_first_argument(
1693
self, func: Callable[..., Any], typ: type
1694
) -> Callable[..., Any] | None:
1695
"""Annotate type hint to the first argument of function if needed."""
1696
try:
1697
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
1698
except TypeError as exc:
1699
logger.warning(
1700
__('Failed to get a function signature for %s: %s'), self.fullname, exc
1701
)
1702
return None
1703
except ValueError:
1704
return None
1705
1706
if len(sig.parameters) == 0:
1707
return None
1708
1709
def dummy(): # type: ignore[no-untyped-def] # NoQA: ANN202
1710
pass
1711
1712
params = list(sig.parameters.values())
1713
if params[0].annotation is Parameter.empty:
1714
params[0] = params[0].replace(annotation=typ)
1715
try:
1716
dummy.__signature__ = sig.replace(parameters=params) # type: ignore[attr-defined]
1717
return dummy
1718
except (AttributeError, TypeError):
1719
# failed to update signature (ex. built-in or extension types)
1720
return None
1721
1722
return func
1723
1724
1725
class DecoratorDocumenter(FunctionDocumenter):
1726
"""Specialized Documenter subclass for decorator functions."""
1727
1728
objtype = 'decorator'
1729
1730
# must be lower than FunctionDocumenter
1731
priority = -1
1732
1733
def format_args(self, **kwargs: Any) -> str:
1734
args = super().format_args(**kwargs)
1735
if ',' in args:
1736
return args
1737
else:
1738
return ''
1739
1740
1741
# Types which have confusing metaclass signatures it would be best not to show.
1742
# These are listed by name, rather than storing the objects themselves, to avoid
1743
# needing to import the modules.
1744
_METACLASS_CALL_BLACKLIST = frozenset({
1745
'enum.EnumType.__call__',
1746
})
1747
1748
1749
# Types whose __new__ signature is a pass-through.
1750
_CLASS_NEW_BLACKLIST = frozenset({
1751
'typing.Generic.__new__',
1752
})
1753
1754
1755
class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore[misc]
1756
"""Specialized Documenter subclass for classes."""
1757
1758
objtype = 'class'
1759
member_order = 20
1760
option_spec: ClassVar[OptionSpec] = {
1761
'members': members_option,
1762
'undoc-members': bool_option,
1763
'no-index': bool_option,
1764
'no-index-entry': bool_option,
1765
'inherited-members': inherited_members_option,
1766
'show-inheritance': bool_option,
1767
'member-order': member_order_option,
1768
'exclude-members': exclude_members_option,
1769
'private-members': members_option,
1770
'special-members': members_option,
1771
'class-doc-from': class_doc_from_option,
1772
'noindex': bool_option,
1773
}
1774
1775
# Must be higher than FunctionDocumenter, ClassDocumenter, and
1776
# AttributeDocumenter as NewType can be an attribute and is a class
1777
# after Python 3.10.
1778
priority = 15
1779
1780
_signature_class: Any = None
1781
_signature_method_name: str = ''
1782
1783
def __init__(self, *args: Any) -> None:
1784
super().__init__(*args)
1785
1786
if self.config.autodoc_class_signature == 'separated':
1787
self.options = self.options.copy()
1788
1789
# show __init__() method
1790
if self.options.special_members is None:
1791
self.options['special-members'] = ['__new__', '__init__']
1792
else:
1793
self.options.special_members.append('__new__')
1794
self.options.special_members.append('__init__')
1795
1796
merge_members_option(self.options)
1797
1798
@classmethod
1799
def can_document_member(
1800
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
1801
) -> bool:
1802
return isinstance(member, type) or (
1803
isattr and isinstance(member, NewType | TypeVar)
1804
)
1805
1806
def import_object(self, raiseerror: bool = False) -> bool:
1807
ret = super().import_object(raiseerror)
1808
# if the class is documented under another name, document it
1809
# as data/attribute
1810
if ret:
1811
if hasattr(self.object, '__name__'):
1812
self.doc_as_attr = self.objpath[-1] != self.object.__name__
1813
# -------------------------------------------------------------------
1814
# Issue #27692, #7448: The original goal of this was that if some
1815
# class is aliased, the alias is generated as a link rather than
1816
# duplicated. For example in
1817
#
1818
# class A:
1819
# pass
1820
# B = A
1821
#
1822
# Then B is an alias of A, and should be generated as such.
1823
#
1824
# The way it was solved is to compare the name under which the
1825
# current class is found and the actual name of the class (stored in
1826
# the attribute __name__):
1827
#
1828
# if hasattr(self.object, '__name__'):
1829
# self.doc_as_attr = (self.objpath[-1] != self.object.__name__)
1830
# else:
1831
# self.doc_as_attr = True
1832
#
1833
# Now, to work around a pickling bug of nested class in Python,
1834
# by using the metaclass NestedMetaclass, we change the attribute
1835
# __name__ of the nested class. For example, in
1836
#
1837
# class A(metaclass=NestedMetaclass):
1838
# class B():
1839
# pass
1840
#
1841
# the class B get its name changed to 'A.B'. Such dots '.' in names
1842
# are not supposed to occur in normal python name. I use it to check
1843
# if the class is a nested one and to compare its __name__ with its
1844
# path.
1845
#
1846
# The original implementation as well as the new one here doesn't work
1847
# if a class is aliased from a different place under the same name. For
1848
# example, in the following,
1849
#
1850
# class A:
1851
# pass
1852
# class Container:
1853
# A = A
1854
#
1855
# The nested copy Container.A is also documented. Actually, it seems
1856
# that there is no way to solve this by introspection. I'll submbit
1857
# this problem on sphinx trac.
1858
#
1859
# References: trac #5986, file sage/misc/nested_class.py
1860
import sys
1861
module = getattr(self.object, '__module__', False)
1862
name = getattr(self.object, '__name__', False)
1863
qualname = getattr(self.object, '__qualname__', name)
1864
if qualname and module:
1865
# walk the standard attribute lookup path for this object
1866
qualname_parts = qualname.split('.')
1867
cls = getattr(sys.modules[module], qualname_parts[0], None)
1868
for part in qualname_parts[1:]:
1869
if cls is None:
1870
break
1871
cls = getattr(cls, part, None)
1872
self.doc_as_attr = (self.objpath != qualname_parts and
1873
self.object is cls)
1874
# -------------------------------------------------------------------
1875
else:
1876
self.doc_as_attr = True
1877
if isinstance(self.object, NewType | TypeVar):
1878
modname = getattr(self.object, '__module__', self.modname)
1879
if modname != self.modname and self.modname.startswith(modname):
1880
bases = self.modname[len(modname) :].strip('.').split('.')
1881
self.objpath = bases + self.objpath
1882
self.modname = modname
1883
return ret
1884
1885
def _get_signature(self) -> tuple[Any | None, str | None, Signature | None]:
1886
if isinstance(self.object, NewType | TypeVar):
1887
# Suppress signature
1888
return None, None, None
1889
1890
def get_user_defined_function_or_method(obj: Any, attr: str) -> Any:
1891
"""Get the `attr` function or method from `obj`, if it is user-defined."""
1892
if inspect.is_builtin_class_method(obj, attr):
1893
return None
1894
attr = self.get_attr(obj, attr, None)
1895
if not (inspect.ismethod(attr) or inspect.isfunction(attr)):
1896
return None
1897
return attr
1898
1899
# This sequence is copied from inspect._signature_from_callable.
1900
# ValueError means that no signature could be found, so we keep going.
1901
1902
# First, we check if obj has a __signature__ attribute
1903
if hasattr(self.object, '__signature__'):
1904
object_sig = self.object.__signature__
1905
if isinstance(object_sig, Signature):
1906
return None, None, object_sig
1907
if callable(object_sig) and isinstance(object_sig_str := object_sig(), str):
1908
# Support for enum.Enum.__signature__
1909
return None, None, inspect.signature_from_str(object_sig_str)
1910
1911
# Next, let's see if it has an overloaded __call__ defined
1912
# in its metaclass
1913
call = get_user_defined_function_or_method(type(self.object), '__call__')
1914
1915
if call is not None:
1916
if f'{call.__module__}.{call.__qualname__}' in _METACLASS_CALL_BLACKLIST:
1917
call = None
1918
1919
if call is not None:
1920
self._events.emit('autodoc-before-process-signature', call, True)
1921
try:
1922
sig = inspect.signature(
1923
call,
1924
bound_method=True,
1925
type_aliases=self.config.autodoc_type_aliases,
1926
)
1927
return type(self.object), '__call__', sig
1928
except ValueError:
1929
pass
1930
1931
# Now we check if the 'obj' class has a '__new__' method
1932
new = get_user_defined_function_or_method(self.object, '__new__')
1933
1934
if new is not None:
1935
if f'{new.__module__}.{new.__qualname__}' in _CLASS_NEW_BLACKLIST:
1936
new = None
1937
1938
if new is not None:
1939
self._events.emit('autodoc-before-process-signature', new, True)
1940
try:
1941
sig = inspect.signature(
1942
new,
1943
bound_method=True,
1944
type_aliases=self.config.autodoc_type_aliases,
1945
)
1946
return self.object, '__new__', sig
1947
except ValueError:
1948
pass
1949
1950
# Finally, we should have at least __init__ implemented
1951
init = get_user_defined_function_or_method(self.object, '__init__')
1952
if init is not None:
1953
self._events.emit('autodoc-before-process-signature', init, True)
1954
try:
1955
sig = inspect.signature(
1956
init,
1957
bound_method=True,
1958
type_aliases=self.config.autodoc_type_aliases,
1959
)
1960
return self.object, '__init__', sig
1961
except ValueError:
1962
pass
1963
1964
# None of the attributes are user-defined, so fall back to let inspect
1965
# handle it.
1966
# We don't know the exact method that inspect.signature will read
1967
# the signature from, so just pass the object itself to our hook.
1968
self._events.emit('autodoc-before-process-signature', self.object, False)
1969
try:
1970
sig = inspect.signature(
1971
self.object,
1972
bound_method=False,
1973
type_aliases=self.config.autodoc_type_aliases,
1974
)
1975
return None, None, sig
1976
except ValueError:
1977
pass
1978
1979
# Still no signature: happens e.g. for old-style classes
1980
# with __init__ in C and no `__text_signature__`.
1981
return None, None, None
1982
1983
def format_args(self, **kwargs: Any) -> str:
1984
if self.config.autodoc_typehints in {'none', 'description'}:
1985
kwargs.setdefault('show_annotation', False)
1986
if self.config.autodoc_typehints_format == 'short':
1987
kwargs.setdefault('unqualified_typehints', True)
1988
if self.config.python_display_short_literal_types:
1989
kwargs.setdefault('short_literals', True)
1990
1991
try:
1992
self._signature_class, _signature_method_name, sig = self._get_signature()
1993
except TypeError as exc:
1994
# __signature__ attribute contained junk
1995
logger.warning(
1996
__('Failed to get a constructor signature for %s: %s'),
1997
self.fullname,
1998
exc,
1999
)
2000
return ''
2001
self._signature_method_name = _signature_method_name or ''
2002
2003
if sig is None:
2004
return ''
2005
2006
return stringify_signature(sig, show_return_annotation=False, **kwargs)
2007
2008
def _find_signature(self) -> tuple[str | None, str | None] | None:
2009
result = super()._find_signature()
2010
if result is not None:
2011
# Strip a return value from signature of constructor in docstring (first entry)
2012
result = (result[0], None)
2013
2014
for i, sig in enumerate(self._signatures):
2015
if sig.endswith(' -> None'):
2016
# Strip a return value from signatures of constructor in docstring (subsequent
2017
# entries)
2018
self._signatures[i] = sig[:-8]
2019
2020
return result
2021
2022
def format_signature(self, **kwargs: Any) -> str:
2023
if self.doc_as_attr:
2024
return ''
2025
if self.config.autodoc_class_signature == 'separated':
2026
# do not show signatures
2027
return ''
2028
2029
if self.config.autodoc_typehints_format == 'short':
2030
kwargs.setdefault('unqualified_typehints', True)
2031
if self.config.python_display_short_literal_types:
2032
kwargs.setdefault('short_literals', True)
2033
2034
sig = super().format_signature()
2035
sigs = []
2036
2037
overloads = self.get_overloaded_signatures()
2038
if overloads and self.config.autodoc_typehints != 'none':
2039
# Use signatures for overloaded methods instead of the implementation method.
2040
method = safe_getattr(
2041
self._signature_class, self._signature_method_name, None
2042
)
2043
__globals__ = safe_getattr(method, '__globals__', {})
2044
for overload in overloads:
2045
overload = evaluate_signature(
2046
overload, __globals__, self.config.autodoc_type_aliases
2047
)
2048
2049
parameters = list(overload.parameters.values())
2050
overload = overload.replace(
2051
parameters=parameters[1:], return_annotation=Parameter.empty
2052
)
2053
sig = stringify_signature(overload, **kwargs)
2054
sigs.append(sig)
2055
else:
2056
sigs.append(sig)
2057
2058
return '\n'.join(sigs)
2059
2060
def get_overloaded_signatures(self) -> list[Signature]:
2061
if self._signature_class and self._signature_method_name:
2062
for cls in self._signature_class.__mro__:
2063
try:
2064
analyzer = ModuleAnalyzer.for_module(cls.__module__)
2065
analyzer.analyze()
2066
qualname = f'{cls.__qualname__}.{self._signature_method_name}'
2067
if qualname in analyzer.overloads:
2068
return analyzer.overloads.get(qualname, [])
2069
elif qualname in analyzer.tagorder:
2070
# the constructor is defined in the class, but not overridden.
2071
return []
2072
except PycodeError:
2073
pass
2074
2075
return []
2076
2077
def get_canonical_fullname(self) -> str | None:
2078
__modname__ = safe_getattr(self.object, '__module__', self.modname)
2079
__qualname__ = safe_getattr(self.object, '__qualname__', None)
2080
if __qualname__ is None:
2081
__qualname__ = safe_getattr(self.object, '__name__', None)
2082
if __qualname__ and '<locals>' in __qualname__:
2083
# No valid qualname found if the object is defined as locals
2084
__qualname__ = None
2085
2086
if __modname__ and __qualname__:
2087
return f'{__modname__}.{__qualname__}'
2088
else:
2089
return None
2090
2091
def add_directive_header(self, sig: str) -> None:
2092
sourcename = self.get_sourcename()
2093
2094
if self.doc_as_attr:
2095
self.directivetype = 'attribute'
2096
super().add_directive_header(sig)
2097
2098
if isinstance(self.object, NewType | TypeVar):
2099
return
2100
2101
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
2102
self.add_line(' :final:', sourcename)
2103
2104
canonical_fullname = self.get_canonical_fullname()
2105
if (
2106
not self.doc_as_attr
2107
and not isinstance(self.object, NewType)
2108
and canonical_fullname
2109
and self.fullname != canonical_fullname
2110
):
2111
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)
2112
2113
# add inheritance info, if wanted
2114
if not self.doc_as_attr and self.options.show_inheritance:
2115
if inspect.getorigbases(self.object):
2116
# A subclass of generic types
2117
# refs: PEP-560 <https://peps.python.org/pep-0560/>
2118
bases = list(self.object.__orig_bases__)
2119
elif hasattr(self.object, '__bases__') and len(self.object.__bases__):
2120
# A normal class
2121
bases = list(self.object.__bases__)
2122
else:
2123
bases = []
2124
2125
self._events.emit(
2126
'autodoc-process-bases', self.fullname, self.object, self.options, bases
2127
)
2128
2129
mode = _get_render_mode(self.config.autodoc_typehints_format)
2130
base_classes = [restify(cls, mode=mode) for cls in bases]
2131
2132
sourcename = self.get_sourcename()
2133
self.add_line('', sourcename)
2134
self.add_line(' ' + _('Bases: %s') % ', '.join(base_classes), sourcename)
2135
2136
def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]:
2137
members = get_class_members(
2138
self.object,
2139
self.objpath,
2140
self.get_attr,
2141
self.config.autodoc_inherit_docstrings,
2142
)
2143
if not want_all:
2144
if not self.options.members:
2145
return False, []
2146
# specific members given
2147
selected = []
2148
for name in self.options.members:
2149
if name in members:
2150
selected.append(members[name])
2151
else:
2152
logger.warning(
2153
__('missing attribute %s in object %s'),
2154
name,
2155
self.fullname,
2156
type='autodoc',
2157
)
2158
return False, selected
2159
elif self.options.inherited_members:
2160
return False, list(members.values())
2161
else:
2162
return False, [m for m in members.values() if m.class_ == self.object]
2163
2164
def get_doc(self) -> list[list[str]] | None:
2165
if isinstance(self.object, TypeVar):
2166
if self.object.__doc__ == TypeVar.__doc__:
2167
return []
2168
if self.doc_as_attr:
2169
# Don't show the docstring of the class when it is an alias.
2170
if self.get_variable_comment():
2171
return []
2172
else:
2173
return None
2174
2175
lines = getattr(self, '_new_docstrings', None)
2176
if lines is not None:
2177
return lines
2178
2179
classdoc_from = self.options.get(
2180
'class-doc-from', self.config.autoclass_content
2181
)
2182
2183
docstrings = []
2184
attrdocstring = getdoc(self.object, self.get_attr)
2185
if attrdocstring:
2186
docstrings.append(attrdocstring)
2187
2188
# for classes, what the "docstring" is can be controlled via a
2189
# config value; the default is only the class docstring
2190
if classdoc_from in {'both', 'init'}:
2191
__init__ = self.get_attr(self.object, '__init__', None)
2192
initdocstring = getdoc(
2193
__init__,
2194
self.get_attr,
2195
self.config.autodoc_inherit_docstrings,
2196
self.object,
2197
'__init__',
2198
)
2199
# for new-style classes, no __init__ means default __init__
2200
if initdocstring is not None and (
2201
initdocstring == object.__init__.__doc__ # for pypy
2202
or initdocstring.strip() == object.__init__.__doc__ # for !pypy
2203
):
2204
initdocstring = None
2205
if not initdocstring:
2206
# try __new__
2207
__new__ = self.get_attr(self.object, '__new__', None)
2208
initdocstring = getdoc(
2209
__new__,
2210
self.get_attr,
2211
self.config.autodoc_inherit_docstrings,
2212
self.object,
2213
'__new__',
2214
)
2215
# for new-style classes, no __new__ means default __new__
2216
if initdocstring is not None and (
2217
initdocstring == object.__new__.__doc__ # for pypy
2218
or initdocstring.strip() == object.__new__.__doc__ # for !pypy
2219
):
2220
initdocstring = None
2221
if initdocstring:
2222
if classdoc_from == 'init':
2223
docstrings = [initdocstring]
2224
else:
2225
docstrings.append(initdocstring)
2226
2227
tab_width = self.directive.state.document.settings.tab_width
2228
return [prepare_docstring(docstring, tab_width) for docstring in docstrings]
2229
2230
def get_variable_comment(self) -> list[str] | None:
2231
try:
2232
key = ('', '.'.join(self.objpath))
2233
if self.doc_as_attr:
2234
analyzer = ModuleAnalyzer.for_module(self.modname)
2235
else:
2236
analyzer = ModuleAnalyzer.for_module(self.get_real_modname())
2237
analyzer.analyze()
2238
return list(analyzer.attr_docs.get(key, []))
2239
except PycodeError:
2240
return None
2241
2242
def add_content(self, more_content: StringList | None) -> None:
2243
mode = _get_render_mode(self.config.autodoc_typehints_format)
2244
short_literals = self.config.python_display_short_literal_types
2245
2246
if isinstance(self.object, NewType):
2247
supertype = restify(self.object.__supertype__, mode=mode)
2248
2249
more_content = StringList([_('alias of %s') % supertype, ''], source='')
2250
if isinstance(self.object, TypeVar):
2251
attrs = [repr(self.object.__name__)]
2252
attrs.extend(
2253
stringify_annotation(constraint, mode, short_literals=short_literals)
2254
for constraint in self.object.__constraints__
2255
)
2256
if self.object.__bound__:
2257
bound = restify(self.object.__bound__, mode=mode)
2258
attrs.append(r'bound=\ ' + bound)
2259
if self.object.__covariant__:
2260
attrs.append('covariant=True')
2261
if self.object.__contravariant__:
2262
attrs.append('contravariant=True')
2263
2264
more_content = StringList(
2265
[_('alias of TypeVar(%s)') % ', '.join(attrs), ''], source=''
2266
)
2267
if self.doc_as_attr and self.modname != self.get_real_modname():
2268
try:
2269
# override analyzer to obtain doccomment around its definition.
2270
self.analyzer = ModuleAnalyzer.for_module(self.modname)
2271
self.analyzer.analyze()
2272
except PycodeError:
2273
pass
2274
2275
if self.doc_as_attr and not self.get_variable_comment():
2276
try:
2277
alias = restify(self.object, mode=mode)
2278
more_content = StringList([_('alias of %s') % alias], source='')
2279
except AttributeError:
2280
pass # Invalid class object is passed.
2281
2282
super().add_content(more_content)
2283
2284
def document_members(self, all_members: bool = False) -> None:
2285
if self.doc_as_attr:
2286
return
2287
super().document_members(all_members)
2288
2289
def generate(
2290
self,
2291
more_content: StringList | None = None,
2292
real_modname: str | None = None,
2293
check_module: bool = False,
2294
all_members: bool = False,
2295
) -> None:
2296
# Do not pass real_modname and use the name from the __module__
2297
# attribute of the class.
2298
# If a class gets imported into the module real_modname
2299
# the analyzer won't find the source of the class, if
2300
# it looks in real_modname.
2301
return super().generate(
2302
more_content=more_content,
2303
check_module=check_module,
2304
all_members=all_members,
2305
)
2306
2307
2308
class ExceptionDocumenter(ClassDocumenter):
2309
"""Specialized ClassDocumenter subclass for exceptions."""
2310
2311
objtype = 'exception'
2312
member_order = 10
2313
2314
# needs a higher priority than ClassDocumenter
2315
priority = ClassDocumenter.priority + 5
2316
2317
@classmethod
2318
def can_document_member(
2319
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
2320
) -> bool:
2321
try:
2322
return isinstance(member, type) and issubclass(member, BaseException)
2323
except TypeError as exc:
2324
# It's possible for a member to be considered a type, but fail
2325
# issubclass checks due to not being a class. For example:
2326
# https://github.com/sphinx-doc/sphinx/issues/11654#issuecomment-1696790436
2327
msg = (
2328
f'{cls.__name__} failed to discern if member {member} with'
2329
f' membername {membername} is a BaseException subclass.'
2330
)
2331
raise ValueError(msg) from exc
2332
2333
2334
class DataDocumenterMixinBase:
2335
# define types of instance variables
2336
config: Config
2337
env: BuildEnvironment
2338
modname: str
2339
parent: Any
2340
object: Any
2341
objpath: list[str]
2342
2343
def should_suppress_directive_header(self) -> bool:
2344
"""Check directive header should be suppressed."""
2345
return False
2346
2347
def should_suppress_value_header(self) -> bool:
2348
"""Check :value: header should be suppressed."""
2349
return False
2350
2351
def update_content(self, more_content: StringList) -> None:
2352
"""Update docstring, for example with TypeVar variance."""
2353
pass
2354
2355
2356
class GenericAliasMixin(DataDocumenterMixinBase):
2357
"""Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
2358
supporting GenericAliases.
2359
"""
2360
2361
def should_suppress_directive_header(self) -> bool:
2362
return (
2363
inspect.isgenericalias(self.object)
2364
or super().should_suppress_directive_header()
2365
)
2366
2367
def update_content(self, more_content: StringList) -> None:
2368
if inspect.isgenericalias(self.object):
2369
mode = _get_render_mode(self.config.autodoc_typehints_format)
2370
alias = restify(self.object, mode=mode)
2371
2372
more_content.append(_('alias of %s') % alias, '')
2373
more_content.append('', '')
2374
2375
super().update_content(more_content)
2376
2377
2378
class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
2379
"""Mixin for DataDocumenter to provide the feature for supporting uninitialized
2380
(type annotation only) global variables.
2381
"""
2382
2383
def import_object(self, raiseerror: bool = False) -> bool:
2384
try:
2385
return super().import_object(raiseerror=True) # type: ignore[misc]
2386
except ImportError as exc:
2387
# annotation only instance variable (PEP-526)
2388
try:
2389
with mock(self.config.autodoc_mock_imports):
2390
parent = import_module(self.modname)
2391
annotations = get_type_hints(
2392
parent,
2393
None,
2394
self.config.autodoc_type_aliases,
2395
include_extras=True,
2396
)
2397
if self.objpath[-1] in annotations:
2398
self.object = UNINITIALIZED_ATTR
2399
self.parent = parent
2400
return True
2401
except ImportError:
2402
pass
2403
2404
if raiseerror:
2405
raise
2406
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
2407
self.env.note_reread()
2408
return False
2409
2410
def should_suppress_value_header(self) -> bool:
2411
return (
2412
self.object is UNINITIALIZED_ATTR or super().should_suppress_value_header()
2413
)
2414
2415
def get_doc(self) -> list[list[str]] | None:
2416
if self.object is UNINITIALIZED_ATTR:
2417
return []
2418
else:
2419
return super().get_doc() # type: ignore[misc]
2420
2421
2422
class DataDocumenter(
2423
GenericAliasMixin, UninitializedGlobalVariableMixin, ModuleLevelDocumenter
2424
):
2425
"""Specialized Documenter subclass for data items."""
2426
2427
objtype = 'data'
2428
member_order = 40
2429
priority = -10
2430
option_spec: ClassVar[OptionSpec] = dict(ModuleLevelDocumenter.option_spec)
2431
option_spec['annotation'] = annotation_option
2432
option_spec['no-value'] = bool_option
2433
2434
@classmethod
2435
def can_document_member(
2436
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
2437
) -> bool:
2438
return isinstance(parent, ModuleDocumenter) and isattr
2439
2440
def update_annotations(self, parent: Any) -> None:
2441
"""Update __annotations__ to support type_comment and so on."""
2442
annotations = dict(inspect.getannotations(parent))
2443
parent.__annotations__ = annotations
2444
2445
try:
2446
analyzer = ModuleAnalyzer.for_module(self.modname)
2447
analyzer.analyze()
2448
for (classname, attrname), annotation in analyzer.annotations.items():
2449
if not classname and attrname not in annotations:
2450
annotations[attrname] = annotation
2451
except PycodeError:
2452
pass
2453
2454
def import_object(self, raiseerror: bool = False) -> bool:
2455
ret = super().import_object(raiseerror)
2456
if self.parent:
2457
self.update_annotations(self.parent)
2458
2459
return ret
2460
2461
def should_suppress_value_header(self) -> bool:
2462
if super().should_suppress_value_header():
2463
return True
2464
else:
2465
doc = self.get_doc() or []
2466
docstring, metadata = separate_metadata(
2467
'\n'.join(functools.reduce(operator.iadd, doc, []))
2468
)
2469
if 'hide-value' in metadata:
2470
return True
2471
2472
return False
2473
2474
def add_directive_header(self, sig: str) -> None:
2475
super().add_directive_header(sig)
2476
sourcename = self.get_sourcename()
2477
if (
2478
self.options.annotation is SUPPRESS
2479
or self.should_suppress_directive_header()
2480
):
2481
pass
2482
elif self.options.annotation:
2483
self.add_line(' :annotation: %s' % self.options.annotation, sourcename)
2484
else:
2485
if self.config.autodoc_typehints != 'none':
2486
# obtain annotation for this data
2487
annotations = get_type_hints(
2488
self.parent,
2489
None,
2490
self.config.autodoc_type_aliases,
2491
include_extras=True,
2492
)
2493
if self.objpath[-1] in annotations:
2494
mode = _get_render_mode(self.config.autodoc_typehints_format)
2495
short_literals = self.config.python_display_short_literal_types
2496
objrepr = stringify_annotation(
2497
annotations.get(self.objpath[-1]),
2498
mode,
2499
short_literals=short_literals,
2500
)
2501
self.add_line(' :type: ' + objrepr, sourcename)
2502
2503
try:
2504
if (
2505
self.options.no_value
2506
or self.should_suppress_value_header()
2507
or ismock(self.object)
2508
):
2509
pass
2510
else:
2511
objrepr = object_description(self.object)
2512
self.add_line(' :value: ' + objrepr, sourcename)
2513
except ValueError:
2514
pass
2515
2516
def document_members(self, all_members: bool = False) -> None:
2517
pass
2518
2519
def get_real_modname(self) -> str:
2520
real_modname = self.get_attr(self.parent or self.object, '__module__', None)
2521
return real_modname or self.modname
2522
2523
def get_module_comment(self, attrname: str) -> list[str] | None:
2524
try:
2525
analyzer = ModuleAnalyzer.for_module(self.modname)
2526
analyzer.analyze()
2527
key = ('', attrname)
2528
if key in analyzer.attr_docs:
2529
return list(analyzer.attr_docs[key])
2530
except PycodeError:
2531
pass
2532
2533
return None
2534
2535
def get_doc(self) -> list[list[str]] | None:
2536
# Check the variable has a docstring-comment
2537
comment = self.get_module_comment(self.objpath[-1])
2538
if comment:
2539
return [comment]
2540
else:
2541
return super().get_doc()
2542
2543
def add_content(self, more_content: StringList | None) -> None:
2544
# Disable analyzing variable comment on Documenter.add_content() to control it on
2545
# DataDocumenter.add_content()
2546
self.analyzer = None
2547
2548
if not more_content:
2549
more_content = StringList()
2550
2551
self.update_content(more_content)
2552
super().add_content(more_content)
2553
2554
2555
class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore[misc]
2556
"""Specialized Documenter subclass for methods (normal, static and class)."""
2557
2558
objtype = 'method'
2559
directivetype = 'method'
2560
member_order = 50
2561
priority = 1 # must be more than FunctionDocumenter
2562
2563
@classmethod
2564
def can_document_member(
2565
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
2566
) -> bool:
2567
return inspect.isroutine(member) and not isinstance(parent, ModuleDocumenter)
2568
2569
def import_object(self, raiseerror: bool = False) -> bool:
2570
ret = super().import_object(raiseerror)
2571
if not ret:
2572
return ret
2573
2574
# to distinguish classmethod/staticmethod
2575
obj = self.parent.__dict__.get(self.object_name, self.object)
2576
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
2577
# document static members before regular methods
2578
self.member_order -= 1
2579
elif inspect.isclassmethod(obj):
2580
# document class methods before static methods as
2581
# they usually behave as alternative constructors
2582
self.member_order -= 2
2583
return ret
2584
2585
def format_args(self, **kwargs: Any) -> str:
2586
if self.config.autodoc_typehints in {'none', 'description'}:
2587
kwargs.setdefault('show_annotation', False)
2588
if self.config.autodoc_typehints_format == 'short':
2589
kwargs.setdefault('unqualified_typehints', True)
2590
if self.config.python_display_short_literal_types:
2591
kwargs.setdefault('short_literals', True)
2592
2593
# -----------------------------------------------------------------
2594
# Issue #9976: Support the _sage_argspec_ attribute which makes it
2595
# possible to get argument specification of decorated callables in
2596
# documentation correct. See e.g. sage.misc.decorators.sage_wraps.
2597
#
2598
# Note, however, that sage.misc.sageinspect.sage_getargspec already
2599
# uses a method _sage_argspec_, that only works on objects, not on
2600
# classes, though.
2601
obj = self.object
2602
if hasattr(obj, "_sage_argspec_"):
2603
argspec = obj._sage_argspec_()
2604
elif inspect.isbuiltin(obj) or inspect.ismethoddescriptor(obj):
2605
# can never get arguments of a C function or method unless
2606
# a function to do so is supplied
2607
argspec = sage_getargspec(obj)
2608
else:
2609
# The check above misses ordinary Python methods in Cython
2610
# files.
2611
argspec = sage_getargspec(obj)
2612
2613
if argspec is not None and argspec[0] and argspec[0][0] in ('cls', 'self'):
2614
del argspec[0][0]
2615
if argspec is None:
2616
return None
2617
args = sage_formatargspec(*argspec)
2618
# -----------------------------------------------------------------
2619
2620
if self.config.strip_signature_backslash:
2621
# escape backslashes for reST
2622
args = args.replace('\\', '\\\\')
2623
return args
2624
2625
def add_directive_header(self, sig: str) -> None:
2626
super().add_directive_header(sig)
2627
2628
sourcename = self.get_sourcename()
2629
obj = self.parent.__dict__.get(self.object_name, self.object)
2630
if inspect.isabstractmethod(obj):
2631
self.add_line(' :abstractmethod:', sourcename)
2632
if inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj):
2633
self.add_line(' :async:', sourcename)
2634
if (
2635
inspect.is_classmethod_like(obj)
2636
or inspect.is_singledispatch_method(obj)
2637
and inspect.is_classmethod_like(obj.func)
2638
):
2639
self.add_line(' :classmethod:', sourcename)
2640
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
2641
self.add_line(' :staticmethod:', sourcename)
2642
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
2643
self.add_line(' :final:', sourcename)
2644
2645
def document_members(self, all_members: bool = False) -> None:
2646
pass
2647
2648
# ------------------------------------------------------------------------
2649
# Issue #34730: The format_signature() of the class MethodDocumenter
2650
# supports overloaded methods via inspect.signature(), which does not work
2651
# with Sage yet. Hence the method was removed from here.
2652
# ------------------------------------------------------------------------
2653
2654
def merge_default_value(self, actual: Signature, overload: Signature) -> Signature:
2655
"""Merge default values of actual implementation to the overload variants."""
2656
parameters = list(overload.parameters.values())
2657
for i, param in enumerate(parameters):
2658
actual_param = actual.parameters.get(param.name)
2659
if actual_param and param.default == '...':
2660
parameters[i] = param.replace(default=actual_param.default)
2661
2662
return overload.replace(parameters=parameters)
2663
2664
def annotate_to_first_argument(
2665
self, func: Callable[..., Any], typ: type
2666
) -> Callable[..., Any] | None:
2667
"""Annotate type hint to the first argument of function if needed."""
2668
try:
2669
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
2670
except TypeError as exc:
2671
logger.warning(
2672
__('Failed to get a method signature for %s: %s'), self.fullname, exc
2673
)
2674
return None
2675
except ValueError:
2676
return None
2677
2678
if len(sig.parameters) == 1:
2679
return None
2680
2681
def dummy(): # type: ignore[no-untyped-def] # NoQA: ANN202
2682
pass
2683
2684
params = list(sig.parameters.values())
2685
if params[1].annotation is Parameter.empty:
2686
params[1] = params[1].replace(annotation=typ)
2687
try:
2688
dummy.__signature__ = sig.replace( # type: ignore[attr-defined]
2689
parameters=params
2690
)
2691
return dummy
2692
except (AttributeError, TypeError):
2693
# failed to update signature (ex. built-in or extension types)
2694
return None
2695
2696
return func
2697
2698
def get_doc(self) -> list[list[str]] | None:
2699
if self._new_docstrings is not None:
2700
# docstring already returned previously, then modified by
2701
# `DocstringSignatureMixin`. Just return the previously-computed
2702
# result, so that we don't lose the processing done by
2703
# `DocstringSignatureMixin`.
2704
return self._new_docstrings
2705
if self.objpath[-1] == '__init__':
2706
docstring = getdoc(
2707
self.object,
2708
self.get_attr,
2709
self.config.autodoc_inherit_docstrings,
2710
self.parent,
2711
self.object_name,
2712
)
2713
if docstring is not None and (
2714
docstring == object.__init__.__doc__ # for pypy
2715
or docstring.strip() == object.__init__.__doc__ # for !pypy
2716
):
2717
docstring = None
2718
if docstring:
2719
tab_width = self.directive.state.document.settings.tab_width
2720
return [prepare_docstring(docstring, tabsize=tab_width)]
2721
else:
2722
return []
2723
elif self.objpath[-1] == '__new__':
2724
docstring = getdoc(
2725
self.object,
2726
self.get_attr,
2727
self.config.autodoc_inherit_docstrings,
2728
self.parent,
2729
self.object_name,
2730
)
2731
if docstring is not None and (
2732
docstring == object.__new__.__doc__ # for pypy
2733
or docstring.strip() == object.__new__.__doc__ # for !pypy
2734
):
2735
docstring = None
2736
if docstring:
2737
tab_width = self.directive.state.document.settings.tab_width
2738
return [prepare_docstring(docstring, tabsize=tab_width)]
2739
else:
2740
return []
2741
else:
2742
return super().get_doc()
2743
2744
2745
class NonDataDescriptorMixin(DataDocumenterMixinBase):
2746
"""Mixin for AttributeDocumenter to provide the feature for supporting non
2747
data-descriptors.
2748
2749
.. note:: This mix-in must be inherited after other mix-ins. Otherwise, docstring
2750
and :value: header will be suppressed unexpectedly.
2751
"""
2752
2753
def import_object(self, raiseerror: bool = False) -> bool:
2754
ret = super().import_object(raiseerror) # type: ignore[misc]
2755
if ret and not inspect.isattributedescriptor(self.object):
2756
self.non_data_descriptor = True
2757
else:
2758
self.non_data_descriptor = False
2759
2760
return ret
2761
2762
def should_suppress_value_header(self) -> bool:
2763
return (
2764
not getattr(self, 'non_data_descriptor', False)
2765
or super().should_suppress_directive_header()
2766
)
2767
2768
def get_doc(self) -> list[list[str]] | None:
2769
if getattr(self, 'non_data_descriptor', False):
2770
# the docstring of non datadescriptor is very probably the wrong thing
2771
# to display
2772
return None
2773
else:
2774
return super().get_doc() # type: ignore[misc]
2775
2776
2777
class SlotsMixin(DataDocumenterMixinBase):
2778
"""Mixin for AttributeDocumenter to provide the feature for supporting __slots__."""
2779
2780
def isslotsattribute(self) -> bool:
2781
"""Check the subject is an attribute in __slots__."""
2782
try:
2783
if parent___slots__ := inspect.getslots(self.parent):
2784
return self.objpath[-1] in parent___slots__
2785
else:
2786
return False
2787
except (ValueError, TypeError):
2788
return False
2789
2790
def import_object(self, raiseerror: bool = False) -> bool:
2791
ret = super().import_object(raiseerror) # type: ignore[misc]
2792
if self.isslotsattribute():
2793
self.object = SLOTSATTR
2794
2795
return ret
2796
2797
def should_suppress_value_header(self) -> bool:
2798
if self.object is SLOTSATTR:
2799
return True
2800
else:
2801
return super().should_suppress_value_header()
2802
2803
def get_doc(self) -> list[list[str]] | None:
2804
if self.object is SLOTSATTR:
2805
try:
2806
parent___slots__ = inspect.getslots(self.parent)
2807
if parent___slots__ and (
2808
docstring := parent___slots__.get(self.objpath[-1])
2809
):
2810
docstring = prepare_docstring(docstring)
2811
return [docstring]
2812
else:
2813
return []
2814
except ValueError as exc:
2815
logger.warning(
2816
__('Invalid __slots__ found on %s. Ignored.'),
2817
(self.parent.__qualname__, exc),
2818
type='autodoc',
2819
)
2820
return []
2821
else:
2822
return super().get_doc() # type: ignore[misc]
2823
2824
2825
class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
2826
"""Mixin for AttributeDocumenter to provide the feature for supporting runtime
2827
instance attributes (that are defined in __init__() methods with doc-comments).
2828
2829
Example::
2830
2831
class Foo:
2832
def __init__(self):
2833
self.attr = None #: This is a target of this mix-in.
2834
"""
2835
2836
RUNTIME_INSTANCE_ATTRIBUTE = object()
2837
2838
def is_runtime_instance_attribute(self, parent: Any) -> bool:
2839
"""Check the subject is an attribute defined in __init__()."""
2840
# An instance variable defined in __init__().
2841
if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore[attr-defined]
2842
return True
2843
return self.is_runtime_instance_attribute_not_commented(parent)
2844
2845
def is_runtime_instance_attribute_not_commented(self, parent: Any) -> bool:
2846
"""Check the subject is an attribute defined in __init__() without comment."""
2847
for cls in inspect.getmro(parent):
2848
try:
2849
module = safe_getattr(cls, '__module__')
2850
qualname = safe_getattr(cls, '__qualname__')
2851
2852
analyzer = ModuleAnalyzer.for_module(module)
2853
analyzer.analyze()
2854
if qualname and self.objpath:
2855
key = f'{qualname}.{self.objpath[-1]}'
2856
if key in analyzer.tagorder:
2857
return True
2858
except (AttributeError, PycodeError):
2859
pass
2860
2861
return False
2862
2863
def import_object(self, raiseerror: bool = False) -> bool:
2864
"""Check the existence of runtime instance attribute after failing to import the
2865
attribute.
2866
"""
2867
try:
2868
return super().import_object(raiseerror=True) # type: ignore[misc]
2869
except ImportError as exc:
2870
try:
2871
with mock(self.config.autodoc_mock_imports):
2872
ret = import_object(
2873
self.modname,
2874
self.objpath[:-1],
2875
'class',
2876
attrgetter=self.get_attr, # type: ignore[attr-defined]
2877
)
2878
parent = ret[3]
2879
if self.is_runtime_instance_attribute(parent):
2880
self.object = self.RUNTIME_INSTANCE_ATTRIBUTE
2881
self.parent = parent
2882
return True
2883
except ImportError:
2884
pass
2885
2886
if raiseerror:
2887
raise
2888
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
2889
self.env.note_reread()
2890
return False
2891
2892
def should_suppress_value_header(self) -> bool:
2893
return (
2894
self.object is self.RUNTIME_INSTANCE_ATTRIBUTE
2895
or super().should_suppress_value_header()
2896
)
2897
2898
def get_doc(self) -> list[list[str]] | None:
2899
if (
2900
self.object is self.RUNTIME_INSTANCE_ATTRIBUTE
2901
and self.is_runtime_instance_attribute_not_commented(self.parent)
2902
):
2903
return None
2904
else:
2905
return super().get_doc() # type: ignore[misc]
2906
2907
2908
class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
2909
"""Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
2910
instance attributes (PEP-526 styled, annotation only attributes).
2911
2912
Example::
2913
2914
class Foo:
2915
attr: int #: This is a target of this mix-in.
2916
"""
2917
2918
def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
2919
"""Check the subject is an annotation only attribute."""
2920
annotations = get_type_hints(
2921
parent, None, self.config.autodoc_type_aliases, include_extras=True
2922
)
2923
return self.objpath[-1] in annotations
2924
2925
def import_object(self, raiseerror: bool = False) -> bool:
2926
"""Check the existence of uninitialized instance attribute when failed to import
2927
the attribute.
2928
"""
2929
try:
2930
return super().import_object(raiseerror=True) # type: ignore[misc]
2931
except ImportError as exc:
2932
try:
2933
ret = import_object(
2934
self.modname,
2935
self.objpath[:-1],
2936
'class',
2937
attrgetter=self.get_attr, # type: ignore[attr-defined]
2938
)
2939
parent = ret[3]
2940
if self.is_uninitialized_instance_attribute(parent):
2941
self.object = UNINITIALIZED_ATTR
2942
self.parent = parent
2943
return True
2944
except ImportError:
2945
pass
2946
2947
if raiseerror:
2948
raise
2949
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
2950
self.env.note_reread()
2951
return False
2952
2953
def should_suppress_value_header(self) -> bool:
2954
return (
2955
self.object is UNINITIALIZED_ATTR or super().should_suppress_value_header()
2956
)
2957
2958
def get_doc(self) -> list[list[str]] | None:
2959
if self.object is UNINITIALIZED_ATTR:
2960
return None
2961
return super().get_doc() # type: ignore[misc]
2962
2963
2964
class AttributeDocumenter( # type: ignore[misc]
2965
GenericAliasMixin,
2966
SlotsMixin,
2967
RuntimeInstanceAttributeMixin,
2968
UninitializedInstanceAttributeMixin,
2969
NonDataDescriptorMixin,
2970
DocstringStripSignatureMixin,
2971
ClassLevelDocumenter,
2972
):
2973
"""Specialized Documenter subclass for attributes."""
2974
2975
objtype = 'attribute'
2976
member_order = 60
2977
option_spec: ClassVar[OptionSpec] = dict(ModuleLevelDocumenter.option_spec)
2978
option_spec['annotation'] = annotation_option
2979
option_spec['no-value'] = bool_option
2980
2981
# must be higher than the MethodDocumenter, else it will recognize
2982
# some non-data descriptors as methods
2983
priority = 10
2984
2985
@staticmethod
2986
def is_function_or_method(obj: Any) -> bool:
2987
return (
2988
inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj)
2989
)
2990
2991
@classmethod
2992
def can_document_member(
2993
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
2994
) -> bool:
2995
if isinstance(parent, ModuleDocumenter):
2996
return False
2997
# ---------------------------------------------------------------------
2998
# Issue #34730: Do not pass objects of the class CachedMethodCaller as
2999
# attributes.
3000
#
3001
# sage: from sphinx.util import inspect
3002
# sage: A = AlgebrasWithBasis(QQ).example()
3003
# sage: member = A.one_basis
3004
# sage: type(member)
3005
# <class 'sage.misc.cachefunc.CachedMethodCallerNoArgs'>
3006
# sage: inspect.isattributedescriptor(member)
3007
# True
3008
# sage: inspect.isroutine(member)
3009
# True
3010
#
3011
if inspect.isattributedescriptor(member) and not inspect.isroutine(member):
3012
return True
3013
# Issue #26522: Pass objects of classes that inherit ClasscallMetaclass
3014
# as attributes rather than method descriptors.
3015
from sage.misc.classcall_metaclass import ClasscallMetaclass
3016
if isinstance(type(member), ClasscallMetaclass):
3017
return True
3018
# ---------------------------------------------------------------------
3019
return not inspect.isroutine(member) and not isinstance(member, type)
3020
3021
def document_members(self, all_members: bool = False) -> None:
3022
pass
3023
3024
def update_annotations(self, parent: Any) -> None:
3025
"""Update __annotations__ to support type_comment and so on."""
3026
try:
3027
annotations = dict(inspect.getannotations(parent))
3028
parent.__annotations__ = annotations
3029
3030
for cls in inspect.getmro(parent):
3031
try:
3032
module = safe_getattr(cls, '__module__')
3033
qualname = safe_getattr(cls, '__qualname__')
3034
3035
analyzer = ModuleAnalyzer.for_module(module)
3036
analyzer.analyze()
3037
anns = analyzer.annotations
3038
for (classname, attrname), annotation in anns.items():
3039
if classname == qualname and attrname not in annotations:
3040
annotations[attrname] = annotation
3041
except (AttributeError, PycodeError):
3042
pass
3043
except (AttributeError, TypeError):
3044
# Failed to set __annotations__ (built-in, extensions, etc.)
3045
pass
3046
3047
def import_object(self, raiseerror: bool = False) -> bool:
3048
ret = super().import_object(raiseerror)
3049
if inspect.isenumattribute(self.object):
3050
self.object = self.object.value
3051
if self.parent:
3052
self.update_annotations(self.parent)
3053
3054
return ret
3055
3056
def get_real_modname(self) -> str:
3057
real_modname = self.get_attr(self.parent or self.object, '__module__', None)
3058
return real_modname or self.modname
3059
3060
def should_suppress_value_header(self) -> bool:
3061
if super().should_suppress_value_header():
3062
return True
3063
else:
3064
doc = self.get_doc()
3065
if doc:
3066
docstring, metadata = separate_metadata(
3067
'\n'.join(functools.reduce(operator.iadd, doc, []))
3068
)
3069
if 'hide-value' in metadata:
3070
return True
3071
3072
return False
3073
3074
def add_directive_header(self, sig: str) -> None:
3075
super().add_directive_header(sig)
3076
sourcename = self.get_sourcename()
3077
if (
3078
self.options.annotation is SUPPRESS
3079
or self.should_suppress_directive_header()
3080
):
3081
pass
3082
elif self.options.annotation:
3083
self.add_line(' :annotation: %s' % self.options.annotation, sourcename)
3084
else:
3085
if self.config.autodoc_typehints != 'none':
3086
# obtain type annotation for this attribute
3087
annotations = get_type_hints(
3088
self.parent,
3089
None,
3090
self.config.autodoc_type_aliases,
3091
include_extras=True,
3092
)
3093
if self.objpath[-1] in annotations:
3094
mode = _get_render_mode(self.config.autodoc_typehints_format)
3095
short_literals = self.config.python_display_short_literal_types
3096
objrepr = stringify_annotation(
3097
annotations.get(self.objpath[-1]),
3098
mode,
3099
short_literals=short_literals,
3100
)
3101
self.add_line(' :type: ' + objrepr, sourcename)
3102
3103
try:
3104
if (
3105
self.options.no_value
3106
or self.should_suppress_value_header()
3107
or ismock(self.object)
3108
):
3109
pass
3110
else:
3111
objrepr = object_description(self.object)
3112
self.add_line(' :value: ' + objrepr, sourcename)
3113
except ValueError:
3114
pass
3115
3116
def get_attribute_comment(self, parent: Any, attrname: str) -> list[str] | None:
3117
for cls in inspect.getmro(parent):
3118
try:
3119
module = safe_getattr(cls, '__module__')
3120
qualname = safe_getattr(cls, '__qualname__')
3121
3122
analyzer = ModuleAnalyzer.for_module(module)
3123
analyzer.analyze()
3124
if qualname and self.objpath:
3125
key = (qualname, attrname)
3126
if key in analyzer.attr_docs:
3127
return list(analyzer.attr_docs[key])
3128
except (AttributeError, PycodeError):
3129
pass
3130
3131
return None
3132
3133
def get_doc(self) -> list[list[str]] | None:
3134
# Check the attribute has a docstring-comment
3135
comment = self.get_attribute_comment(self.parent, self.objpath[-1])
3136
if comment:
3137
return [comment]
3138
3139
try:
3140
# Disable `autodoc_inherit_docstring` temporarily to avoid to obtain
3141
# a docstring from the value which descriptor returns unexpectedly.
3142
# See: https://github.com/sphinx-doc/sphinx/issues/7805
3143
orig = self.config.autodoc_inherit_docstrings
3144
self.config.autodoc_inherit_docstrings = False
3145
return super().get_doc()
3146
finally:
3147
self.config.autodoc_inherit_docstrings = orig
3148
3149
def add_content(self, more_content: StringList | None) -> None:
3150
# Disable analyzing attribute comment on Documenter.add_content() to control it on
3151
# AttributeDocumenter.add_content()
3152
self.analyzer = None
3153
3154
if more_content is None:
3155
more_content = StringList()
3156
self.update_content(more_content)
3157
super().add_content(more_content)
3158
3159
3160
class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore[misc]
3161
"""Specialized Documenter subclass for properties."""
3162
3163
objtype = 'property'
3164
member_order = 60
3165
3166
# before AttributeDocumenter
3167
priority = AttributeDocumenter.priority + 1
3168
3169
@classmethod
3170
def can_document_member(
3171
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
3172
) -> bool:
3173
if isinstance(parent, ClassDocumenter):
3174
if inspect.isproperty(member):
3175
return True
3176
else:
3177
__dict__ = safe_getattr(parent.object, '__dict__', {})
3178
obj = __dict__.get(membername)
3179
return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__)
3180
else:
3181
return False
3182
3183
def import_object(self, raiseerror: bool = False) -> bool:
3184
"""Check the existence of uninitialized instance attribute when failed to import
3185
the attribute.
3186
"""
3187
ret = super().import_object(raiseerror)
3188
if ret and not inspect.isproperty(self.object):
3189
__dict__ = safe_getattr(self.parent, '__dict__', {})
3190
obj = __dict__.get(self.objpath[-1])
3191
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
3192
self.object = obj.__func__
3193
self.isclassmethod: bool = True
3194
return True
3195
else:
3196
return False
3197
3198
self.isclassmethod = False
3199
return ret
3200
3201
def format_args(self, **kwargs: Any) -> str:
3202
func = self._get_property_getter()
3203
if func is None:
3204
return ''
3205
3206
# update the annotations of the property getter
3207
self._events.emit('autodoc-before-process-signature', func, False)
3208
# correctly format the arguments for a property
3209
return super().format_args(**kwargs)
3210
3211
def document_members(self, all_members: bool = False) -> None:
3212
pass
3213
3214
def get_real_modname(self) -> str:
3215
real_modname = self.get_attr(self.parent or self.object, '__module__', None)
3216
return real_modname or self.modname
3217
3218
def add_directive_header(self, sig: str) -> None:
3219
super().add_directive_header(sig)
3220
sourcename = self.get_sourcename()
3221
if inspect.isabstractmethod(self.object):
3222
self.add_line(' :abstractmethod:', sourcename)
3223
if self.isclassmethod:
3224
self.add_line(' :classmethod:', sourcename)
3225
3226
func = self._get_property_getter()
3227
if func is None or self.config.autodoc_typehints == 'none':
3228
return
3229
3230
try:
3231
signature = inspect.signature(
3232
func, type_aliases=self.config.autodoc_type_aliases
3233
)
3234
if signature.return_annotation is not Parameter.empty:
3235
mode = _get_render_mode(self.config.autodoc_typehints_format)
3236
short_literals = self.config.python_display_short_literal_types
3237
objrepr = stringify_annotation(
3238
signature.return_annotation, mode, short_literals=short_literals
3239
)
3240
self.add_line(' :type: ' + objrepr, sourcename)
3241
except TypeError as exc:
3242
logger.warning(
3243
__('Failed to get a function signature for %s: %s'), self.fullname, exc
3244
)
3245
pass
3246
except ValueError:
3247
pass
3248
3249
def _get_property_getter(self) -> Callable[..., Any] | None:
3250
if safe_getattr(self.object, 'fget', None): # property
3251
return self.object.fget
3252
if safe_getattr(self.object, 'func', None): # cached_property
3253
return self.object.func
3254
return None
3255
3256
3257
def autodoc_attrgetter(
3258
obj: Any, name: str, *defargs: Any, registry: SphinxComponentRegistry
3259
) -> Any:
3260
"""Alternative getattr() for types"""
3261
for typ, func in registry.autodoc_attrgetters.items():
3262
if isinstance(obj, typ):
3263
return func(obj, name, *defargs)
3264
3265
return safe_getattr(obj, name, *defargs)
3266
3267
3268
def setup(app: Sphinx) -> ExtensionMetadata:
3269
app.add_autodocumenter(ModuleDocumenter)
3270
app.add_autodocumenter(ClassDocumenter)
3271
app.add_autodocumenter(ExceptionDocumenter)
3272
app.add_autodocumenter(DataDocumenter)
3273
app.add_autodocumenter(FunctionDocumenter)
3274
app.add_autodocumenter(DecoratorDocumenter)
3275
app.add_autodocumenter(MethodDocumenter)
3276
app.add_autodocumenter(AttributeDocumenter)
3277
app.add_autodocumenter(PropertyDocumenter)
3278
3279
app.add_config_value(
3280
'autoclass_content',
3281
'class',
3282
'env',
3283
types=ENUM('both', 'class', 'init'),
3284
)
3285
app.add_config_value(
3286
'autodoc_member_order',
3287
'alphabetical',
3288
'env',
3289
types=ENUM('alphabetical', 'bysource', 'groupwise'),
3290
)
3291
app.add_config_value(
3292
'autodoc_class_signature',
3293
'mixed',
3294
'env',
3295
types=ENUM('mixed', 'separated'),
3296
)
3297
app.add_config_value('autodoc_default_options', {}, 'env', types=frozenset({dict}))
3298
app.add_config_value(
3299
'autodoc_docstring_signature', True, 'env', types=frozenset({bool})
3300
)
3301
app.add_config_value(
3302
'autodoc_mock_imports', [], 'env', types=frozenset({list, tuple})
3303
)
3304
app.add_config_value(
3305
'autodoc_typehints',
3306
'signature',
3307
'env',
3308
types=ENUM('signature', 'description', 'none', 'both'),
3309
)
3310
app.add_config_value(
3311
'autodoc_typehints_description_target',
3312
'all',
3313
'env',
3314
types=ENUM('all', 'documented', 'documented_params'),
3315
)
3316
app.add_config_value('autodoc_type_aliases', {}, 'env', types=frozenset({dict}))
3317
app.add_config_value(
3318
'autodoc_typehints_format',
3319
'short',
3320
'env',
3321
types=ENUM('fully-qualified', 'short'),
3322
)
3323
app.add_config_value('autodoc_warningiserror', True, 'env', types=frozenset({bool}))
3324
app.add_config_value(
3325
'autodoc_inherit_docstrings', True, 'env', types=frozenset({bool})
3326
)
3327
app.add_event('autodoc-before-process-signature')
3328
app.add_event('autodoc-process-docstring')
3329
app.add_event('autodoc-process-signature')
3330
app.add_event('autodoc-skip-member')
3331
app.add_event('autodoc-process-bases')
3332
3333
app.setup_extension('sphinx.ext.autodoc.preserve_defaults')
3334
app.setup_extension('sphinx.ext.autodoc.type_comment')
3335
app.setup_extension('sphinx.ext.autodoc.typehints')
3336
3337
return {
3338
'version': sphinx.__display_version__,
3339
'parallel_read_safe': True,
3340
}
3341
3342