Fixed #24917 -- Made admindocs display model methods that take arguments.
This commit is contained in:
parent
3c5862ccb0
commit
f3dc173240
|
@ -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' %}">‹ {% trans 'Back to Model Documentation' %}</a></p>
|
<p class="small"><a href="{% url 'django-admindocs-models-index' %}">‹ {% trans 'Back to Model Documentation' %}</a></p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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,10 +258,28 @@ 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)
|
||||||
|
# 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({
|
fields.append({
|
||||||
'name': func_name,
|
'name': func_name,
|
||||||
'data_type': get_return_data_type(func_name),
|
'data_type': get_return_data_type(func_name),
|
||||||
'verbose': verbose,
|
'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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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::
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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='ltd'</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):
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue