summaryrefslogtreecommitdiff
path: root/docs/builder.py
blob: 7a9432fdae1def9cff0a50c1bbdf98d54b58369d (plain)
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
from dataclasses import dataclass
from functools import cached_property

from sphinxcontrib.serializinghtml import JSONHTMLBuilder

IGNORED_DOMAIN_TYPES = {"module"}


@dataclass
class DomainObject:
    """
    A wrapper around sphinx's domain object descriptions
    https://www.sphinx-doc.org/en/master/extdev/domainapi.html#sphinx.domains.Domain.get_objects
    """

    name: str
    dispname: str
    type: str
    docname: str
    anchor: str
    priority: int

    @property
    def short_name(self) -> str:
        """
        Returns a shortened version of the object's name.

        - If the second-to-last part of the name starts with an uppercase letter
          (indicating a class method or class attribute), returns the last two parts.
        - Otherwise, returns only the last part (likely a function, or class).

        Examples:
        - "django.db.models.query.QuerySet.select_related" → "QuerySet.select_related"
        - "django.db.models.query.QuerySet" → "QuerySet"
        - "django.template.context_processors.static" → "static"
        """
        parts = self.name.split(".")

        if len(parts) < 2:
            return self.name

        last, second_last = parts[-1], parts[-2]
        return f"{second_last}.{last}" if second_last[0].isupper() else last


class PythonObjectsJSONHTMLBuilder(JSONHTMLBuilder):
    @cached_property
    def domain_objects(self):
        domain = self.env.get_domain("py")
        return [
            DomainObject(*item)
            for item in domain.get_objects()
            if item[2] not in IGNORED_DOMAIN_TYPES  # item[2] is 'type'.
        ]

    def get_doc_context(self, docname, body, metatags):
        out_dict = super().get_doc_context(docname, body, metatags)
        python_objects = self.get_python_objects(docname)
        out_dict["python_objects"] = python_objects
        out_dict["python_objects_search"] = " ".join(
            # Keeps the code suffix to improve the search results for terms such as
            # "select" for QuerySet.select_related.
            [key.split(".")[-1] for key in python_objects.keys()]
        )
        return out_dict

    def get_python_objects(self, docname):
        return {
            obj.short_name: obj.name
            for obj in self.domain_objects
            if obj.docname == docname
        }


def setup(app):
    app.add_builder(PythonObjectsJSONHTMLBuilder, override=True)

    # JSONHTMLBuilder marks parallel read/write as safe, so our implementation
    # should also handle that.
    return {
        "version": "0.1",
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }