File: motor_extensions.py

package info (click to toggle)
python-motor 3.7.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,572 kB
  • sloc: python: 12,252; javascript: 137; makefile: 74; sh: 8
file content (180 lines) | stat: -rw-r--r-- 6,790 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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# Copyright 2012-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Motor specific extensions to Sphinx."""

import re

from docutils.nodes import doctest_block, literal_block
from sphinx import addnodes
from sphinx.addnodes import desc, desc_content, desc_signature, seealso, versionmodified
from sphinx.util.inspect import safe_getattr

import motor
import motor.core

# This is a place to store info while parsing, to be used before generating.
motor_info = {}


def has_node_of_type(root, klass):
    if isinstance(root, klass):
        return True

    for child in root.children:  # noqa: SIM110
        if has_node_of_type(child, klass):
            return True

    return False


def find_by_path(root, classes):
    if not classes:
        return [root]

    _class = classes[0]
    rv = []
    for child in root.children:
        if isinstance(child, _class):
            rv.extend(find_by_path(child, classes[1:]))

    return rv


docstring_warnings = []


def maybe_warn_about_code_block(name, content_node):
    if has_node_of_type(content_node, (literal_block, doctest_block)):
        docstring_warnings.append(name)


def has_coro_annotation(signature_node):
    try:
        return "coroutine" in signature_node[0][0]
    except IndexError:
        return False


def process_motor_nodes(app, doctree):
    # Search doctree for Motor's methods and attributes whose docstrings were
    # copied from PyMongo, and fix them up for Motor:
    #   1. Add a 'coroutine' annotation to the beginning of the declaration.
    #   2. Remove all version annotations like "New in version 2.0" since
    #      PyMongo's version numbers are meaningless in Motor's docs.
    #   3. Remove "seealso" directives that reference PyMongo's docs.
    #
    # We do this here, rather than by registering a callback to Sphinx's
    # 'autodoc-process-signature' event, because it's way easier to handle the
    # parsed doctree before it's turned into HTML than it is to update the RST.
    for objnode in doctree.traverse(desc):
        if objnode["objtype"] in ("method", "attribute"):
            signature_node = find_by_path(objnode, [desc_signature])[0]
            name = ".".join([signature_node["module"], signature_node["fullname"]])

            assert name.startswith("motor.")
            obj_motor_info = motor_info.get(name)
            if obj_motor_info:
                desc_content_node = find_by_path(objnode, [desc_content])[0]
                if desc_content_node.line is None and obj_motor_info["is_pymongo_docstring"]:
                    maybe_warn_about_code_block(name, desc_content_node)

                if obj_motor_info["is_async_method"]:  # noqa: SIM102
                    # Might be a handwritten RST with "coroutine" already.
                    if not has_coro_annotation(signature_node):
                        coro_annotation = addnodes.desc_annotation(
                            "coroutine ", "coroutine ", classes=["coro-annotation"]
                        )

                        signature_node.insert(0, coro_annotation)

                if obj_motor_info["is_pymongo_docstring"]:
                    # Remove all "versionadded", "versionchanged" and
                    # "deprecated" directives from the docs we imported from
                    # PyMongo
                    version_nodes = find_by_path(desc_content_node, [versionmodified])

                    for version_node in version_nodes:
                        version_node.parent.remove(version_node)

                    # Remove all "seealso" directives that contain :doc:
                    # references from PyMongo's docs
                    seealso_nodes = find_by_path(desc_content_node, [seealso])

                    for seealso_node in seealso_nodes:
                        if 'reftype="doc"' in str(seealso_node):
                            seealso_node.parent.remove(seealso_node)


def get_motor_attr(motor_class, name, *defargs):
    """If any Motor attributes can't be accessed, grab the equivalent PyMongo
    attribute. While we're at it, store some info about each attribute
    in the global motor_info dict.
    """
    attr = safe_getattr(motor_class, name, *defargs)

    # Store some info for process_motor_nodes()
    full_name = f"{motor_class.__module__}.{motor_class.__name__}.{name}"

    full_name_legacy = f"motor.{motor_class.__module__}.{motor_class.__name__}.{name}"

    # These sub-attributes are set in motor.asynchronize()
    has_coroutine_annotation = getattr(attr, "coroutine_annotation", False)
    is_async_method = getattr(attr, "is_async_method", False)
    is_cursor_method = getattr(attr, "is_motorcursor_chaining_method", False)
    if is_async_method or is_cursor_method:
        pymongo_method = getattr(motor_class.__delegate_class__, attr.pymongo_method_name)
    else:
        pymongo_method = None

    # attr.doc is set by statement like 'error = AsyncRead(doc="OBSOLETE")'.
    is_pymongo_doc = pymongo_method and attr.__doc__ == pymongo_method.__doc__

    motor_info[full_name] = motor_info[full_name_legacy] = {
        "is_async_method": is_async_method or has_coroutine_annotation,
        "is_pymongo_docstring": is_pymongo_doc,
        "pymongo_method": pymongo_method,
    }

    return attr


pymongo_ref_pat = re.compile(r":doc:`(.*?)`", re.MULTILINE)


def _sub_pymongo_ref(match):
    ref = match.group(1)
    return ":doc:`%s`" % ref.lstrip("/")


def process_motor_docstring(app, what, name, obj, options, lines):
    if name in motor_info and motor_info[name].get("is_pymongo_docstring"):
        joined = "\n".join(lines)
        subbed = pymongo_ref_pat.sub(_sub_pymongo_ref, joined)
        lines[:] = subbed.split("\n")


def build_finished(app, exception):
    if not exception and docstring_warnings:
        print("PyMongo docstrings with code blocks that need update:")
        for name in sorted(docstring_warnings):
            print(name)


def setup(app):
    app.add_autodoc_attrgetter(type(motor.core.AgnosticBase), get_motor_attr)
    app.connect("autodoc-process-docstring", process_motor_docstring)
    app.connect("doctree-read", process_motor_nodes)
    app.connect("build-finished", build_finished)
    return {"parallel_write_safe": True, "parallel_read_safe": False}