Fixed #30255 -- Fixed admindocs errors when rendering docstrings without leading newlines.

Used inspect.cleandoc() which implements PEP-257 instead of an internal
hook.
This commit is contained in:
Baptiste Mispelon 2019-11-28 17:10:20 +01:00 committed by Mariusz Felisiak
parent e8fcdaad5c
commit f47ba7e780
3 changed files with 15 additions and 31 deletions

View File

@ -3,6 +3,7 @@
import re import re
from email.errors import HeaderParseError from email.errors import HeaderParseError
from email.parser import HeaderParser from email.parser import HeaderParser
from inspect import cleandoc
from django.urls import reverse from django.urls import reverse
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
@ -24,26 +25,13 @@ def get_view_name(view_func):
return mod_name + '.' + view_name return mod_name + '.' + view_name
def trim_docstring(docstring):
"""
Uniformly trim leading/trailing whitespace from docstrings.
Based on https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
"""
if not docstring or not docstring.strip():
return ''
# Convert tabs to spaces and split into lines
lines = docstring.expandtabs().splitlines()
indent = min(len(line) - len(line.lstrip()) for line in lines if line.lstrip())
trimmed = [lines[0].lstrip()] + [line[indent:].rstrip() for line in lines[1:]]
return "\n".join(trimmed).strip()
def parse_docstring(docstring): def parse_docstring(docstring):
""" """
Parse out the parts of a docstring. Return (title, body, metadata). Parse out the parts of a docstring. Return (title, body, metadata).
""" """
docstring = trim_docstring(docstring) if not docstring:
return '', '', {}
docstring = cleandoc(docstring)
parts = re.split(r'\n{2,}', docstring) parts = re.split(r'\n{2,}', docstring)
title = parts[0] title = parts[0]
if len(parts) == 1: if len(parts) == 1:

View File

@ -1,5 +1,6 @@
import inspect import inspect
from importlib import import_module from importlib import import_module
from inspect import cleandoc
from pathlib import Path from pathlib import Path
from django.apps import apps from django.apps import apps
@ -256,7 +257,7 @@ class ModelDetailView(BaseAdminDocsView):
continue continue
verbose = func.__doc__ verbose = func.__doc__
verbose = verbose and ( verbose = verbose and (
utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name) utils.parse_rst(cleandoc(verbose), 'model', _('model:') + opts.model_name)
) )
# Show properties and methods without arguments as fields. # Show properties and methods without arguments as fields.
# Otherwise, show as a 'method with arguments'. # Otherwise, show as a 'method with arguments'.

View File

@ -1,8 +1,9 @@
import unittest import unittest
from django.contrib.admindocs.utils import ( from django.contrib.admindocs.utils import (
docutils_is_available, parse_docstring, parse_rst, trim_docstring, docutils_is_available, parse_docstring, parse_rst,
) )
from django.test.utils import captured_stderr
from .tests import AdminDocsSimpleTestCase from .tests import AdminDocsSimpleTestCase
@ -31,19 +32,6 @@ class TestUtils(AdminDocsSimpleTestCase):
def setUp(self): def setUp(self):
self.docstring = self.__doc__ self.docstring = self.__doc__
def test_trim_docstring(self):
trim_docstring_output = trim_docstring(self.docstring)
trimmed_docstring = (
'This __doc__ output is required for testing. I copied this '
'example from\n`admindocs` documentation. (TITLE)\n\n'
'Display an individual :model:`myapp.MyModel`.\n\n'
'**Context**\n\n``RequestContext``\n\n``mymodel``\n'
' An instance of :model:`myapp.MyModel`.\n\n'
'**Template:**\n\n:template:`myapp/my_template.html` '
'(DESCRIPTION)\n\nsome_metadata: some data'
)
self.assertEqual(trim_docstring_output, trimmed_docstring)
def test_parse_docstring(self): def test_parse_docstring(self):
title, description, metadata = parse_docstring(self.docstring) title, description, metadata = parse_docstring(self.docstring)
docstring_title = ( docstring_title = (
@ -106,6 +94,13 @@ class TestUtils(AdminDocsSimpleTestCase):
self.assertEqual(parse_rst('`title`', 'filter'), markup % 'filters/#title') self.assertEqual(parse_rst('`title`', 'filter'), markup % 'filters/#title')
self.assertEqual(parse_rst('`title`', 'tag'), markup % 'tags/#title') self.assertEqual(parse_rst('`title`', 'tag'), markup % 'tags/#title')
def test_parse_rst_with_docstring_no_leading_line_feed(self):
title, body, _ = parse_docstring('firstline\n\n second line')
with captured_stderr() as stderr:
self.assertEqual(parse_rst(title, ''), '<p>firstline</p>\n')
self.assertEqual(parse_rst(body, ''), '<p>second line</p>\n')
self.assertEqual(stderr.getvalue(), '')
def test_publish_parts(self): def test_publish_parts(self):
""" """
Django shouldn't break the default role for interpreted text Django shouldn't break the default role for interpreted text