Browse Source

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

Zan Anderle 10 years ago
parent
commit
f3dc173240

+ 25 - 0
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 %}

+ 31 - 8
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)
 

+ 52 - 1
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
 

+ 9 - 4
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::
 

+ 6 - 0
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`
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 

+ 11 - 0
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
 

+ 32 - 1
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):

+ 35 - 0
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)