From 81d92cc73fb5e35c1e6a539dc1eb5f60b02dd316 Mon Sep 17 00:00:00 2001 From: Joe Gregorio Date: Mon, 9 Jul 2012 16:46:02 -0400 Subject: Build cleaner and easier to read docs for dynamic surfaces. Reviewed in http://codereview.appspot.com/6376043/. --- describe.py | 321 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 303 insertions(+), 18 deletions(-) (limited to 'describe.py') diff --git a/describe.py b/describe.py index ff2ec1fea..8ee8e7a19 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 = """ +""" + +DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/' + '{api}/{apiVersion}/rest') + +METHOD_TEMPLATE = """
+ $name($params) +
$doc
+
+""" + +COLLECTION_LINK = """

+ $name() +

+

Returns the $name Resource.

+""" + +METHOD_LINK = """

+ $name($params)

+

$firstline

""" + + +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('%s' % (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 = [ + '', + css, + '

%s

' % breadcrumbs(path[:-1], root_discovery), + '

Instance Methods

' + ] + + # 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) + + + # 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('

Method Details

') + for name in methods: + dname = name.rsplit('_')[0] + html.append(method(name, getattr(resource, name).__doc__)) + + html.append('') - obj, name = pydoc.resolve(type(resource)) - page = pydoc.html.page( - pydoc.describe(obj), pydoc.html.document(obj, name)) + return '\n'.join(html) - for name in collections: - page = re.sub('strong>(%s)<' % name, r'strong>\1<' % (path + name + ".html"), page) - for name in collections: - document(getattr(resource, name)(), path + name + ".") + +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: -- cgit v1.2.3