Build cleaner and easier to read docs for dynamic surfaces.

Reviewed in http://codereview.appspot.com/6376043/.
diff --git a/apiclient/discovery.py b/apiclient/discovery.py
index ce54f88..b142ff1 100644
--- a/apiclient/discovery.py
+++ b/apiclient/discovery.py
@@ -74,7 +74,7 @@
 RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
                   'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
                   'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
-                  'pass', 'print', 'raise', 'return', 'try', 'while' ]
+                  'pass', 'print', 'raise', 'return', 'try', 'while', 'body']
 
 
 def fix_method_name(name):
@@ -396,7 +396,9 @@
         methodDesc['parameters']['body']['type'] = 'object'
     if 'mediaUpload' in methodDesc:
       methodDesc['parameters']['media_body'] = {
-          'description': 'The filename of the media request body.',
+          'description':
+            'The filename of the media request body, or an instance of a '
+            'MediaUpload object.',
           'type': 'string',
           'required': False,
           }
@@ -596,9 +598,20 @@
 
     # Skip undocumented params and params common to all methods.
     skip_parameters = rootDesc.get('parameters', {}).keys()
-    skip_parameters.append(STACK_QUERY_PARAMETERS)
+    skip_parameters.extend(STACK_QUERY_PARAMETERS)
 
-    for arg in argmap.iterkeys():
+    all_args = argmap.keys()
+    args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
+
+    # Move body to the front of the line.
+    if 'body' in all_args:
+      args_ordered.append('body')
+
+    for name in all_args:
+      if name not in args_ordered:
+        args_ordered.append(name)
+
+    for arg in args_ordered:
       if arg in skip_parameters:
         continue
 
@@ -653,13 +666,13 @@
     def methodNext(self, previous_request, previous_response):
       """Retrieves the next page of results.
 
-      Args:
-        previous_request: The request for the previous page.
-        previous_response: The response from the request for the previous page.
+Args:
+  previous_request: The request for the previous page. (required)
+  previous_response: The response from the request for the previous page. (required)
 
-      Returns:
-        A request object that you can call 'execute()' on to request the next
-        page. Returns None if there are no more items in the collection.
+Returns:
+  A request object that you can call 'execute()' on to request the next
+  page. Returns None if there are no more items in the collection.
       """
       # Retrieve nextPageToken from previous_response
       # Use as pageToken in previous_request to create new request.
diff --git a/describe.py b/describe.py
index ff2ec1f..8ee8e7a 100644
--- a/describe.py
+++ b/describe.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 #
-# Copyright 2007 Google Inc.
+# Copyright 2012 Google Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,47 +14,332 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+"""Create documentation for generate API surfaces.
+
+Command-line tool that creates documentation for all APIs listed in discovery.
+The documentation is generated from a combination of the discovery document and
+the generated API surface itself.
+"""
+
 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
 
+import json
 import os
-import pydoc
 import re
 import sys
 import httplib2
 
-from oauth2client.anyjson import simplejson
+from string import Template
+
 from apiclient.discovery import build
+from oauth2client.anyjson import simplejson
+import uritemplate
+
 
 BASE = 'docs/dyn'
 
-def document(resource, path):
-  print path
+CSS = """<style>
+
+body, h1, h2, h3, div, span, p, pre, a {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-weight: inherit;
+  font-style: inherit;
+  font-size: 100%;
+  font-family: inherit;
+  vertical-align: baseline;
+}
+
+body {
+  font-size: 13px;
+  padding: 1em;
+}
+
+h1 {
+  font-size: 26px;
+  margin-bottom: 1em;
+}
+
+h2 {
+  font-size: 24px;
+  margin-bottom: 1em;
+}
+
+h3 {
+  font-size: 20px;
+  margin-bottom: 1em;
+  margin-top: 1em;
+}
+
+pre, code {
+  line-height: 1.5;
+  font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
+}
+
+pre {
+  margin-top: 0.5em;
+}
+
+h1, h2, h3, p {
+  font-family: Arial, sans serif;
+}
+
+h1, h2, h3 {
+  border-bottom: solid #CCC 1px;
+}
+
+.toc_element {
+  margin-top: 0.5em;
+}
+
+.firstline {
+  margin-left: 2 em;
+}
+
+.method  {
+  margin-top: 1em;
+  border: solid 1px #CCC;
+  padding: 1em;
+  background: #EEE;
+}
+
+.details {
+  font-weight: bold;
+  font-size: 14px;
+}
+
+</style>
+"""
+
+DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
+                 '{api}/{apiVersion}/rest')
+
+METHOD_TEMPLATE = """<div class="method">
+    <code class="details" id="$name">$name($params)</code>
+  <pre>$doc</pre>
+</div>
+"""
+
+COLLECTION_LINK = """<p class="toc_element">
+  <code><a href="$href">$name()</a></code>
+</p>
+<p class="firstline">Returns the $name Resource.</p>
+"""
+
+METHOD_LINK = """<p class="toc_element">
+  <code><a href="#$name">$name($params)</a></code></p>
+<p class="firstline">$firstline</p>"""
+
+
+def safe_version(version):
+  """Create a safe version of the verion string.
+
+  Needed so that we can distinguish between versions
+  and sub-collections in URIs. I.e. we don't want
+  adsense_v1.1 to refer to the '1' collection in the v1
+  version of the adsense api.
+
+  Args:
+    version: string, The version string.
+  Returns:
+    The string with '.' replaced with '_'.
+  """
+
+  return version.replace('.', '_')
+
+
+def unsafe_version(version):
+  """Undoes what safe_version() does.
+
+  See safe_version() for the details.
+
+
+  Args:
+    version: string, The safe version string.
+  Returns:
+    The string with '_' replaced with '.'.
+  """
+
+  return version.replace('_', '.')
+
+
+def method_params(doc):
+  """Document the parameters of a method.
+
+  Args:
+    doc: string, The method's docstring.
+
+  Returns:
+    The method signature as a string.
+  """
+  doclines = doc.splitlines()
+  if 'Args:' in doclines:
+    begin = doclines.index('Args:')
+    if 'Returns:' in doclines[begin+1:]:
+      end = doclines.index('Returns:', begin)
+      args = doclines[begin+1: end]
+    else:
+      args = doclines[begin+1:]
+
+    parameters = []
+    for line in args:
+      m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
+      if m is None:
+        continue
+      pname = m.group(1)
+      desc = m.group(2)
+      if '(required)' not in desc:
+        pname = pname + '=None'
+      parameters.append(pname)
+    parameters = ', '.join(parameters)
+  else:
+    parameters = ''
+  return parameters
+
+
+def method(name, doc):
+  """Documents an individual method.
+
+  Args:
+    name: string, Name of the method.
+    doc: string, The methods docstring.
+  """
+
+  params = method_params(doc)
+  return Template(METHOD_TEMPLATE).substitute(name=name, params=params, doc=doc)
+
+
+def breadcrumbs(path, root_discovery):
+  """Create the breadcrumb trail to this page of documentation.
+
+  Args:
+    path: string, Dot separated name of the resource.
+    root_discovery: Deserialized discovery document.
+
+  Returns:
+    HTML with links to each of the parent resources of this resource.
+  """
+  parts = path.split('.')
+
+  crumbs = []
+  accumulated = []
+
+  for i, p in enumerate(parts):
+    prefix = '.'.join(accumulated)
+    # The first time through prefix will be [], so we avoid adding in a
+    # superfluous '.' to prefix.
+    if prefix:
+      prefix += '.'
+    display = p
+    if i == 0:
+      display = root_discovery.get('title', display)
+    crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
+    accumulated.append(p)
+
+  return ' . '.join(crumbs)
+
+
+def document_collection(resource, path, root_discovery, discovery, css=CSS):
+  """Document a single collection in an API.
+
+  Args:
+    resource: Collection or service being documented.
+    path: string, Dot separated name of the resource.
+    root_discovery: Deserialized discovery document.
+    discovery: Deserialized discovery document, but just the portion that
+      describes the resource.
+    css: string, The CSS to include in the generated file.
+  """
   collections = []
+  methods = []
+  resource_name = path.split('.')[-2]
+  html = [
+      '<html><body>',
+      css,
+      '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
+      '<h2>Instance Methods</h2>'
+      ]
+
+  # Which methods are for collections.
   for name in dir(resource):
-    if not "_" in name and callable(getattr(resource, name)) and hasattr(
-          getattr(resource, name), '__is_resource__'):
-      collections.append(name)
+    if not name.startswith('_') and callable(getattr(resource, name)):
+      if hasattr(getattr(resource, name), '__is_resource__'):
+        collections.append(name)
+      else:
+        methods.append(name)
 
-  obj, name = pydoc.resolve(type(resource))
-  page = pydoc.html.page(
-      pydoc.describe(obj), pydoc.html.document(obj, name))
 
-  for name in collections:
-    page = re.sub('strong>(%s)<' % name, r'strong><a href="%s">\1</a><' % (path + name + ".html"), page)
-  for name in collections:
-    document(getattr(resource, name)(), path + name + ".")
+  # TOC
+  if collections:
+    for name in collections:
+      if not name.startswith('_') and callable(getattr(resource, name)):
+        href = path + name + '.html'
+        html.append(Template(COLLECTION_LINK).substitute(href=href, name=name))
+
+  if methods:
+    for name in methods:
+      if not name.startswith('_') and callable(getattr(resource, name)):
+        doc = getattr(resource, name).__doc__
+        params = method_params(doc)
+        firstline = doc.splitlines()[0]
+        html.append(Template(METHOD_LINK).substitute(
+            name=name, params=params, firstline=firstline))
+
+  if methods:
+    html.append('<h3>Method Details</h3>')
+    for name in methods:
+      dname = name.rsplit('_')[0]
+      html.append(method(name, getattr(resource, name).__doc__))
+
+  html.append('</body></html>')
+
+  return '\n'.join(html)
+
+
+def document_collection_recursive(resource, path, root_discovery, discovery):
+
+  html = document_collection(resource, path, root_discovery, discovery)
 
   f = open(os.path.join(BASE, path + 'html'), 'w')
-  f.write(page)
+  f.write(html)
   f.close()
 
+  for name in dir(resource):
+    if (not name.startswith('_')
+        and callable(getattr(resource, name))
+        and hasattr(getattr(resource, name), '__is_resource__')):
+      dname = name.rsplit('_')[0]
+      collection = getattr(resource, name)()
+      document_collection_recursive(collection, path + name + '.', root_discovery,
+               discovery['resources'].get(dname, {}))
+
 def document_api(name, version):
+  """Document the given API.
+
+  Args:
+    name: string, Name of the API.
+    version: string, Version of the API.
+  """
   service = build(name, version)
-  document(service, '%s.%s.' % (name, version))
+  response, content = http.request(
+      uritemplate.expand(
+          DISCOVERY_URI, {
+              'api': name,
+              'apiVersion': version})
+          )
+  discovery = json.loads(content)
+
+  version = safe_version(version)
+
+  document_collection_recursive(
+      service, '%s_%s.' % (name, version), discovery, discovery)
+
 
 if __name__ == '__main__':
   http = httplib2.Http()
-  resp, content = http.request('https://www.googleapis.com/discovery/v0.3/directory?preferred=true')
+  resp, content = http.request(
+      'https://www.googleapis.com/discovery/v1/apis?preferred=true')
   if resp.status == 200:
     directory = simplejson.loads(content)['items']
     for api in directory:
diff --git a/samples/api-python-client-doc/embed.html b/samples/api-python-client-doc/embed.html
index 96b2ba4..ce84a8e 100644
--- a/samples/api-python-client-doc/embed.html
+++ b/samples/api-python-client-doc/embed.html
@@ -68,7 +68,7 @@
       <tr>
         <td><img class=icon src="{{ item.icons.x16 }}"/> {{ item.title }}</td>
         <td><a target=_top href="{{ item.documentationLink }}">Documentation</a></td>
-        <td><a target=_top href="/{{ item.name }}/{{ item.version }}">PyDoc</a></td>
+        <td><a target=_top href="/{{ item.name }}_{{ item.safe_version }}.html">PyDoc</a></td>
         <td>{{ item.name }}</td>
         <td>{{ item.version}}</td>
       </tr>
diff --git a/samples/api-python-client-doc/gadget.html b/samples/api-python-client-doc/gadget.html
index 49a9436..0287e28 100644
--- a/samples/api-python-client-doc/gadget.html
+++ b/samples/api-python-client-doc/gadget.html
@@ -8,7 +8,7 @@
       <tr> 
         <td><img style="width: 16px; height: 16px" src="{{ item.icons.x16 }}"/> {{ item.name }} </td>
         <td><a href="{{ item.documentationLink }}">Documentation</a></td>
-        <td><a href="/{{ item.name }}/{{ item.version }}">PyDoc</a></td>
+        <td><a href="/{{ item.name }}_{{ item.safe_version }}.html">PyDoc</a></td>
       </tr> 
       {% endfor %}
     </table>
diff --git a/samples/api-python-client-doc/index.html b/samples/api-python-client-doc/index.html
index af9afbe..1710431 100644
--- a/samples/api-python-client-doc/index.html
+++ b/samples/api-python-client-doc/index.html
@@ -69,7 +69,7 @@
       <tr>
         <td><img class=icon src="{{ item.icons.x16 }}"/> {{ item.title }}</td>
         <td><a href="{{ item.documentationLink }}">Documentation</a></td>
-        <td><a href="/{{ item.name }}/{{ item.version }}">PyDoc</a></td>
+        <td><a href="/{{ item.name }}_{{ item.safe_version }}.html">PyDoc</a></td>
         <td>{{ item.name }}</td>
         <td>{{ item.version}}</td>
       </tr>
diff --git a/samples/api-python-client-doc/main.py b/samples/api-python-client-doc/main.py
index 40c3ffc..23ce65e 100755
--- a/samples/api-python-client-doc/main.py
+++ b/samples/api-python-client-doc/main.py
@@ -26,10 +26,14 @@
 
 import httplib2
 import inspect
+import logging
 import os
 import pydoc
 import re
 
+import describe
+import uritemplate
+
 from apiclient import discovery
 from apiclient.errors import HttpError
 from google.appengine.api import memcache
@@ -50,6 +54,9 @@
     uri += ('&userIp=' + ip)
   resp, content = http.request(uri)
   directory = simplejson.loads(content)['items']
+  for item in directory:
+    item['title'] = item.get('title', item.get('description', ''))
+    item['safe_version'] = describe.safe_version(item['version'])
   return directory
 
 
@@ -59,8 +66,6 @@
 
   def get(self):
     directory = get_directory_doc()
-    for item in directory:
-      item['title'] = item.get('title', item.get('description', ''))
     path = os.path.join(os.path.dirname(__file__), 'index.html')
     self.response.out.write(
         template.render(
@@ -73,8 +78,6 @@
 
   def get(self):
     directory = get_directory_doc()
-    for item in directory:
-      item['title'] = item.get('title', item.get('description', ''))
     path = os.path.join(os.path.dirname(__file__), 'gadget.html')
     self.response.out.write(
         template.render(
@@ -88,8 +91,6 @@
 
   def get(self):
     directory = get_directory_doc()
-    for item in directory:
-      item['title'] = item.get('title', item.get('description', ''))
     path = os.path.join(os.path.dirname(__file__), 'embed.html')
     self.response.out.write(
         template.render(
@@ -97,54 +98,54 @@
                    }))
 
 
-def _render(resource):
-  """Use pydoc helpers on an instance to generate the help documentation.
-  """
-  obj, name = pydoc.resolve(type(resource))
-  return pydoc.html.page(
-      pydoc.describe(obj), pydoc.html.document(obj, name))
-
-
 class ResourceHandler(webapp.RequestHandler):
   """Handles serving the PyDoc for a given collection.
   """
 
   def get(self, service_name, version, collection):
+
+    real_version = describe.unsafe_version(version)
+
+    logging.info('%s %s %s', service_name, version, collection)
     http = httplib2.Http(memcache)
     try:
-      resource = discovery.build(service_name, version, http=http)
+      resource = discovery.build(service_name, real_version, http=http)
     except:
+      logging.error('Failed to build service.')
       return self.error(404)
+
+    DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
+      '{api}/{apiVersion}/rest')
+    response, content = http.request(
+        uritemplate.expand(
+            DISCOVERY_URI, {
+                'api': service_name,
+                'apiVersion': real_version})
+            )
+    root_discovery = simplejson.loads(content)
+    collection_discovery = root_discovery
+
     # descend the object path
     if collection:
       try:
-        path = collection.split('/')
+        path = collection.split('.')
         if path:
           for method in path:
             resource = getattr(resource, method)()
+            collection_discovery = collection_discovery['resources'][method]
       except:
+        logging.error('Failed to parse the collections.')
         return self.error(404)
+    logging.info('Built everything successfully so far.')
 
-    page = _render(resource)
+    path = '%s_%s.' % (service_name, version)
+    if collection:
+      path += '.'.join(collection.split('/'))
+      path += '.'
 
-    collections = []
-    for name in dir(resource):
-      if not "_" in name and callable(getattr(resource, name)) and hasattr(
-          getattr(resource, name), '__is_resource__'):
-        collections.append(name)
+    page = describe.document_collection(
+        resource, path, root_discovery, collection_discovery)
 
-    if collection is None:
-      collection_path = ''
-    else:
-      collection_path = collection + '/'
-    for name in collections:
-      page = re.sub('strong>(%s)<' % name,
-          r'strong><a href="/%s/%s/%s">\1</a><' % (
-          service_name, version, collection_path + name), page)
-
-    # TODO(jcgregorio) breadcrumbs
-    # TODO(jcgregorio) sample code?
-    page = re.sub('<p>', r'<a href="/">Home</a><p>', page, 1)
     self.response.out.write(page)
 
 
@@ -154,9 +155,9 @@
       (r'/', MainHandler),
       (r'/_gadget/', GadgetHandler),
       (r'/_embed/', EmbedHandler),
-      (r'/([^\/]*)/([^\/]*)(?:/(.*))?', ResourceHandler),
+      (r'/([^_]+)_([^\.]+)(?:\.(.*))?\.html$', ResourceHandler),
       ],
-      debug=False)
+      debug=True)
   util.run_wsgi_app(application)