File: classes.py

package info (click to toggle)
python-sphinxcontrib-django 2.5-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 636 kB
  • sloc: python: 1,450; makefile: 20; sh: 6
file content (167 lines) | stat: -rw-r--r-- 5,828 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
"""
This module contains all functions which are used to improve the documentation of classes.
"""
from __future__ import annotations

from django import forms
from django.db import models
from sphinx.application import Sphinx
from sphinx.pycode import ModuleAnalyzer

from .field_utils import get_field_type, get_field_verbose_name


def improve_class_docstring(app: Sphinx, cls: type, lines: list[str]) -> None:
    """
    Improve the documentation of a class if it's a Django model or form

    :param app: The Sphinx application object
    :param cls: The instance of the class to document
    :param lines: The docstring lines
    """
    if issubclass(cls, models.Model):
        improve_model_docstring(app, cls, lines)
    elif issubclass(cls, forms.BaseForm):
        improve_form_docstring(cls, lines)


def improve_model_docstring(app: Sphinx, model: models.Model, lines: list[str]) -> None:
    """
    Improve the documentation of a Django :class:`~django.db.models.Model` subclass.

    This adds all model fields as parameters to the ``__init__()`` method.

    :param app: The Sphinx application object
    :param model: The instance of the model to document
    :param lines: The docstring lines
    """

    # Add database table name
    if app.config.django_show_db_tables:
        add_db_table_name(app, model, lines)

    # Get predefined params to exclude them from the automatically inserted params
    param_offset = len(":param ")
    predefined_params = [
        line[param_offset : line.find(":", param_offset)]
        for line in lines
        if line.startswith(":param ") and ":" in line[param_offset:]
    ]

    # Get all fields of this model which are not already explicitly included in the docstring
    all_fields = [
        field
        for field in model._meta.get_fields(include_parents=True)
        if field.name not in predefined_params
    ]
    # Get all related fields (ForeignKey, OneToOneField, ManyToManyField)
    related_fields = [
        field
        for field in all_fields
        if isinstance(field, models.fields.related.RelatedField)
    ]
    # Get all reverse relationships
    reverse_related_fields = [
        field
        for field in all_fields
        if isinstance(field, models.fields.reverse_related.ForeignObjectRel)
    ]
    # All fields which are neither related nor reverse related
    non_related_fields = [
        field
        for field in all_fields
        if field not in related_fields + reverse_related_fields
    ]

    # Analyze model to get inline field docstrings
    analyzer = ModuleAnalyzer.for_module(model.__module__)
    analyzer.analyze()
    field_docs = {
        field_name: field_docstring
        for (_, field_name), field_docstring in analyzer.attr_docs.items()
    }

    # Add the normal fields to the docstring
    add_model_parameters(non_related_fields, lines, field_docs)

    # Add the related fields
    if related_fields:
        lines.append("")
        lines.append("Relationship fields:")
        lines.append("")
        add_model_parameters(related_fields, lines, field_docs)

    # Add the reverse related fields
    if reverse_related_fields:
        lines.append("")
        lines.append("Reverse relationships:")
        lines.append("")
        add_model_parameters(reverse_related_fields, lines, field_docs)

    # Add the inheritance diagram
    if (
        "sphinx.ext.inheritance_diagram" in app.extensions
        and "sphinx.ext.graphviz" in app.extensions
        and not any("inheritance-diagram::" in line for line in lines)
    ):
        lines.append("")
        lines.append(f".. inheritance-diagram:: {model.__module__}.{model.__name__}")
        lines.append("")


def add_db_table_name(app: Sphinx, model: models.Model, lines: list[str]) -> None:
    """
    Format and add table name by extension configuration.

    :param app: The Sphinx application object
    :param model: The instance of the model to document
    :param lines: The docstring lines
    """
    if model._meta.abstract and not app.config.django_show_db_tables_abstract:
        return

    table_name = None if model._meta.abstract else model._meta.db_table
    lines.insert(0, "")
    lines.insert(0, f"**Database table:** ``{table_name}``")


def add_model_parameters(
    fields: list[models.Field], lines: list[str], field_docs: dict
) -> None:
    """
    Add the given fields as model parameter with the ``:param:`` directive

    :param fields: The list of fields
    :param lines: The list of current docstring lines
    :param field_docs: The attribute docstrings of the model
    """
    for field in fields:
        # Add docstrings if they are found
        docstring_lines = field_docs.get(field.name, [])
        # Add param doc line
        param = f":param {field.name}: "
        lines.append(param + get_field_verbose_name(field))
        if docstring_lines:
            # Separate from verbose name
            lines.append("")
        # Add and indent existing docstring lines
        lines.extend([(" " * len(param)) + line for line in docstring_lines])

        # Add type
        lines.append(f":type {field.name}: {get_field_type(field, include_role=False)}")


def improve_form_docstring(form: forms.Form, lines: list[str]) -> None:
    """
    Improve the documentation of a Django :class:`~django.forms.Form` class.
    This highlights the available fields in the form.

    :param form: The form object
    :param lines: The list of existing docstring lines
    """
    lines.append("**Form fields:**")
    lines.append("")
    for name, field in form.base_fields.items():
        field_type = f"{field.__class__.__module__}.{field.__class__.__name__}"
        label = field.label or name.replace("_", " ").title()
        lines.append(f"* ``{name}``: {label} (:class:`~{field_type}`)")