1"""
2    sphinx.domains.python
3    ~~~~~~~~~~~~~~~~~~~~~
4
5    The Python domain.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import builtins
12import inspect
13import re
14import sys
15import typing
16import warnings
17from inspect import Parameter
18from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple, cast
19
20from docutils import nodes
21from docutils.nodes import Element, Node
22from docutils.parsers.rst import directives
23
24from sphinx import addnodes
25from sphinx.addnodes import desc_signature, pending_xref
26from sphinx.application import Sphinx
27from sphinx.builders import Builder
28from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
29from sphinx.directives import ObjectDescription
30from sphinx.domains import Domain, Index, IndexEntry, ObjType
31from sphinx.environment import BuildEnvironment
32from sphinx.locale import _, __
33from sphinx.pycode.ast import ast
34from sphinx.pycode.ast import parse as ast_parse
35from sphinx.roles import XRefRole
36from sphinx.util import logging
37from sphinx.util.docfields import Field, GroupedField, TypedField
38from sphinx.util.docutils import SphinxDirective
39from sphinx.util.inspect import signature_from_str
40from sphinx.util.nodes import make_id, make_refnode
41from sphinx.util.typing import TextlikeNode
42
43if False:
44    # For type annotation
45    from typing import Type  # for python3.5.1
46
47
48logger = logging.getLogger(__name__)
49
50
51# REs for Python signatures
52py_sig_re = re.compile(
53    r'''^ ([\w.]*\.)?            # class name(s)
54          (\w+)  \s*             # thing name
55          (?: \(\s*(.*)\s*\)     # optional: arguments
56           (?:\s* -> \s* (.*))?  #           return annotation
57          )? $                   # and nothing more
58          ''', re.VERBOSE)
59
60
61pairindextypes = {
62    'module':    _('module'),
63    'keyword':   _('keyword'),
64    'operator':  _('operator'),
65    'object':    _('object'),
66    'exception': _('exception'),
67    'statement': _('statement'),
68    'builtin':   _('built-in function'),
69}
70
71ObjectEntry = NamedTuple('ObjectEntry', [('docname', str),
72                                         ('node_id', str),
73                                         ('objtype', str)])
74ModuleEntry = NamedTuple('ModuleEntry', [('docname', str),
75                                         ('node_id', str),
76                                         ('synopsis', str),
77                                         ('platform', str),
78                                         ('deprecated', bool)])
79
80
81def type_to_xref(text: str, env: BuildEnvironment = None) -> addnodes.pending_xref:
82    """Convert a type string to a cross reference node."""
83    if text == 'None':
84        reftype = 'obj'
85    else:
86        reftype = 'class'
87
88    if env:
89        kwargs = {'py:module': env.ref_context.get('py:module'),
90                  'py:class': env.ref_context.get('py:class')}
91    else:
92        kwargs = {}
93
94    return pending_xref('', nodes.Text(text),
95                        refdomain='py', reftype=reftype, reftarget=text, **kwargs)
96
97
98def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Node]:
99    """Parse type annotation."""
100    def unparse(node: ast.AST) -> List[Node]:
101        if isinstance(node, ast.Attribute):
102            return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))]
103        elif isinstance(node, ast.BinOp):
104            result = unparse(node.left)  # type: List[Node]
105            result.extend(unparse(node.op))
106            result.extend(unparse(node.right))
107            return result
108        elif isinstance(node, ast.BitOr):
109            return [nodes.Text(' '), addnodes.desc_sig_punctuation('', '|'), nodes.Text(' ')]
110        elif isinstance(node, ast.Expr):
111            return unparse(node.value)
112        elif isinstance(node, ast.Index):
113            return unparse(node.value)
114        elif isinstance(node, ast.List):
115            result = [addnodes.desc_sig_punctuation('', '[')]
116            for elem in node.elts:
117                result.extend(unparse(elem))
118                result.append(addnodes.desc_sig_punctuation('', ', '))
119            result.pop()
120            result.append(addnodes.desc_sig_punctuation('', ']'))
121            return result
122        elif isinstance(node, ast.Module):
123            return sum((unparse(e) for e in node.body), [])
124        elif isinstance(node, ast.Name):
125            return [nodes.Text(node.id)]
126        elif isinstance(node, ast.Subscript):
127            result = unparse(node.value)
128            result.append(addnodes.desc_sig_punctuation('', '['))
129            result.extend(unparse(node.slice))
130            result.append(addnodes.desc_sig_punctuation('', ']'))
131            return result
132        elif isinstance(node, ast.Tuple):
133            if node.elts:
134                result = []
135                for elem in node.elts:
136                    result.extend(unparse(elem))
137                    result.append(addnodes.desc_sig_punctuation('', ', '))
138                result.pop()
139            else:
140                result = [addnodes.desc_sig_punctuation('', '('),
141                          addnodes.desc_sig_punctuation('', ')')]
142
143            return result
144        else:
145            if sys.version_info >= (3, 6):
146                if isinstance(node, ast.Constant):
147                    if node.value is Ellipsis:
148                        return [addnodes.desc_sig_punctuation('', "...")]
149                    else:
150                        return [nodes.Text(node.value)]
151
152            if sys.version_info < (3, 8):
153                if isinstance(node, ast.Ellipsis):
154                    return [addnodes.desc_sig_punctuation('', "...")]
155                elif isinstance(node, ast.NameConstant):
156                    return [nodes.Text(node.value)]
157
158            raise SyntaxError  # unsupported syntax
159
160    if env is None:
161        warnings.warn("The env parameter for _parse_annotation becomes required now.",
162                      RemovedInSphinx50Warning, stacklevel=2)
163
164    try:
165        tree = ast_parse(annotation)
166        result = unparse(tree)
167        for i, node in enumerate(result):
168            if isinstance(node, nodes.Text) and node.strip():
169                result[i] = type_to_xref(str(node), env)
170        return result
171    except SyntaxError:
172        return [type_to_xref(annotation, env)]
173
174
175def _parse_arglist(arglist: str, env: BuildEnvironment = None) -> addnodes.desc_parameterlist:
176    """Parse a list of arguments using AST parser"""
177    params = addnodes.desc_parameterlist(arglist)
178    sig = signature_from_str('(%s)' % arglist)
179    last_kind = None
180    for param in sig.parameters.values():
181        if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
182            # PEP-570: Separator for Positional Only Parameter: /
183            params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/'))
184        if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
185                                                              param.POSITIONAL_ONLY,
186                                                              None):
187            # PEP-3102: Separator for Keyword Only Parameter: *
188            params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '*'))
189
190        node = addnodes.desc_parameter()
191        if param.kind == param.VAR_POSITIONAL:
192            node += addnodes.desc_sig_operator('', '*')
193            node += addnodes.desc_sig_name('', param.name)
194        elif param.kind == param.VAR_KEYWORD:
195            node += addnodes.desc_sig_operator('', '**')
196            node += addnodes.desc_sig_name('', param.name)
197        else:
198            node += addnodes.desc_sig_name('', param.name)
199
200        if param.annotation is not param.empty:
201            children = _parse_annotation(param.annotation, env)
202            node += addnodes.desc_sig_punctuation('', ':')
203            node += nodes.Text(' ')
204            node += addnodes.desc_sig_name('', '', *children)  # type: ignore
205        if param.default is not param.empty:
206            if param.annotation is not param.empty:
207                node += nodes.Text(' ')
208                node += addnodes.desc_sig_operator('', '=')
209                node += nodes.Text(' ')
210            else:
211                node += addnodes.desc_sig_operator('', '=')
212            node += nodes.inline('', param.default, classes=['default_value'],
213                                 support_smartquotes=False)
214
215        params += node
216        last_kind = param.kind
217
218    if last_kind == Parameter.POSITIONAL_ONLY:
219        # PEP-570: Separator for Positional Only Parameter: /
220        params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/'))
221
222    return params
223
224
225def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
226    """"Parse" a list of arguments separated by commas.
227
228    Arguments can have "optional" annotations given by enclosing them in
229    brackets.  Currently, this will split at any comma, even if it's inside a
230    string literal (e.g. default argument value).
231    """
232    paramlist = addnodes.desc_parameterlist()
233    stack = [paramlist]  # type: List[Element]
234    try:
235        for argument in arglist.split(','):
236            argument = argument.strip()
237            ends_open = ends_close = 0
238            while argument.startswith('['):
239                stack.append(addnodes.desc_optional())
240                stack[-2] += stack[-1]
241                argument = argument[1:].strip()
242            while argument.startswith(']'):
243                stack.pop()
244                argument = argument[1:].strip()
245            while argument.endswith(']') and not argument.endswith('[]'):
246                ends_close += 1
247                argument = argument[:-1].strip()
248            while argument.endswith('['):
249                ends_open += 1
250                argument = argument[:-1].strip()
251            if argument:
252                stack[-1] += addnodes.desc_parameter(argument, argument)
253            while ends_open:
254                stack.append(addnodes.desc_optional())
255                stack[-2] += stack[-1]
256                ends_open -= 1
257            while ends_close:
258                stack.pop()
259                ends_close -= 1
260        if len(stack) != 1:
261            raise IndexError
262    except IndexError:
263        # if there are too few or too many elements on the stack, just give up
264        # and treat the whole argument list as one argument, discarding the
265        # already partially populated paramlist node
266        paramlist = addnodes.desc_parameterlist()
267        paramlist += addnodes.desc_parameter(arglist, arglist)
268        signode += paramlist
269    else:
270        signode += paramlist
271
272
273# This override allows our inline type specifiers to behave like :class: link
274# when it comes to handling "." and "~" prefixes.
275class PyXrefMixin:
276    def make_xref(self, rolename: str, domain: str, target: str,
277                  innernode: "Type[TextlikeNode]" = nodes.emphasis,
278                  contnode: Node = None, env: BuildEnvironment = None) -> Node:
279        result = super().make_xref(rolename, domain, target,  # type: ignore
280                                   innernode, contnode, env)
281        result['refspecific'] = True
282        result['py:module'] = env.ref_context.get('py:module')
283        result['py:class'] = env.ref_context.get('py:class')
284        if target.startswith(('.', '~')):
285            prefix, result['reftarget'] = target[0], target[1:]
286            if prefix == '.':
287                text = target[1:]
288            elif prefix == '~':
289                text = target.split('.')[-1]
290            for node in result.traverse(nodes.Text):
291                node.parent[node.parent.index(node)] = nodes.Text(text)
292                break
293        return result
294
295    def make_xrefs(self, rolename: str, domain: str, target: str,
296                   innernode: "Type[TextlikeNode]" = nodes.emphasis,
297                   contnode: Node = None, env: BuildEnvironment = None) -> List[Node]:
298        delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+)'
299        delims_re = re.compile(delims)
300        sub_targets = re.split(delims, target)
301
302        split_contnode = bool(contnode and contnode.astext() == target)
303
304        results = []
305        for sub_target in filter(None, sub_targets):
306            if split_contnode:
307                contnode = nodes.Text(sub_target)
308
309            if delims_re.match(sub_target):
310                results.append(contnode or innernode(sub_target, sub_target))
311            else:
312                results.append(self.make_xref(rolename, domain, sub_target,
313                                              innernode, contnode, env))
314
315        return results
316
317
318class PyField(PyXrefMixin, Field):
319    def make_xref(self, rolename: str, domain: str, target: str,
320                  innernode: "Type[TextlikeNode]" = nodes.emphasis,
321                  contnode: Node = None, env: BuildEnvironment = None) -> Node:
322        if rolename == 'class' and target == 'None':
323            # None is not a type, so use obj role instead.
324            rolename = 'obj'
325
326        return super().make_xref(rolename, domain, target, innernode, contnode, env)
327
328
329class PyGroupedField(PyXrefMixin, GroupedField):
330    pass
331
332
333class PyTypedField(PyXrefMixin, TypedField):
334    def make_xref(self, rolename: str, domain: str, target: str,
335                  innernode: "Type[TextlikeNode]" = nodes.emphasis,
336                  contnode: Node = None, env: BuildEnvironment = None) -> Node:
337        if rolename == 'class' and target == 'None':
338            # None is not a type, so use obj role instead.
339            rolename = 'obj'
340
341        return super().make_xref(rolename, domain, target, innernode, contnode, env)
342
343
344class PyObject(ObjectDescription[Tuple[str, str]]):
345    """
346    Description of a general Python object.
347
348    :cvar allow_nesting: Class is an object that allows for nested namespaces
349    :vartype allow_nesting: bool
350    """
351    option_spec = {
352        'noindex': directives.flag,
353        'noindexentry': directives.flag,
354        'module': directives.unchanged,
355        'annotation': directives.unchanged,
356    }
357
358    doc_field_types = [
359        PyTypedField('parameter', label=_('Parameters'),
360                     names=('param', 'parameter', 'arg', 'argument',
361                            'keyword', 'kwarg', 'kwparam'),
362                     typerolename='class', typenames=('paramtype', 'type'),
363                     can_collapse=True),
364        PyTypedField('variable', label=_('Variables'), rolename='obj',
365                     names=('var', 'ivar', 'cvar'),
366                     typerolename='class', typenames=('vartype',),
367                     can_collapse=True),
368        PyGroupedField('exceptions', label=_('Raises'), rolename='exc',
369                       names=('raises', 'raise', 'exception', 'except'),
370                       can_collapse=True),
371        Field('returnvalue', label=_('Returns'), has_arg=False,
372              names=('returns', 'return')),
373        PyField('returntype', label=_('Return type'), has_arg=False,
374                names=('rtype',), bodyrolename='class'),
375    ]
376
377    allow_nesting = False
378
379    def get_signature_prefix(self, sig: str) -> str:
380        """May return a prefix to put before the object name in the
381        signature.
382        """
383        return ''
384
385    def needs_arglist(self) -> bool:
386        """May return true if an empty argument list is to be generated even if
387        the document contains none.
388        """
389        return False
390
391    def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
392        """Transform a Python signature into RST nodes.
393
394        Return (fully qualified name of the thing, classname if any).
395
396        If inside a class, the current class name is handled intelligently:
397        * it is stripped from the displayed name if present
398        * it is added to the full name (return value) if not present
399        """
400        m = py_sig_re.match(sig)
401        if m is None:
402            raise ValueError
403        prefix, name, arglist, retann = m.groups()
404
405        # determine module and class name (if applicable), as well as full name
406        modname = self.options.get('module', self.env.ref_context.get('py:module'))
407        classname = self.env.ref_context.get('py:class')
408        if classname:
409            add_module = False
410            if prefix and (prefix == classname or
411                           prefix.startswith(classname + ".")):
412                fullname = prefix + name
413                # class name is given again in the signature
414                prefix = prefix[len(classname):].lstrip('.')
415            elif prefix:
416                # class name is given in the signature, but different
417                # (shouldn't happen)
418                fullname = classname + '.' + prefix + name
419            else:
420                # class name is not given in the signature
421                fullname = classname + '.' + name
422        else:
423            add_module = True
424            if prefix:
425                classname = prefix.rstrip('.')
426                fullname = prefix + name
427            else:
428                classname = ''
429                fullname = name
430
431        signode['module'] = modname
432        signode['class'] = classname
433        signode['fullname'] = fullname
434
435        sig_prefix = self.get_signature_prefix(sig)
436        if sig_prefix:
437            signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
438
439        if prefix:
440            signode += addnodes.desc_addname(prefix, prefix)
441        elif add_module and self.env.config.add_module_names:
442            if modname and modname != 'exceptions':
443                # exceptions are a special case, since they are documented in the
444                # 'exceptions' module.
445                nodetext = modname + '.'
446                signode += addnodes.desc_addname(nodetext, nodetext)
447
448        signode += addnodes.desc_name(name, name)
449        if arglist:
450            try:
451                signode += _parse_arglist(arglist, self.env)
452            except SyntaxError:
453                # fallback to parse arglist original parser.
454                # it supports to represent optional arguments (ex. "func(foo [, bar])")
455                _pseudo_parse_arglist(signode, arglist)
456            except NotImplementedError as exc:
457                logger.warning("could not parse arglist (%r): %s", arglist, exc,
458                               location=signode)
459                _pseudo_parse_arglist(signode, arglist)
460        else:
461            if self.needs_arglist():
462                # for callables, add an empty parameter list
463                signode += addnodes.desc_parameterlist()
464
465        if retann:
466            children = _parse_annotation(retann, self.env)
467            signode += addnodes.desc_returns(retann, '', *children)
468
469        anno = self.options.get('annotation')
470        if anno:
471            signode += addnodes.desc_annotation(' ' + anno, ' ' + anno)
472
473        return fullname, prefix
474
475    def get_index_text(self, modname: str, name: Tuple[str, str]) -> str:
476        """Return the text for the index entry of the object."""
477        raise NotImplementedError('must be implemented in subclasses')
478
479    def add_target_and_index(self, name_cls: Tuple[str, str], sig: str,
480                             signode: desc_signature) -> None:
481        modname = self.options.get('module', self.env.ref_context.get('py:module'))
482        fullname = (modname + '.' if modname else '') + name_cls[0]
483        node_id = make_id(self.env, self.state.document, '', fullname)
484        signode['ids'].append(node_id)
485
486        # Assign old styled node_id(fullname) not to break old hyperlinks (if possible)
487        # Note: Will removed in Sphinx-5.0  (RemovedInSphinx50Warning)
488        if node_id != fullname and fullname not in self.state.document.ids:
489            signode['ids'].append(fullname)
490
491        self.state.document.note_explicit_target(signode)
492
493        domain = cast(PythonDomain, self.env.get_domain('py'))
494        domain.note_object(fullname, self.objtype, node_id, location=signode)
495
496        if 'noindexentry' not in self.options:
497            indextext = self.get_index_text(modname, name_cls)
498            if indextext:
499                self.indexnode['entries'].append(('single', indextext, node_id, '', None))
500
501    def before_content(self) -> None:
502        """Handle object nesting before content
503
504        :py:class:`PyObject` represents Python language constructs. For
505        constructs that are nestable, such as a Python classes, this method will
506        build up a stack of the nesting hierarchy so that it can be later
507        de-nested correctly, in :py:meth:`after_content`.
508
509        For constructs that aren't nestable, the stack is bypassed, and instead
510        only the most recent object is tracked. This object prefix name will be
511        removed with :py:meth:`after_content`.
512        """
513        prefix = None
514        if self.names:
515            # fullname and name_prefix come from the `handle_signature` method.
516            # fullname represents the full object name that is constructed using
517            # object nesting and explicit prefixes. `name_prefix` is the
518            # explicit prefix given in a signature
519            (fullname, name_prefix) = self.names[-1]
520            if self.allow_nesting:
521                prefix = fullname
522            elif name_prefix:
523                prefix = name_prefix.strip('.')
524        if prefix:
525            self.env.ref_context['py:class'] = prefix
526            if self.allow_nesting:
527                classes = self.env.ref_context.setdefault('py:classes', [])
528                classes.append(prefix)
529        if 'module' in self.options:
530            modules = self.env.ref_context.setdefault('py:modules', [])
531            modules.append(self.env.ref_context.get('py:module'))
532            self.env.ref_context['py:module'] = self.options['module']
533
534    def after_content(self) -> None:
535        """Handle object de-nesting after content
536
537        If this class is a nestable object, removing the last nested class prefix
538        ends further nesting in the object.
539
540        If this class is not a nestable object, the list of classes should not
541        be altered as we didn't affect the nesting levels in
542        :py:meth:`before_content`.
543        """
544        classes = self.env.ref_context.setdefault('py:classes', [])
545        if self.allow_nesting:
546            try:
547                classes.pop()
548            except IndexError:
549                pass
550        self.env.ref_context['py:class'] = (classes[-1] if len(classes) > 0
551                                            else None)
552        if 'module' in self.options:
553            modules = self.env.ref_context.setdefault('py:modules', [])
554            if modules:
555                self.env.ref_context['py:module'] = modules.pop()
556            else:
557                self.env.ref_context.pop('py:module')
558
559
560class PyModulelevel(PyObject):
561    """
562    Description of an object on module level (functions, data).
563    """
564
565    def run(self) -> List[Node]:
566        for cls in self.__class__.__mro__:
567            if cls.__name__ != 'DirectiveAdapter':
568                warnings.warn('PyModulelevel is deprecated. '
569                              'Please check the implementation of %s' % cls,
570                              RemovedInSphinx40Warning, stacklevel=2)
571                break
572        else:
573            warnings.warn('PyModulelevel is deprecated',
574                          RemovedInSphinx40Warning, stacklevel=2)
575
576        return super().run()
577
578    def needs_arglist(self) -> bool:
579        return self.objtype == 'function'
580
581    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
582        if self.objtype == 'function':
583            if not modname:
584                return _('%s() (built-in function)') % name_cls[0]
585            return _('%s() (in module %s)') % (name_cls[0], modname)
586        elif self.objtype == 'data':
587            if not modname:
588                return _('%s (built-in variable)') % name_cls[0]
589            return _('%s (in module %s)') % (name_cls[0], modname)
590        else:
591            return ''
592
593
594class PyFunction(PyObject):
595    """Description of a function."""
596
597    option_spec = PyObject.option_spec.copy()
598    option_spec.update({
599        'async': directives.flag,
600    })
601
602    def get_signature_prefix(self, sig: str) -> str:
603        if 'async' in self.options:
604            return 'async '
605        else:
606            return ''
607
608    def needs_arglist(self) -> bool:
609        return True
610
611    def add_target_and_index(self, name_cls: Tuple[str, str], sig: str,
612                             signode: desc_signature) -> None:
613        super().add_target_and_index(name_cls, sig, signode)
614        if 'noindexentry' not in self.options:
615            modname = self.options.get('module', self.env.ref_context.get('py:module'))
616            node_id = signode['ids'][0]
617
618            name, cls = name_cls
619            if modname:
620                text = _('%s() (in module %s)') % (name, modname)
621                self.indexnode['entries'].append(('single', text, node_id, '', None))
622            else:
623                text = '%s; %s()' % (pairindextypes['builtin'], name)
624                self.indexnode['entries'].append(('pair', text, node_id, '', None))
625
626    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
627        # add index in own add_target_and_index() instead.
628        return None
629
630
631class PyDecoratorFunction(PyFunction):
632    """Description of a decorator."""
633
634    def run(self) -> List[Node]:
635        # a decorator function is a function after all
636        self.name = 'py:function'
637        return super().run()
638
639    def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
640        ret = super().handle_signature(sig, signode)
641        signode.insert(0, addnodes.desc_addname('@', '@'))
642        return ret
643
644    def needs_arglist(self) -> bool:
645        return False
646
647
648class PyVariable(PyObject):
649    """Description of a variable."""
650
651    option_spec = PyObject.option_spec.copy()
652    option_spec.update({
653        'type': directives.unchanged,
654        'value': directives.unchanged,
655    })
656
657    def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
658        fullname, prefix = super().handle_signature(sig, signode)
659
660        typ = self.options.get('type')
661        if typ:
662            annotations = _parse_annotation(typ, self.env)
663            signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations)
664
665        value = self.options.get('value')
666        if value:
667            signode += addnodes.desc_annotation(value, ' = ' + value)
668
669        return fullname, prefix
670
671    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
672        name, cls = name_cls
673        if modname:
674            return _('%s (in module %s)') % (name, modname)
675        else:
676            return _('%s (built-in variable)') % name
677
678
679class PyClasslike(PyObject):
680    """
681    Description of a class-like object (classes, interfaces, exceptions).
682    """
683
684    option_spec = PyObject.option_spec.copy()
685    option_spec.update({
686        'final': directives.flag,
687    })
688
689    allow_nesting = True
690
691    def get_signature_prefix(self, sig: str) -> str:
692        if 'final' in self.options:
693            return 'final %s ' % self.objtype
694        else:
695            return '%s ' % self.objtype
696
697    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
698        if self.objtype == 'class':
699            if not modname:
700                return _('%s (built-in class)') % name_cls[0]
701            return _('%s (class in %s)') % (name_cls[0], modname)
702        elif self.objtype == 'exception':
703            return name_cls[0]
704        else:
705            return ''
706
707
708class PyClassmember(PyObject):
709    """
710    Description of a class member (methods, attributes).
711    """
712
713    def run(self) -> List[Node]:
714        for cls in self.__class__.__mro__:
715            if cls.__name__ != 'DirectiveAdapter':
716                warnings.warn('PyClassmember is deprecated. '
717                              'Please check the implementation of %s' % cls,
718                              RemovedInSphinx40Warning, stacklevel=2)
719                break
720        else:
721            warnings.warn('PyClassmember is deprecated',
722                          RemovedInSphinx40Warning, stacklevel=2)
723
724        return super().run()
725
726    def needs_arglist(self) -> bool:
727        return self.objtype.endswith('method')
728
729    def get_signature_prefix(self, sig: str) -> str:
730        if self.objtype == 'staticmethod':
731            return 'static '
732        elif self.objtype == 'classmethod':
733            return 'classmethod '
734        return ''
735
736    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
737        name, cls = name_cls
738        add_modules = self.env.config.add_module_names
739        if self.objtype == 'method':
740            try:
741                clsname, methname = name.rsplit('.', 1)
742            except ValueError:
743                if modname:
744                    return _('%s() (in module %s)') % (name, modname)
745                else:
746                    return '%s()' % name
747            if modname and add_modules:
748                return _('%s() (%s.%s method)') % (methname, modname, clsname)
749            else:
750                return _('%s() (%s method)') % (methname, clsname)
751        elif self.objtype == 'staticmethod':
752            try:
753                clsname, methname = name.rsplit('.', 1)
754            except ValueError:
755                if modname:
756                    return _('%s() (in module %s)') % (name, modname)
757                else:
758                    return '%s()' % name
759            if modname and add_modules:
760                return _('%s() (%s.%s static method)') % (methname, modname,
761                                                          clsname)
762            else:
763                return _('%s() (%s static method)') % (methname, clsname)
764        elif self.objtype == 'classmethod':
765            try:
766                clsname, methname = name.rsplit('.', 1)
767            except ValueError:
768                if modname:
769                    return _('%s() (in module %s)') % (name, modname)
770                else:
771                    return '%s()' % name
772            if modname:
773                return _('%s() (%s.%s class method)') % (methname, modname,
774                                                         clsname)
775            else:
776                return _('%s() (%s class method)') % (methname, clsname)
777        elif self.objtype == 'attribute':
778            try:
779                clsname, attrname = name.rsplit('.', 1)
780            except ValueError:
781                if modname:
782                    return _('%s (in module %s)') % (name, modname)
783                else:
784                    return name
785            if modname and add_modules:
786                return _('%s (%s.%s attribute)') % (attrname, modname, clsname)
787            else:
788                return _('%s (%s attribute)') % (attrname, clsname)
789        else:
790            return ''
791
792
793class PyMethod(PyObject):
794    """Description of a method."""
795
796    option_spec = PyObject.option_spec.copy()
797    option_spec.update({
798        'abstractmethod': directives.flag,
799        'async': directives.flag,
800        'classmethod': directives.flag,
801        'final': directives.flag,
802        'property': directives.flag,
803        'staticmethod': directives.flag,
804    })
805
806    def needs_arglist(self) -> bool:
807        if 'property' in self.options:
808            return False
809        else:
810            return True
811
812    def get_signature_prefix(self, sig: str) -> str:
813        prefix = []
814        if 'final' in self.options:
815            prefix.append('final')
816        if 'abstractmethod' in self.options:
817            prefix.append('abstract')
818        if 'async' in self.options:
819            prefix.append('async')
820        if 'classmethod' in self.options:
821            prefix.append('classmethod')
822        if 'property' in self.options:
823            prefix.append('property')
824        if 'staticmethod' in self.options:
825            prefix.append('static')
826
827        if prefix:
828            return ' '.join(prefix) + ' '
829        else:
830            return ''
831
832    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
833        name, cls = name_cls
834        try:
835            clsname, methname = name.rsplit('.', 1)
836            if modname and self.env.config.add_module_names:
837                clsname = '.'.join([modname, clsname])
838        except ValueError:
839            if modname:
840                return _('%s() (in module %s)') % (name, modname)
841            else:
842                return '%s()' % name
843
844        if 'classmethod' in self.options:
845            return _('%s() (%s class method)') % (methname, clsname)
846        elif 'property' in self.options:
847            return _('%s() (%s property)') % (methname, clsname)
848        elif 'staticmethod' in self.options:
849            return _('%s() (%s static method)') % (methname, clsname)
850        else:
851            return _('%s() (%s method)') % (methname, clsname)
852
853
854class PyClassMethod(PyMethod):
855    """Description of a classmethod."""
856
857    option_spec = PyObject.option_spec.copy()
858
859    def run(self) -> List[Node]:
860        self.name = 'py:method'
861        self.options['classmethod'] = True
862
863        return super().run()
864
865
866class PyStaticMethod(PyMethod):
867    """Description of a staticmethod."""
868
869    option_spec = PyObject.option_spec.copy()
870
871    def run(self) -> List[Node]:
872        self.name = 'py:method'
873        self.options['staticmethod'] = True
874
875        return super().run()
876
877
878class PyDecoratorMethod(PyMethod):
879    """Description of a decoratormethod."""
880
881    def run(self) -> List[Node]:
882        self.name = 'py:method'
883        return super().run()
884
885    def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
886        ret = super().handle_signature(sig, signode)
887        signode.insert(0, addnodes.desc_addname('@', '@'))
888        return ret
889
890    def needs_arglist(self) -> bool:
891        return False
892
893
894class PyAttribute(PyObject):
895    """Description of an attribute."""
896
897    option_spec = PyObject.option_spec.copy()
898    option_spec.update({
899        'type': directives.unchanged,
900        'value': directives.unchanged,
901    })
902
903    def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
904        fullname, prefix = super().handle_signature(sig, signode)
905
906        typ = self.options.get('type')
907        if typ:
908            annotations = _parse_annotation(typ, self.env)
909            signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations)
910
911        value = self.options.get('value')
912        if value:
913            signode += addnodes.desc_annotation(value, ' = ' + value)
914
915        return fullname, prefix
916
917    def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
918        name, cls = name_cls
919        try:
920            clsname, attrname = name.rsplit('.', 1)
921            if modname and self.env.config.add_module_names:
922                clsname = '.'.join([modname, clsname])
923        except ValueError:
924            if modname:
925                return _('%s (in module %s)') % (name, modname)
926            else:
927                return name
928
929        return _('%s (%s attribute)') % (attrname, clsname)
930
931
932class PyDecoratorMixin:
933    """
934    Mixin for decorator directives.
935    """
936    def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
937        for cls in self.__class__.__mro__:
938            if cls.__name__ != 'DirectiveAdapter':
939                warnings.warn('PyDecoratorMixin is deprecated. '
940                              'Please check the implementation of %s' % cls,
941                              RemovedInSphinx50Warning, stacklevel=2)
942                break
943        else:
944            warnings.warn('PyDecoratorMixin is deprecated',
945                          RemovedInSphinx50Warning, stacklevel=2)
946
947        ret = super().handle_signature(sig, signode)  # type: ignore
948        signode.insert(0, addnodes.desc_addname('@', '@'))
949        return ret
950
951    def needs_arglist(self) -> bool:
952        return False
953
954
955class PyModule(SphinxDirective):
956    """
957    Directive to mark description of a new module.
958    """
959
960    has_content = False
961    required_arguments = 1
962    optional_arguments = 0
963    final_argument_whitespace = False
964    option_spec = {
965        'platform': lambda x: x,
966        'synopsis': lambda x: x,
967        'noindex': directives.flag,
968        'deprecated': directives.flag,
969    }
970
971    def run(self) -> List[Node]:
972        domain = cast(PythonDomain, self.env.get_domain('py'))
973
974        modname = self.arguments[0].strip()
975        noindex = 'noindex' in self.options
976        self.env.ref_context['py:module'] = modname
977        ret = []  # type: List[Node]
978        if not noindex:
979            # note module to the domain
980            node_id = make_id(self.env, self.state.document, 'module', modname)
981            target = nodes.target('', '', ids=[node_id], ismod=True)
982            self.set_source_info(target)
983
984            # Assign old styled node_id not to break old hyperlinks (if possible)
985            # Note: Will removed in Sphinx-5.0  (RemovedInSphinx50Warning)
986            old_node_id = self.make_old_id(modname)
987            if node_id != old_node_id and old_node_id not in self.state.document.ids:
988                target['ids'].append(old_node_id)
989
990            self.state.document.note_explicit_target(target)
991
992            domain.note_module(modname,
993                               node_id,
994                               self.options.get('synopsis', ''),
995                               self.options.get('platform', ''),
996                               'deprecated' in self.options)
997            domain.note_object(modname, 'module', node_id, location=target)
998
999            # the platform and synopsis aren't printed; in fact, they are only
1000            # used in the modindex currently
1001            ret.append(target)
1002            indextext = '%s; %s' % (pairindextypes['module'], modname)
1003            inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)])
1004            ret.append(inode)
1005        return ret
1006
1007    def make_old_id(self, name: str) -> str:
1008        """Generate old styled node_id.
1009
1010        Old styled node_id is incompatible with docutils' node_id.
1011        It can contain dots and hyphens.
1012
1013        .. note:: Old styled node_id was mainly used until Sphinx-3.0.
1014        """
1015        return 'module-%s' % name
1016
1017
1018class PyCurrentModule(SphinxDirective):
1019    """
1020    This directive is just to tell Sphinx that we're documenting
1021    stuff in module foo, but links to module foo won't lead here.
1022    """
1023
1024    has_content = False
1025    required_arguments = 1
1026    optional_arguments = 0
1027    final_argument_whitespace = False
1028    option_spec = {}  # type: Dict
1029
1030    def run(self) -> List[Node]:
1031        modname = self.arguments[0].strip()
1032        if modname == 'None':
1033            self.env.ref_context.pop('py:module', None)
1034        else:
1035            self.env.ref_context['py:module'] = modname
1036        return []
1037
1038
1039class PyXRefRole(XRefRole):
1040    def process_link(self, env: BuildEnvironment, refnode: Element,
1041                     has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
1042        refnode['py:module'] = env.ref_context.get('py:module')
1043        refnode['py:class'] = env.ref_context.get('py:class')
1044        if not has_explicit_title:
1045            title = title.lstrip('.')    # only has a meaning for the target
1046            target = target.lstrip('~')  # only has a meaning for the title
1047            # if the first character is a tilde, don't display the module/class
1048            # parts of the contents
1049            if title[0:1] == '~':
1050                title = title[1:]
1051                dot = title.rfind('.')
1052                if dot != -1:
1053                    title = title[dot + 1:]
1054        # if the first character is a dot, search more specific namespaces first
1055        # else search builtins first
1056        if target[0:1] == '.':
1057            target = target[1:]
1058            refnode['refspecific'] = True
1059        return title, target
1060
1061
1062def filter_meta_fields(app: Sphinx, domain: str, objtype: str, content: Element) -> None:
1063    """Filter ``:meta:`` field from its docstring."""
1064    if domain != 'py':
1065        return
1066
1067    for node in content:
1068        if isinstance(node, nodes.field_list):
1069            fields = cast(List[nodes.field], node)
1070            for field in fields:
1071                field_name = cast(nodes.field_body, field[0]).astext().strip()
1072                if field_name == 'meta' or field_name.startswith('meta '):
1073                    node.remove(field)
1074                    break
1075
1076
1077class PythonModuleIndex(Index):
1078    """
1079    Index subclass to provide the Python module index.
1080    """
1081
1082    name = 'modindex'
1083    localname = _('Python Module Index')
1084    shortname = _('modules')
1085
1086    def generate(self, docnames: Iterable[str] = None
1087                 ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
1088        content = {}  # type: Dict[str, List[IndexEntry]]
1089        # list of prefixes to ignore
1090        ignores = None  # type: List[str]
1091        ignores = self.domain.env.config['modindex_common_prefix']  # type: ignore
1092        ignores = sorted(ignores, key=len, reverse=True)
1093        # list of all modules, sorted by module name
1094        modules = sorted(self.domain.data['modules'].items(),
1095                         key=lambda x: x[0].lower())
1096        # sort out collapsable modules
1097        prev_modname = ''
1098        num_toplevels = 0
1099        for modname, (docname, node_id, synopsis, platforms, deprecated) in modules:
1100            if docnames and docname not in docnames:
1101                continue
1102
1103            for ignore in ignores:
1104                if modname.startswith(ignore):
1105                    modname = modname[len(ignore):]
1106                    stripped = ignore
1107                    break
1108            else:
1109                stripped = ''
1110
1111            # we stripped the whole module name?
1112            if not modname:
1113                modname, stripped = stripped, ''
1114
1115            entries = content.setdefault(modname[0].lower(), [])
1116
1117            package = modname.split('.')[0]
1118            if package != modname:
1119                # it's a submodule
1120                if prev_modname == package:
1121                    # first submodule - make parent a group head
1122                    if entries:
1123                        last = entries[-1]
1124                        entries[-1] = IndexEntry(last[0], 1, last[2], last[3],
1125                                                 last[4], last[5], last[6])
1126                elif not prev_modname.startswith(package):
1127                    # submodule without parent in list, add dummy entry
1128                    entries.append(IndexEntry(stripped + package, 1, '', '', '', '', ''))
1129                subtype = 2
1130            else:
1131                num_toplevels += 1
1132                subtype = 0
1133
1134            qualifier = _('Deprecated') if deprecated else ''
1135            entries.append(IndexEntry(stripped + modname, subtype, docname,
1136                                      node_id, platforms, qualifier, synopsis))
1137            prev_modname = modname
1138
1139        # apply heuristics when to collapse modindex at page load:
1140        # only collapse if number of toplevel modules is larger than
1141        # number of submodules
1142        collapse = len(modules) - num_toplevels < num_toplevels
1143
1144        # sort by first letter
1145        sorted_content = sorted(content.items())
1146
1147        return sorted_content, collapse
1148
1149
1150class PythonDomain(Domain):
1151    """Python language domain."""
1152    name = 'py'
1153    label = 'Python'
1154    object_types = {
1155        'function':     ObjType(_('function'),      'func', 'obj'),
1156        'data':         ObjType(_('data'),          'data', 'obj'),
1157        'class':        ObjType(_('class'),         'class', 'exc', 'obj'),
1158        'exception':    ObjType(_('exception'),     'exc', 'class', 'obj'),
1159        'method':       ObjType(_('method'),        'meth', 'obj'),
1160        'classmethod':  ObjType(_('class method'),  'meth', 'obj'),
1161        'staticmethod': ObjType(_('static method'), 'meth', 'obj'),
1162        'attribute':    ObjType(_('attribute'),     'attr', 'obj'),
1163        'module':       ObjType(_('module'),        'mod', 'obj'),
1164    }  # type: Dict[str, ObjType]
1165
1166    directives = {
1167        'function':        PyFunction,
1168        'data':            PyVariable,
1169        'class':           PyClasslike,
1170        'exception':       PyClasslike,
1171        'method':          PyMethod,
1172        'classmethod':     PyClassMethod,
1173        'staticmethod':    PyStaticMethod,
1174        'attribute':       PyAttribute,
1175        'module':          PyModule,
1176        'currentmodule':   PyCurrentModule,
1177        'decorator':       PyDecoratorFunction,
1178        'decoratormethod': PyDecoratorMethod,
1179    }
1180    roles = {
1181        'data':  PyXRefRole(),
1182        'exc':   PyXRefRole(),
1183        'func':  PyXRefRole(fix_parens=True),
1184        'class': PyXRefRole(),
1185        'const': PyXRefRole(),
1186        'attr':  PyXRefRole(),
1187        'meth':  PyXRefRole(fix_parens=True),
1188        'mod':   PyXRefRole(),
1189        'obj':   PyXRefRole(),
1190    }
1191    initial_data = {
1192        'objects': {},  # fullname -> docname, objtype
1193        'modules': {},  # modname -> docname, synopsis, platform, deprecated
1194    }  # type: Dict[str, Dict[str, Tuple[Any]]]
1195    indices = [
1196        PythonModuleIndex,
1197    ]
1198
1199    @property
1200    def objects(self) -> Dict[str, ObjectEntry]:
1201        return self.data.setdefault('objects', {})  # fullname -> ObjectEntry
1202
1203    def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None:
1204        """Note a python object for cross reference.
1205
1206        .. versionadded:: 2.1
1207        """
1208        if name in self.objects:
1209            other = self.objects[name]
1210            logger.warning(__('duplicate object description of %s, '
1211                              'other instance in %s, use :noindex: for one of them'),
1212                           name, other.docname, location=location)
1213        self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
1214
1215    @property
1216    def modules(self) -> Dict[str, ModuleEntry]:
1217        return self.data.setdefault('modules', {})  # modname -> ModuleEntry
1218
1219    def note_module(self, name: str, node_id: str, synopsis: str,
1220                    platform: str, deprecated: bool) -> None:
1221        """Note a python module for cross reference.
1222
1223        .. versionadded:: 2.1
1224        """
1225        self.modules[name] = ModuleEntry(self.env.docname, node_id,
1226                                         synopsis, platform, deprecated)
1227
1228    def clear_doc(self, docname: str) -> None:
1229        for fullname, obj in list(self.objects.items()):
1230            if obj.docname == docname:
1231                del self.objects[fullname]
1232        for modname, mod in list(self.modules.items()):
1233            if mod.docname == docname:
1234                del self.modules[modname]
1235
1236    def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
1237        # XXX check duplicates?
1238        for fullname, obj in otherdata['objects'].items():
1239            if obj.docname in docnames:
1240                self.objects[fullname] = obj
1241        for modname, mod in otherdata['modules'].items():
1242            if mod.docname in docnames:
1243                self.modules[modname] = mod
1244
1245    def find_obj(self, env: BuildEnvironment, modname: str, classname: str,
1246                 name: str, type: str, searchmode: int = 0
1247                 ) -> List[Tuple[str, ObjectEntry]]:
1248        """Find a Python object for "name", perhaps using the given module
1249        and/or classname.  Returns a list of (name, object entry) tuples.
1250        """
1251        # skip parens
1252        if name[-2:] == '()':
1253            name = name[:-2]
1254
1255        if not name:
1256            return []
1257
1258        matches = []  # type: List[Tuple[str, ObjectEntry]]
1259
1260        newname = None
1261        if searchmode == 1:
1262            if type is None:
1263                objtypes = list(self.object_types)
1264            else:
1265                objtypes = self.objtypes_for_role(type)
1266            if objtypes is not None:
1267                if modname and classname:
1268                    fullname = modname + '.' + classname + '.' + name
1269                    if fullname in self.objects and self.objects[fullname].objtype in objtypes:
1270                        newname = fullname
1271                if not newname:
1272                    if modname and modname + '.' + name in self.objects and \
1273                       self.objects[modname + '.' + name].objtype in objtypes:
1274                        newname = modname + '.' + name
1275                    elif name in self.objects and self.objects[name].objtype in objtypes:
1276                        newname = name
1277                    else:
1278                        # "fuzzy" searching mode
1279                        searchname = '.' + name
1280                        matches = [(oname, self.objects[oname]) for oname in self.objects
1281                                   if oname.endswith(searchname) and
1282                                   self.objects[oname].objtype in objtypes]
1283        else:
1284            # NOTE: searching for exact match, object type is not considered
1285            if name in self.objects:
1286                newname = name
1287            elif type == 'mod':
1288                # only exact matches allowed for modules
1289                return []
1290            elif classname and classname + '.' + name in self.objects:
1291                newname = classname + '.' + name
1292            elif modname and modname + '.' + name in self.objects:
1293                newname = modname + '.' + name
1294            elif modname and classname and \
1295                    modname + '.' + classname + '.' + name in self.objects:
1296                newname = modname + '.' + classname + '.' + name
1297        if newname is not None:
1298            matches.append((newname, self.objects[newname]))
1299        return matches
1300
1301    def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
1302                     type: str, target: str, node: pending_xref, contnode: Element
1303                     ) -> Element:
1304        modname = node.get('py:module')
1305        clsname = node.get('py:class')
1306        searchmode = 1 if node.hasattr('refspecific') else 0
1307        matches = self.find_obj(env, modname, clsname, target,
1308                                type, searchmode)
1309
1310        if not matches and type == 'attr':
1311            # fallback to meth (for property)
1312            matches = self.find_obj(env, modname, clsname, target, 'meth', searchmode)
1313
1314        if not matches:
1315            return None
1316        elif len(matches) > 1:
1317            logger.warning(__('more than one target found for cross-reference %r: %s'),
1318                           target, ', '.join(match[0] for match in matches),
1319                           type='ref', subtype='python', location=node)
1320        name, obj = matches[0]
1321
1322        if obj[2] == 'module':
1323            return self._make_module_refnode(builder, fromdocname, name, contnode)
1324        else:
1325            return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name)
1326
1327    def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
1328                         target: str, node: pending_xref, contnode: Element
1329                         ) -> List[Tuple[str, Element]]:
1330        modname = node.get('py:module')
1331        clsname = node.get('py:class')
1332        results = []  # type: List[Tuple[str, Element]]
1333
1334        # always search in "refspecific" mode with the :any: role
1335        matches = self.find_obj(env, modname, clsname, target, None, 1)
1336        for name, obj in matches:
1337            if obj[2] == 'module':
1338                results.append(('py:mod',
1339                                self._make_module_refnode(builder, fromdocname,
1340                                                          name, contnode)))
1341            else:
1342                results.append(('py:' + self.role_for_objtype(obj[2]),
1343                                make_refnode(builder, fromdocname, obj[0], obj[1],
1344                                             contnode, name)))
1345        return results
1346
1347    def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str,
1348                             contnode: Node) -> Element:
1349        # get additional info for modules
1350        module = self.modules[name]
1351        title = name
1352        if module.synopsis:
1353            title += ': ' + module.synopsis
1354        if module.deprecated:
1355            title += _(' (deprecated)')
1356        if module.platform:
1357            title += ' (' + module.platform + ')'
1358        return make_refnode(builder, fromdocname, module.docname, module.node_id,
1359                            contnode, title)
1360
1361    def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
1362        for modname, mod in self.modules.items():
1363            yield (modname, modname, 'module', mod.docname, mod.node_id, 0)
1364        for refname, obj in self.objects.items():
1365            if obj.objtype != 'module':  # modules are already handled
1366                yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
1367
1368    def get_full_qualified_name(self, node: Element) -> str:
1369        modname = node.get('py:module')
1370        clsname = node.get('py:class')
1371        target = node.get('reftarget')
1372        if target is None:
1373            return None
1374        else:
1375            return '.'.join(filter(None, [modname, clsname, target]))
1376
1377
1378def builtin_resolver(app: Sphinx, env: BuildEnvironment,
1379                     node: pending_xref, contnode: Element) -> Element:
1380    """Do not emit nitpicky warnings for built-in types."""
1381    def istyping(s: str) -> bool:
1382        if s.startswith('typing.'):
1383            s = s.split('.', 1)[1]
1384
1385        return s in typing.__all__  # type: ignore
1386
1387    if node.get('refdomain') != 'py':
1388        return None
1389    elif node.get('reftype') in ('class', 'obj') and node.get('reftarget') == 'None':
1390        return contnode
1391    elif node.get('reftype') in ('class', 'exc'):
1392        reftarget = node.get('reftarget')
1393        if inspect.isclass(getattr(builtins, reftarget, None)):
1394            # built-in class
1395            return contnode
1396        elif istyping(reftarget):
1397            # typing class
1398            return contnode
1399
1400    return None
1401
1402
1403def setup(app: Sphinx) -> Dict[str, Any]:
1404    app.setup_extension('sphinx.directives')
1405
1406    app.add_domain(PythonDomain)
1407    app.connect('object-description-transform', filter_meta_fields)
1408    app.connect('missing-reference', builtin_resolver, priority=900)
1409
1410    return {
1411        'version': 'builtin',
1412        'env_version': 2,
1413        'parallel_read_safe': True,
1414        'parallel_write_safe': True,
1415    }
1416