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
|
"""
This module contains utility functions for fields which are used by both the
:mod:`~sphinxcontrib_django.docstrings.attributes` and
:mod:`~sphinxcontrib_django.docstrings.classes` modules.
"""
from __future__ import annotations
from django.apps import apps
from django.contrib import contenttypes
from django.db import models
from django.utils.encoding import force_str
def get_field_type(field: models.Field, include_role: bool = True) -> str:
"""
Get the type of a field including the correct intersphinx mappings.
:param field: The field
:param include_directive: Whether or not the role :any:`py:class` should be included
:return: The type of the field
"""
if isinstance(field, models.fields.related.RelatedField):
to = field.remote_field.model
if isinstance(to, str):
# This happens with foreign keys of abstract models
to = get_model_from_string(field, to)
return (
f":class:`~{type(field).__module__}.{type(field).__name__}` to"
f" :class:`~{to.__module__}.{to.__name__}`"
)
if isinstance(field, models.fields.reverse_related.ForeignObjectRel):
to = field.remote_field.model
return (
"Reverse"
f" :class:`~{type(field.remote_field).__module__}.{type(field.remote_field).__name__}`"
f" from :class:`~{to.__module__}.{to.__name__}`"
)
if include_role:
# For the docstrings of attributes, the :class: role is required
return f":class:`~{type(field).__module__}.{type(field).__name__}`"
# For the :param: role in class docstrings, the :class: role is not required
return f"~{type(field).__module__}.{type(field).__name__}"
def get_field_verbose_name(field: models.Field) -> str:
"""
Get the verbose name of the field.
If the field has a ``help_text``, it is also included.
In case the field is a related field, the ``related_name`` is used to link to the remote model.
For reverse related fields, the originating field is linked.
:param field: The field
"""
help_text = ""
# Check whether the field is a reverse related field
if isinstance(field, models.fields.reverse_related.ForeignObjectRel):
# Convert related name to a readable name if ``snake_case`` is used
related_name = (
field.related_name.replace("_", " ") if field.related_name else None
)
if isinstance(field, models.fields.reverse_related.OneToOneRel):
# If a related name is given, use it, else use the verbose name of the remote model
related_name = related_name or field.remote_field.model._meta.verbose_name
# If field is a OneToOne field, use the prefix "The"
verbose_name = (
f"The {related_name} of this {field.model._meta.verbose_name}"
)
else:
# This means field is an instance of ManyToOneRel or ManyToManyRel
# If a related name is given, use it, else use the verbose name of the remote model
related_name = (
related_name or field.remote_field.model._meta.verbose_name_plural
)
# If field is a foreign key or a ManyToMany field, use the prefix "All"
verbose_name = (
f"All {related_name} of this {field.model._meta.verbose_name}"
)
# Link to the origin of the reverse related field if it's not from an abstract model
if not field.remote_field.model._meta.abstract:
verbose_name += (
f" (related name of :attr:`~{field.remote_field.model.__module__}"
f".{field.remote_field.model.__name__}.{field.remote_field.name}`)"
)
elif hasattr(contenttypes, "fields") and isinstance(
field, contenttypes.fields.GenericForeignKey
):
# GenericForeignKey does not inherit from django.db.models.Field and has no verbose_name
return (
"Generic foreign key to the"
" :class:`~django.contrib.contenttypes.models.ContentType` specified in"
f" :attr:`~{field.model.__module__}.{field.model.__name__}.{field.ct_field}`"
)
else:
# This means the field is either a normal field or a forward related field
# If the field is a primary key, include a notice
primary_key = "Primary key: " if field.primary_key else ""
field_verbose_name = force_str(field.verbose_name)
# Make the first letter upper case while leave the rest unchanged
# (str.capitalize() would make the rest lower case, e.g. ID => Id)
verbose_name = (
primary_key + field_verbose_name[:1].upper() + field_verbose_name[1:]
)
help_text = force_str(field.help_text)
# Add help text if field has one
if help_text:
# Separate verbose name and help text by a dot
if not verbose_name.endswith("."):
verbose_name += ". "
verbose_name += help_text
if isinstance(field, models.fields.related.RelatedField):
# If field is a forward related field, reference the remote model
to = field.remote_field.model
if isinstance(to, str):
# This happens with foreign keys of abstract models
to = get_model_from_string(field, to)
# If a related name is defined
if hasattr(field.remote_field, "related_name"):
related_name = (
field.remote_field.related_name or field.model.__name__.lower()
)
# Link to the related field if it's not an abstract model
if not field.model._meta.abstract:
verbose_name += (
" (related name:"
f" :attr:`~{to.__module__}.{to.__name__}.{related_name}`)"
)
return verbose_name
def get_model_from_string(field: models.Field, model_string: str) -> type[models.Model]:
"""
Get a model class from a string
:param field: The field
:param model_string: The string label of the model
:return: The class of the model
"""
if "." in model_string:
model = apps.get_model(model_string)
elif model_string == "self":
model = field.model
else:
model = apps.get_model(field.model._meta.app_label, model_string)
return model
|