summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZan Anderle <zan.anderle@gmail.com>2014-10-30 20:45:01 +0100
committerTim Graham <timograham@gmail.com>2015-09-07 15:07:39 -0400
commitf3dc173240d2a91c831b08dd6820e1d83322f3da (patch)
tree99567a09fd1818664a81f97b2491b9f43002df1a
parent3c5862ccb0fc77418c3b20c21c6b57c40b483464 (diff)
Fixed #24917 -- Made admindocs display model methods that take arguments.
-rw-r--r--django/contrib/admindocs/templates/admin_doc/model_detail.html25
-rw-r--r--django/contrib/admindocs/views.py39
-rw-r--r--django/utils/inspect.py53
-rw-r--r--docs/ref/contrib/admin/admindocs.txt13
-rw-r--r--docs/releases/1.9.txt6
-rw-r--r--tests/admin_docs/models.py11
-rw-r--r--tests/admin_docs/tests.py33
-rw-r--r--tests/utils_tests/test_inspect.py35
8 files changed, 201 insertions, 14 deletions
diff --git a/django/contrib/admindocs/templates/admin_doc/model_detail.html b/django/contrib/admindocs/templates/admin_doc/model_detail.html
index 59d8abcd8f..6472119f54 100644
--- a/django/contrib/admindocs/templates/admin_doc/model_detail.html
+++ b/django/contrib/admindocs/templates/admin_doc/model_detail.html
@@ -27,6 +27,7 @@
{{ description }}
+<h3>{% trans 'Fields' %}</h3>
<div class="module">
<table class="model">
<thead>
@@ -48,6 +49,30 @@
</table>
</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>
</div>
{% endblock %}
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 543b3e6b02..996650cefb 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -14,7 +14,10 @@ from django.db import models
from django.http import Http404
from django.template.engine import Engine
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.views.generic import TemplateView
@@ -219,7 +222,7 @@ class ModelDetailView(BaseAdminDocsView):
fields.append({
'name': field.name,
'data_type': data_type,
- 'verbose': verbose,
+ 'verbose': verbose or '',
'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),
})
+ methods = []
# Gather model methods.
for func_name, func in model.__dict__.items():
- if inspect.isfunction(func) and func_has_no_args(func):
+ if inspect.isfunction(func):
try:
for exclude in MODEL_METHODS_EXCLUDE:
if func_name.startswith(exclude):
@@ -254,11 +258,29 @@ class ModelDetailView(BaseAdminDocsView):
verbose = func.__doc__
if verbose:
verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name)
- fields.append({
- 'name': func_name,
- 'data_type': get_return_data_type(func_name),
- 'verbose': verbose,
- })
+ # If a method has no arguments, show it as a 'field', otherwise
+ # as a 'method with arguments'.
+ if func_has_no_args(func) and not func_accepts_kwargs(func) and not func_accepts_var_args(func):
+ 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
for rel in opts.related_objects:
@@ -282,6 +304,7 @@ class ModelDetailView(BaseAdminDocsView):
'summary': title,
'description': body,
'fields': fields,
+ 'methods': methods,
})
return super(ModelDetailView, self).get_context_data(**kwargs)
diff --git a/django/utils/inspect.py b/django/utils/inspect.py
index 3e3ad0ac23..597b2c095e 100644
--- a/django/utils/inspect.py
+++ b/django/utils/inspect.py
@@ -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):
if six.PY2:
# 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):
args = inspect.getargspec(func)[0] if six.PY2 else [
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
diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt
index 2d76748e02..241e12da36 100644
--- a/docs/ref/contrib/admin/admindocs.txt
+++ b/docs/ref/contrib/admin/admindocs.txt
@@ -60,10 +60,15 @@ Model reference
===============
The **models** section of the ``admindocs`` page describes each model in the
-system along with all the fields and methods (without any arguments) available
-on it. While model properties don't have any arguments, they are not listed.
-Relationships to other models appear as hyperlinks. Descriptions are pulled
-from ``help_text`` attributes on fields or from docstrings on model methods.
+system along with all the fields and methods available on it. Relationships
+to other models appear as hyperlinks. Descriptions are pulled 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::
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index a0219bd370..04b5a6e781 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -163,6 +163,12 @@ Minor features
* 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`
^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/tests/admin_docs/models.py b/tests/admin_docs/models.py
index 9ddcb762e1..a425ae0fcd 100644
--- a/tests/admin_docs/models.py
+++ b/tests/admin_docs/models.py
@@ -44,6 +44,17 @@ class Person(models.Model):
def _get_full_name(self):
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):
pass
diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py
index 7619e01465..ff860f9929 100644
--- a/tests/admin_docs/tests.py
+++ b/tests/admin_docs/tests.py
@@ -246,7 +246,7 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
def setUp(self):
self.client.login(username='super', password='secret')
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):
"""
@@ -261,6 +261,34 @@ class TestModelDetailView(TestDataMixin, AdminDocsTestCase):
self.assertNotContains(self.response, "<td>set_status</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):
"""
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, 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.")
class TestUtils(AdminDocsTestCase):
diff --git a/tests/utils_tests/test_inspect.py b/tests/utils_tests/test_inspect.py
new file mode 100644
index 0000000000..88a9fb0d74
--- /dev/null
+++ b/tests/utils_tests/test_inspect.py
@@ -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)