Fixed #24917 -- Made admindocs display model methods that take arguments.

This commit is contained in:
Zan Anderle 2014-10-30 20:45:01 +01:00 committed by Tim Graham
parent 3c5862ccb0
commit f3dc173240
8 changed files with 201 additions and 14 deletions

View File

@ -27,6 +27,7 @@
{{ description }} {{ description }}
<h3>{% trans 'Fields' %}</h3>
<div class="module"> <div class="module">
<table class="model"> <table class="model">
<thead> <thead>
@ -48,6 +49,30 @@
</table> </table>
</div> </div>
{% if methods %}
<h3>{% trans 'Methods with arguments' %}</h3>
<div class="module">
<table class="model">
<thead>
<tr>
<th>{% trans 'Method' %}</th>
<th>{% trans 'Arguments' %}</th>
<th>{% trans 'Description' %}</th>
</tr>
</thead>
<tbody>
{% for method in methods|dictsort:"name" %}
<tr>
<td>{{ method.name }}</td>
<td>{{ method.arguments }}</td>
<td>{{ method.verbose }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<p class="small"><a href="{% url 'django-admindocs-models-index' %}">&lsaquo; {% trans 'Back to Model Documentation' %}</a></p> <p class="small"><a href="{% url 'django-admindocs-models-index' %}">&lsaquo; {% trans 'Back to Model Documentation' %}</a></p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -14,7 +14,10 @@ from django.db import models
from django.http import Http404 from django.http import Http404
from django.template.engine import Engine from django.template.engine import Engine
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.inspect import func_has_no_args from django.utils.inspect import (
func_accepts_kwargs, func_accepts_var_args, func_has_no_args,
get_func_full_args,
)
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
@ -219,7 +222,7 @@ class ModelDetailView(BaseAdminDocsView):
fields.append({ fields.append({
'name': field.name, 'name': field.name,
'data_type': data_type, 'data_type': data_type,
'verbose': verbose, 'verbose': verbose or '',
'help_text': field.help_text, 'help_text': field.help_text,
}) })
@ -242,9 +245,10 @@ class ModelDetailView(BaseAdminDocsView):
'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name), 'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name),
}) })
methods = []
# Gather model methods. # Gather model methods.
for func_name, func in model.__dict__.items(): for func_name, func in model.__dict__.items():
if inspect.isfunction(func) and func_has_no_args(func): if inspect.isfunction(func):
try: try:
for exclude in MODEL_METHODS_EXCLUDE: for exclude in MODEL_METHODS_EXCLUDE:
if func_name.startswith(exclude): if func_name.startswith(exclude):
@ -254,11 +258,29 @@ class ModelDetailView(BaseAdminDocsView):
verbose = func.__doc__ verbose = func.__doc__
if verbose: if verbose:
verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name) verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name)
fields.append({ # If a method has no arguments, show it as a 'field', otherwise
'name': func_name, # as a 'method with arguments'.
'data_type': get_return_data_type(func_name), if func_has_no_args(func) and not func_accepts_kwargs(func) and not func_accepts_var_args(func):
'verbose': verbose, fields.append({
}) 'name': func_name,
'data_type': get_return_data_type(func_name),
'verbose': verbose or '',
})
else:
arguments = get_func_full_args(func)
print_arguments = arguments
# Join arguments with ', ' and in case of default value,
# join it with '='. Use repr() so that strings will be
# correctly displayed.
print_arguments = ', '.join([
'='.join(list(arg_el[:1]) + [repr(el) for el in arg_el[1:]])
for arg_el in arguments
])
methods.append({
'name': func_name,
'arguments': print_arguments,
'verbose': verbose or '',
})
# Gather related objects # Gather related objects
for rel in opts.related_objects: for rel in opts.related_objects:
@ -282,6 +304,7 @@ class ModelDetailView(BaseAdminDocsView):
'summary': title, 'summary': title,
'description': body, 'description': body,
'fields': fields, 'fields': fields,
'methods': methods,
}) })
return super(ModelDetailView, self).get_context_data(**kwargs) return super(ModelDetailView, self).get_context_data(**kwargs)

View File

@ -43,6 +43,44 @@ def get_func_args(func):
] ]
def get_func_full_args(func):
"""
Return a list of (argument name, default value) tuples. If the argument
does not have a default value, omit it in the tuple. Arguments such as
*args and **kwargs are also included.
"""
if six.PY2:
argspec = inspect.getargspec(func)
args = argspec.args[1:] # ignore 'self'
defaults = argspec.defaults or []
# Split args into two lists depending on whether they have default value
no_default = args[:len(args) - len(defaults)]
with_default = args[len(args) - len(defaults):]
# Join the two lists and combine it with default values
args = [(arg,) for arg in no_default] + zip(with_default, defaults)
# Add possible *args and **kwargs and prepend them with '*' or '**'
varargs = [('*' + argspec.varargs,)] if argspec.varargs else []
kwargs = [('**' + argspec.keywords,)] if argspec.keywords else []
return args + varargs + kwargs
sig = inspect.signature(func)
args = []
for arg_name, param in sig.parameters.items():
name = arg_name
# Ignore 'self'
if name == 'self':
continue
if param.kind == inspect.Parameter.VAR_POSITIONAL:
name = '*' + name
elif param.kind == inspect.Parameter.VAR_KEYWORD:
name = '**' + name
if param.default != inspect.Parameter.empty:
args.append((name, param.default))
else:
args.append((name,))
return args
def func_accepts_kwargs(func): def func_accepts_kwargs(func):
if six.PY2: if six.PY2:
# Not all callables are inspectable with getargspec, so we'll # Not all callables are inspectable with getargspec, so we'll
@ -64,10 +102,23 @@ def func_accepts_kwargs(func):
) )
def func_accepts_var_args(func):
"""
Return True if function 'func' accepts positional arguments *args.
"""
if six.PY2:
return inspect.getargspec(func)[1] is not None
return any(
p for p in inspect.signature(func).parameters.values()
if p.kind == p.VAR_POSITIONAL
)
def func_has_no_args(func): def func_has_no_args(func):
args = inspect.getargspec(func)[0] if six.PY2 else [ args = inspect.getargspec(func)[0] if six.PY2 else [
p for p in inspect.signature(func).parameters.values() p for p in inspect.signature(func).parameters.values()
if p.kind == p.POSITIONAL_OR_KEYWORD and p.default is p.empty if p.kind == p.POSITIONAL_OR_KEYWORD
] ]
return len(args) == 1 return len(args) == 1

View File

@ -60,10 +60,15 @@ Model reference
=============== ===============
The **models** section of the ``admindocs`` page describes each model in the The **models** section of the ``admindocs`` page describes each model in the
system along with all the fields and methods (without any arguments) available system along with all the fields and methods available on it. Relationships
on it. While model properties don't have any arguments, they are not listed. to other models appear as hyperlinks. Descriptions are pulled from ``help_text``
Relationships to other models appear as hyperlinks. Descriptions are pulled attributes on fields or from docstrings on model methods.
from ``help_text`` attributes on fields or from docstrings on model methods.
.. versionchanged:: 1.9
The **models** section of the ``admindocs`` now describes methods that take
arguments as well. In previous versions it was restricted to methods
without arguments.
A model with useful documentation might look like this:: A model with useful documentation might look like this::

View File

@ -163,6 +163,12 @@ Minor features
* JavaScript slug generation now supports Romanian characters. * JavaScript slug generation now supports Romanian characters.
:mod:`django.contrib.admindocs`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* The model section of the ``admindocs`` now also describes methods that take
arguments, rather than ignoring them.
:mod:`django.contrib.auth` :mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -44,6 +44,17 @@ class Person(models.Model):
def _get_full_name(self): def _get_full_name(self):
return "%s %s" % (self.first_name, self.last_name) return "%s %s" % (self.first_name, self.last_name)
def rename_company(self, new_name):
self.company.name = new_name
self.company.save()
return new_name
def dummy_function(self, baz, rox, *some_args, **some_kwargs):
return some_kwargs
def suffix_company_name(self, suffix='ltd'):
return self.company.name + suffix
def add_image(self): def add_image(self):
pass pass

View File

@ -246,7 +246,7 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
def setUp(self): def setUp(self):
self.client.login(username='super', password='secret') self.client.login(username='super', password='secret')
with captured_stderr() as self.docutils_stderr: with captured_stderr() as self.docutils_stderr:
self.response = self.client.get(reverse('django-admindocs-models-detail', args=['admin_docs', 'person'])) self.response = self.client.get(reverse('django-admindocs-models-detail', args=['admin_docs', 'Person']))
def test_method_excludes(self): def test_method_excludes(self):
""" """
@ -261,6 +261,34 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
self.assertNotContains(self.response, "<td>set_status</td>") self.assertNotContains(self.response, "<td>set_status</td>")
self.assertNotContains(self.response, "<td>save_changes</td>") self.assertNotContains(self.response, "<td>save_changes</td>")
def test_methods_with_arguments(self):
"""
Methods that take arguments should also displayed.
"""
self.assertContains(self.response, "<h3>Methods with arguments</h3>")
self.assertContains(self.response, "<td>rename_company</td>")
self.assertContains(self.response, "<td>dummy_function</td>")
self.assertContains(self.response, "<td>suffix_company_name</td>")
def test_methods_with_arguments_display_arguments(self):
"""
Methods with arguments should have their arguments displayed.
"""
self.assertContains(self.response, "<td>new_name</td>")
def test_methods_with_arguments_display_arguments_default_value(self):
"""
Methods with keyword arguments should have their arguments displayed.
"""
self.assertContains(self.response, "<td>suffix=&#39;ltd&#39;</td>")
def test_methods_with_multiple_arguments_display_arguments(self):
"""
Methods with multiple arguments should have all their arguments
displayed, but omitting 'self'.
"""
self.assertContains(self.response, "<td>baz, rox, *some_args, **some_kwargs</td>")
def test_method_data_types(self): def test_method_data_types(self):
""" """
We should be able to get a basic idea of the type returned We should be able to get a basic idea of the type returned
@ -368,6 +396,9 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
self.assertContains(self.response, body, html=True) self.assertContains(self.response, body, html=True)
self.assertContains(self.response, model_body, html=True) self.assertContains(self.response, model_body, html=True)
def test_model_detail_title(self):
self.assertContains(self.response, '<h1>admin_docs.Person</h1>', html=True)
@unittest.skipUnless(utils.docutils_is_available, "no docutils installed.") @unittest.skipUnless(utils.docutils_is_available, "no docutils installed.")
class TestUtils(AdminDocsTestCase): class TestUtils(AdminDocsTestCase):

View File

@ -0,0 +1,35 @@
import unittest
from django.utils import inspect
class Person(object):
def no_arguments(self):
return None
def one_argument(self, something):
return something
def just_args(self, *args):
return args
def all_kinds(self, name, address='home', age=25, *args, **kwargs):
return kwargs
class TestInspectMethods(unittest.TestCase):
def test_get_func_full_args_no_arguments(self):
self.assertEqual(inspect.get_func_full_args(Person.no_arguments), [])
def test_get_func_full_args_one_argument(self):
self.assertEqual(inspect.get_func_full_args(Person.one_argument), [('something',)])
def test_get_func_full_args_all_arguments(self):
arguments = [('name',), ('address', 'home'), ('age', 25), ('*args',), ('**kwargs',)]
self.assertEqual(inspect.get_func_full_args(Person.all_kinds), arguments)
def test_func_accepts_var_args_has_var_args(self):
self.assertEqual(inspect.func_accepts_var_args(Person.just_args), True)
def test_func_accepts_var_args_no_var_args(self):
self.assertEqual(inspect.func_accepts_var_args(Person.one_argument), False)