File: smart_resolver.py

package info (click to toggle)
sphinx-automodapi 0.18.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 944 kB
  • sloc: python: 1,696; makefile: 197
file content (139 lines) | stat: -rw-r--r-- 5,819 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
The classes in the astropy docs are documented by their API location,
which is not necessarily where they are defined in the source.  This
causes a problem when certain automated features of the doc build,
such as the inheritance diagrams or the `Bases` list of a class
reference a class by its canonical location rather than its "user"
location.

In the `autodoc-process-docstring` event, a mapping from the actual
name to the API name is maintained.  Later, in the `missing-reference`
event, unresolved references are looked up in this dictionary and
corrected if possible.
"""

from docutils.nodes import literal, reference


def process_docstring(app, what, name, obj, options, lines):
    if isinstance(obj, type):
        env = app.env
        if not hasattr(env, 'class_name_mapping'):
            env.class_name_mapping = {}
        mapping = env.class_name_mapping
        mapping[obj.__module__ + '.' + obj.__name__] = name


def merge_mapping(app, env, docnames, env_other):
    if not hasattr(env_other, 'class_name_mapping'):
        return
    if not hasattr(env, 'class_name_mapping'):
        env.class_name_mapping = {}
    env.class_name_mapping.update(env_other.class_name_mapping)


def missing_reference_handler(app, env, node, contnode):
    """
    Handler to be connect to the sphinx 'missing-reference' event.  The handler a
    resolves reference (node) and returns a new node when sphinx could not
    originally resolve the reference.

    see `missing-reference in sphinx documentation
    <https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-missing-reference>`_

    :param app: The Sphinx application object
    :param env: The build environment (``app.builder.env`)
    :param node: The ``pending_xref`` node to be resolved. Its attributes reftype,
                 reftarget, modname and classname attributes determine the type and
                 target of the reference.
    :param contnode: The node that carries the text and formatting inside the
                     future reference and should be a child of the returned
                     reference node.
    """
    # a good example of how a missing reference handle works look to
    #  https://github.com/sphinx-doc/sphinx/issues/1572#issuecomment-68590981
    #
    # Important attributes of the "node":
    #
    #      example role:  :ref:`title <target>`
    #
    #  'reftype'     - role name (in the example above 'ref' is the reftype)
    #  'reftarget'   - target of the role, as given in the role content
    #                  (in the example 'target' is the reftarget
    #  'refexplicit' - the explicit title of the role
    #                  (in the example 'title' is the refexplicit)
    #  'refdoc'      - document in which the role appeared
    #  'refdomain'   - domain of the role, in our case emtpy

    if not hasattr(env, 'class_name_mapping'):
        env.class_name_mapping = {}
    mapping = env.class_name_mapping

    reftype = node['reftype']
    reftarget = node['reftarget']
    refexplicit = node.get('refexplicit')  # default: None
    refdoc = node.get('refdoc', env.docname)
    if reftype in ('obj', 'class', 'exc', 'meth'):
        suffix = ''
        if reftarget not in mapping:
            if '.' in reftarget:
                front, suffix = reftarget.rsplit('.', 1)
            else:
                front = None
                suffix = reftarget

            if suffix.startswith('_') and not suffix.startswith('__'):
                # If this is a reference to a hidden class or method,
                # we can't link to it, but we don't want to have a
                # nitpick warning.
                return node[0].deepcopy()

            if reftype in ('obj', 'meth') and front is not None:
                if front in mapping:
                    reftarget = front
                    suffix = '.' + suffix

            if (reftype in ('class', ) and '.' in reftarget and
                    reftarget not in mapping):

                if '.' in front:
                    reftarget, _ = front.rsplit('.', 1)
                    suffix = '.' + suffix
                reftarget = reftarget + suffix
                prefix = reftarget.rsplit('.')[0]
                inventory = getattr(env, 'intersphinx_named_inventory', {})
                if (reftarget not in mapping and
                        prefix in inventory):

                    if 'py:class' in inventory[prefix] and \
                            reftarget in inventory[prefix]['py:class']:
                        newtarget = inventory[prefix]['py:class'][reftarget][2]
                        if not refexplicit and '~' not in node.rawsource:
                            contnode = literal(text=reftarget)
                        newnode = reference('', '', internal=True)
                        newnode['reftitle'] = reftarget
                        newnode['refuri'] = newtarget
                        newnode.append(contnode)

                        return newnode

        if reftarget in mapping:
            newtarget = mapping[reftarget] + suffix
            if not refexplicit and '~' not in node.rawsource:
                contnode = literal(text=newtarget)
            newnode = env.domains['py'].resolve_xref(env, refdoc, app.builder, 'class',
                                                     newtarget, node, contnode)
            if newnode is not None:
                newnode['reftitle'] = reftarget
            return newnode


def setup(app):

    app.connect('autodoc-process-docstring', process_docstring)
    app.connect('missing-reference', missing_reference_handler)
    app.connect('env-merge-info', merge_mapping)

    return {'parallel_read_safe': True,
            'parallel_write_safe': True}