File: attributes.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 (135 lines) | stat: -rw-r--r-- 5,723 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
"""
This module contains all functions which are used to improve the documentation of attributes.
"""
from __future__ import annotations

from django.db import models
from django.db.models.fields import related_descriptors
from django.db.models.fields.files import FileDescriptor
from django.db.models.manager import ManagerDescriptor
from django.db.models.query_utils import DeferredAttribute
from django.utils.module_loading import import_string
from sphinx.util.docstrings import prepare_docstring

from .field_utils import get_field_type, get_field_verbose_name

FIELD_DESCRIPTORS = (FileDescriptor, related_descriptors.ForwardManyToOneDescriptor)

# Support for some common third party fields
try:
    from phonenumber_field.modelfields import PhoneNumberDescriptor

    FIELD_DESCRIPTORS += (PhoneNumberDescriptor,)
except ImportError:
    PhoneNumberDescriptor = None


def improve_attribute_docstring(app, attribute, name, lines):
    """
    Improve the documentation of various model fields.

    This improves the navigation between related objects.

    :param app: The Sphinx application object
    :type app: ~sphinx.application.Sphinx

    :param attribute: The instance of the object to document
    :type attribute: object

    :param name: The full dotted path to the object
    :type name: str

    :param lines: The docstring lines
    :type lines: list [ str ]
    """
    # Save initial docstring lines to append them to the modified lines
    docstring_lines = lines.copy()
    lines.clear()
    if isinstance(attribute, DeferredAttribute):
        # This only points to a field name, not a field.
        # Get the field by importing the name.
        cls_path, field_name = name.rsplit(".", 1)
        model = import_string(cls_path)
        field = model._meta.get_field(field_name)
        if isinstance(field, models.fields.related.RelatedField):
            # If a deferred attribute is a related field, it is an automatically created field
            # with the postfix "_id" and contains the reference to the id of the related model
            # instance. These are usually undocumented, so they only are included in the docs
            # is sphinx is invoked with the undoc-members option.
            lines.append(
                f"Internal field, use :class:`~{cls_path}.{field.name}` instead."
            )
        else:
            lines.extend(get_field_details(app, field))
    elif isinstance(attribute, FIELD_DESCRIPTORS):
        # Display a reasonable output for forward descriptors (foreign key and one to one fields).
        lines.extend(get_field_details(app, attribute.field))
    elif isinstance(attribute, related_descriptors.ManyToManyDescriptor):
        # Check this case first since ManyToManyDescriptor inherits from ReverseManyToOneDescriptor
        # This descriptor is used for both forward and reverse relationships
        if attribute.reverse:
            lines.extend(get_field_details(app, attribute.rel))
        else:
            lines.extend(get_field_details(app, attribute.field))
    elif isinstance(attribute, related_descriptors.ReverseManyToOneDescriptor):
        lines.extend(get_field_details(app, attribute.rel))
    elif isinstance(attribute, related_descriptors.ReverseOneToOneDescriptor):
        lines.extend(get_field_details(app, attribute.related))
    elif isinstance(attribute, (models.Manager, ManagerDescriptor)):
        # Somehow the 'objects' manager doesn't pass through the docstrings.
        module, model_name, field_name = name.rsplit(".", 2)
        lines.append("Django manager to access the ORM")
        lines.append(f"Use ``{model_name}.objects.all()`` to fetch all objects.")
    # Check if there are initial docstrings to be appended
    if docstring_lines:
        # Get default docstring of attribute
        parent_docstring = type(attribute).__doc__
        # Ignore non-string __doc__
        if not isinstance(parent_docstring, str):
            parent_docstring = ""
        # Only append the initial docstring of the attribute if it's overwritten
        if docstring_lines != prepare_docstring(parent_docstring) or not lines:
            if lines:
                # If lines are not empty, append a separating new line before docstring
                lines.append("")
            # Remove last element because it's a newline
            lines.extend(docstring_lines[:-1])


def get_field_details(app, field):
    """
    This function returns the detail docstring of a model field.
    It includes the field type and the verbose name of the field.

    :param app: The Sphinx application object
    :type app: ~sphinx.application.Sphinx

    :param field: The field
    :type field: ~django.db.models.Field

    :return: The field details as list of strings
    :rtype: list [ str ]
    """
    choices_limit = app.config.django_choices_to_show

    field_details = [
        f"Type: {get_field_type(field)}",
        "",
        f"{get_field_verbose_name(field)}",
    ]
    if hasattr(field, "choices") and field.choices:
        field_details.extend(["", "Choices:", ""])
        field_details.extend(
            [
                f"* ``{key}``" if key != "" else "* ``''`` (Empty string)"
                for key, value in field.choices[:choices_limit]
            ]
        )
        # Check if list has been truncated
        if len(field.choices) > choices_limit:
            # If only one element has been truncated, just list it as well
            if len(field.choices) == choices_limit + 1:
                field_details.append(f"* ``{field.choices[-1][0]}``")
            else:
                field_details.append(f"* and {len(field.choices) - choices_limit} more")
    return field_details