Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Empty string enum rendering + Add configurable amount of choices to display #42

Merged
merged 1 commit into from
Jul 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Unreleased
----------

* [ `#39 <https://github.com/edoburu/sphinxcontrib-django/issues/39>`_ ] Fix table names of abstract models (`@insspb <https://github.com/insspb>`__)
* [ `#41 <https://github.com/edoburu/sphinxcontrib-django/issues/41>`_ ] Fix rendering of iterable choices (`@insspb <https://github.com/insspb>`__)


Version 2.3 (2023-04-12)
Expand Down
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ Optionally, you can include the table names of your models in their docstrings w
# Add abstract database tables names (only takes effect if django_show_db_tables is True)
django_show_db_tables_abstract = True # Boolean, default: False

Optionally, you can extend amount of displayed choices in model fields with them:

.. code-block:: python

# Integer amount of model field choices to show, default 10
django_choices_to_show = 10

Advanced Usage
--------------
Expand Down
9 changes: 5 additions & 4 deletions sphinxcontrib_django/docstrings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .. import __version__
from .attributes import improve_attribute_docstring
from .classes import improve_class_docstring
from .config import EXCLUDE_MEMBERS, INCLUDE_MEMBERS
from .config import CHOICES_LIMIT, EXCLUDE_MEMBERS, INCLUDE_MEMBERS
from .data import improve_data_docstring
from .methods import improve_method_docstring

Expand Down Expand Up @@ -64,7 +64,8 @@ def setup(app):
app.add_config_value("django_show_db_tables", False, True)
# Set default of django_show_db_tables_abstract to False
app.add_config_value("django_show_db_tables_abstract", False, True)

# Integer amount of model field choices to show
app.add_config_value("django_choices_to_show", CHOICES_LIMIT, True)
# Setup Django after config is initialized
app.connect("config-inited", setup_django)

Expand Down Expand Up @@ -111,7 +112,7 @@ def setup_django(app, config):
raise ConfigError(
"The module you specified in the configuration 'django_settings' in your"
" conf.py cannot be imported. Make sure the module path is correct and the"
" source directoy is added to sys.path."
" source directory is added to sys.path."
) from e
os.environ["DJANGO_SETTINGS_MODULE"] = config.django_settings
django.setup()
Expand Down Expand Up @@ -182,7 +183,7 @@ def improve_docstring(app, what, name, obj, options, lines):
if what == "class":
improve_class_docstring(app, obj, lines)
elif what == "attribute":
improve_attribute_docstring(obj, name, lines)
improve_attribute_docstring(app, obj, name, lines)
elif what == "method":
improve_method_docstring(name, lines)
elif what == "data":
Expand Down
36 changes: 23 additions & 13 deletions sphinxcontrib_django/docstrings/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from django.utils.module_loading import import_string
from sphinx.util.docstrings import prepare_docstring

from .config import CHOICES_LIMIT
from .field_utils import get_field_type, get_field_verbose_name

FIELD_DESCRIPTORS = (FileDescriptor, related_descriptors.ForwardManyToOneDescriptor)
Expand All @@ -23,12 +22,15 @@
PhoneNumberDescriptor = None


def improve_attribute_docstring(attribute, name, lines):
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

Expand Down Expand Up @@ -56,21 +58,21 @@ def improve_attribute_docstring(attribute, name, lines):
f"Internal field, use :class:`~{cls_path}.{field.name}` instead."
)
else:
lines.extend(get_field_details(field))
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(attribute.field))
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(attribute.rel))
lines.extend(get_field_details(app, attribute.rel))
else:
lines.extend(get_field_details(attribute.field))
lines.extend(get_field_details(app, attribute.field))
elif isinstance(attribute, related_descriptors.ReverseManyToOneDescriptor):
lines.extend(get_field_details(attribute.rel))
lines.extend(get_field_details(app, attribute.rel))
elif isinstance(attribute, related_descriptors.ReverseOneToOneDescriptor):
lines.extend(get_field_details(attribute.related))
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)
Expand All @@ -92,17 +94,22 @@ def improve_attribute_docstring(attribute, name, lines):
lines.extend(docstring_lines[:-1])


def get_field_details(field):
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)}",
"",
Expand All @@ -111,13 +118,16 @@ def get_field_details(field):
if hasattr(field, "choices") and field.choices:
field_details.extend(["", "Choices:", ""])
field_details.extend(
[f"* ``{key}``" for key, value in field.choices[:CHOICES_LIMIT]]
[
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 len(field.choices) > choices_limit:
# If only one element has been truncated, just list it as well
if len(field.choices) == CHOICES_LIMIT + 1:
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")
field_details.append(f"* and {len(field.choices) - choices_limit} more")
return field_details
3 changes: 2 additions & 1 deletion sphinxcontrib_django/docstrings/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"polymorphic_super_sub_accessors_replaced",
}

#: How many choices should be shown for model fields
#: How many choices should be shown for model fields by default,
#: used as default for ``django_choices_to_show`` option
CHOICES_LIMIT = 10
3 changes: 3 additions & 0 deletions tests/roots/test-docstrings/dummy_django_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ class ChoiceModel(models.Model):
choice_limit_above = models.IntegerField(
choices=[(i, i) for i in range(CHOICES_LIMIT + 2)]
)
choice_with_empty = models.CharField(
choices=[("", "Empty"), ("Something", "Not empty")]
)


class TaggedItem(models.Model):
Expand Down
58 changes: 58 additions & 0 deletions tests/test_attribute_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,64 @@ def test_choice_field_limit_above(app, do_autodoc):
]


@pytest.mark.sphinx(
"html", testroot="docstrings", confoverrides={"django_choices_to_show": 15}
)
def test_choice_field_custom_limit(app, do_autodoc):
actual = do_autodoc(
app, "attribute", "dummy_django_app.models.ChoiceModel.choice_limit_above"
)
print(actual)
assert list(actual) == [
"",
".. py:attribute:: ChoiceModel.choice_limit_above",
" :module: dummy_django_app.models",
"",
" Type: :class:`~django.db.models.IntegerField`",
"",
" Choice limit above",
"",
" Choices:",
"",
" * ``0``",
" * ``1``",
" * ``2``",
" * ``3``",
" * ``4``",
" * ``5``",
" * ``6``",
" * ``7``",
" * ``8``",
" * ``9``",
" * ``10``",
" * ``11``",
"",
]


@pytest.mark.sphinx("html", testroot="docstrings")
def test_choice_field_empty(app, do_autodoc):
actual = do_autodoc(
app, "attribute", "dummy_django_app.models.ChoiceModel.choice_with_empty"
)
print(actual)
assert list(actual) == [
"",
".. py:attribute:: ChoiceModel.choice_with_empty",
" :module: dummy_django_app.models",
"",
" Type: :class:`~django.db.models.CharField`",
"",
" Choice with empty",
"",
" Choices:",
"",
" * ``''`` (Empty string)",
" * ``Something``",
"",
]


if PHONENUMBER:

@pytest.mark.sphinx("html", testroot="docstrings")
Expand Down
Loading