Add api_docs decorator and parameter info. Revamp templates.

Remove OAuth2 api documentation until it lands.
This commit is contained in:
Max Goodman
2012-03-21 11:49:00 -07:00
parent 100991117c
commit 1890d24ba0
10 changed files with 428 additions and 113 deletions

View File

@@ -1,70 +1,142 @@
import re
from collections import defaultdict
from itertools import chain
import inspect
from pylons.i18n import _
from reddit_base import RedditController
from r2.lib.utils import Storage
from r2.lib.pages import BoringPage, ApiHelp
# API sections displayed in the documentation page.
# Each section can have a title and a markdown-formatted description.
section_info = {
'account': {
'title': _('account'),
},
'flair': {
'title': _('flair'),
},
'links_and_comments': {
'title': _('links & comments'),
},
'messages': {
'title': _('private messages'),
},
'moderation': {
'title': _('moderation'),
},
'misc': {
'title': _('misc'),
},
'listings': {
'title': _('listings'),
},
'search': {
'title': _('search'),
},
'subreddits': {
'title': _('subreddits'),
},
'users': {
'title': _('users'),
}
}
api_section = Storage((k, k) for k in section_info)
def api_doc(section, **kwargs):
"""
Add documentation annotations to the decorated function.
See ApidocsController.docs_from_controller for a list of annotation fields.
"""
def add_metadata(api_function):
doc = api_function._api_doc = getattr(api_function, '_api_doc', {})
if 'extends' in kwargs:
kwargs['extends'] = kwargs['extends']._api_doc
doc.update(kwargs)
doc['section'] = section
return api_function
return add_metadata
class ApidocsController(RedditController):
@staticmethod
def docs_from_controller(controller, url_prefix='/api'):
"""
Examines a controller for documentation. A dictionary of URLs is
returned. For each URL, a dictionary of HTTP methods (GET, POST, etc.)
is contained. For each URL/method pair, a dictionary containing the
following items is available:
Examines a controller for documentation. A dictionary index of
sections containing dictionaries of URLs is returned. For each URL, a
dictionary of HTTP methods (GET, POST, etc.) is contained. For each
URL/method pair, a dictionary containing the following items is
available:
- `__doc__`: Markdown-formatted docstring.
- `oauth2_scopes`: List of OAuth2 scopes
- *more to come...*
- `doc`: Markdown-formatted docstring.
- `uri`: Manually-specified URI to list the API method as
- `uri_variants`: Alternate URIs to access the API method from
- `extensions`: URI extensions the API method supports
- `parameters`: Dictionary of possible parameter names and descriptions.
- `extends`: API method from which to inherit documentation
"""
api_methods = defaultdict(dict)
api_docs = defaultdict(lambda: defaultdict(dict))
for name, func in controller.__dict__.iteritems():
i = name.find('_')
if i > 0:
method = name[:i]
action = name[i+1:]
else:
method, sep, action = name.partition('_')
if not action:
continue
if func.__doc__ and method in ('GET', 'POST'):
docs = {
'__doc__': re.sub(r'\n +', '\n', func.__doc__).strip(),
}
api_doc = getattr(func, '_api_doc', None)
if api_doc and 'section' in api_doc and method in ('GET', 'POST'):
docs = {}
docs['doc'] = inspect.getdoc(func)
if hasattr(func, 'oauth2_perms'):
scopes = func.oauth2_perms.get('allowed_scopes')
if scopes:
docs['oauth2_scopes'] = scopes
if 'extends' in api_doc:
docs.update(api_doc['extends'])
# parameters are handled separately.
docs['parameters'] = {}
docs.update(api_doc)
# TODO: in the future, it would be cool to introspect the
# validators in order to generate a list of request
# parameters. Some decorators also give a hint as to response
# type (JSON, etc.) which could be included as well.
uri = docs.get('uri') or '/'.join((url_prefix, action))
if 'extensions' in docs:
# if only one extension was specified, add it to the URI.
if len(docs['extensions']) == 1:
uri += '.' + docs['extensions'][0]
del docs['extensions']
docs['uri'] = uri
api_methods['/'.join((url_prefix, action))][method] = docs
# add every variant to the index -- the templates will filter
# out variants in the long-form documentation
for variant in chain([uri], docs.get('uri_variants', [])):
api_docs[docs['section']][variant][method] = docs
return api_methods
return api_docs
def GET_docs(self):
# controllers to gather docs from.
from r2.controllers.api import ApiController, ApiminimalController
from r2.controllers.apiv1 import APIv1Controller
from r2.controllers.oauth2 import OAuth2FrontendController, OAuth2AccessController, scope_info
from r2.controllers.front import FrontController
from r2.controllers import listingcontroller
api_methods = defaultdict(dict)
for controller, url_prefix in ((ApiController, '/api'),
(ApiminimalController, '/api'),
(OAuth2FrontendController, '/api/v1'),
(OAuth2AccessController, '/api/v1'),
(APIv1Controller, '/api/v1')):
for url, methods in self.docs_from_controller(controller, url_prefix).iteritems():
api_methods[url].update(methods)
api_controllers = [
(ApiController, '/api'),
(ApiminimalController, '/api'),
(FrontController, '')
]
for name, value in vars(listingcontroller).iteritems():
if name.endswith('Controller'):
api_controllers.append((value, ''))
# merge documentation info together.
api_docs = defaultdict(dict)
for controller, url_prefix in api_controllers:
for section, contents in self.docs_from_controller(controller, url_prefix).iteritems():
api_docs[section].update(contents)
return BoringPage(
_('api documentation'),
content=ApiHelp(
api_methods=api_methods,
oauth2_scopes=scope_info,
)
api_docs=api_docs
),
css_class="api-help",
show_sidebar=False,
show_firsttext=False
).render()

View File

@@ -464,6 +464,7 @@ def paginated_listing(default_page_size=25, max_page_size=100, backend='sql'):
count=VCount('count'),
target=VTarget("target"),
show=VLength('show', 3))
@utils.wraps_api(fn)
def new_fn(self, before, **env):
if c.render_style == "htmllite":
c.link_target = env.get("target")
@@ -532,6 +533,7 @@ def require_https():
def prevent_framing_and_css(allow_cname_frame=False):
def wrap(f):
@utils.wraps_api(f)
def no_funny_business(*args, **kwargs):
c.allow_styles = False
if not (allow_cname_frame and c.cname and not c.authorized_cname):

View File

@@ -44,6 +44,7 @@ from datetime import datetime, timedelta
from curses.ascii import isprint
import re, inspect
import pycountry
from itertools import chain
def visible_promo(article):
is_promo = getattr(article, "promoted", None) is not None
@@ -86,6 +87,12 @@ class Validator(object):
c.errors.add(error, msg_params = msg_params, field = field)
def param_docs(self):
param_info = {}
for param in filter(None, tup(self.param)):
param_info[param] = None
return param_info
def __call__(self, url):
a = []
if self.param:
@@ -129,8 +136,17 @@ def _make_validated_kw(fn, simple_vals, param_vals, env):
kw[var] = validator(env)
return kw
def set_api_docs(fn, simple_vals, param_vals):
doc = fn._api_doc = getattr(fn, '_api_doc', {})
param_info = doc.get('parameters', {})
for validator in chain(simple_vals, param_vals.itervalues()):
param_info.update(validator.param_docs())
doc['parameters'] = param_info
doc['lineno'] = fn.func_code.co_firstlineno
def validate(*simple_vals, **param_vals):
def val(fn):
@utils.wraps_api(fn)
def newfn(self, *a, **env):
try:
kw = _make_validated_kw(fn, simple_vals, param_vals, env)
@@ -140,8 +156,7 @@ def validate(*simple_vals, **param_vals):
except VerifiedUserRequiredException:
return self.intermediate_redirect('/verify')
newfn.__name__ = fn.__name__
newfn.__doc__ = fn.__doc__
set_api_docs(newfn, simple_vals, param_vals)
return newfn
return val
@@ -157,6 +172,7 @@ def api_validate(response_type=None):
def wrap(response_function):
def _api_validate(*simple_vals, **param_vals):
def val(fn):
@utils.wraps_api(fn)
def newfn(self, *a, **env):
renderstyle = request.params.get("renderstyle")
if renderstyle:
@@ -187,8 +203,7 @@ def api_validate(response_type=None):
responder.send_failure(errors.VERIFIED_USER_REQUIRED)
return self.api_wrapper(responder.make_response())
newfn.__name__ = fn.__name__
newfn.__doc__ = fn.__doc__
set_api_docs(newfn, simple_vals, param_vals)
return newfn
return val
return _api_validate

View File

@@ -3787,7 +3787,7 @@ class UserIPHistory(Templated):
super(UserIPHistory, self).__init__()
class ApiHelp(Templated):
def __init__(self, api_methods, oauth2_scopes, *a, **kw):
self.api_methods = api_methods
self.oauth2_scopes = oauth2_scopes
api_source_url = "https://github.com/reddit/reddit/blob/master/r2/r2/controllers/api.py"
def __init__(self, api_docs, *a, **kw):
self.api_docs = api_docs
super(ApiHelp, self).__init__(*a, **kw)

View File

@@ -32,6 +32,7 @@ from BeautifulSoup import BeautifulSoup
from time import sleep
from datetime import datetime, timedelta
from functools import wraps, partial, WRAPPER_ASSIGNMENTS
from pylons import g
from pylons.i18n import ungettext, _
from r2.lib.filters import _force_unicode
@@ -1384,3 +1385,8 @@ def constant_time_compare(actual, expected):
result |= ord(actual[i]) ^ ord(expected[i % expected_len])
return result == 0
def wraps_api(f):
# work around issue where wraps() requires attributes to exist
if not hasattr(f, '_api_doc'):
f._api_doc = {}
return wraps(f, assigned=WRAPPER_ASSIGNMENTS+('_api_doc',))

View File

@@ -5140,3 +5140,179 @@ tr.gold-accent + tr > td {
margin-bottom: .5em;
display: inline-block;
}
.content.api-help {
font-size: 1.25em;
margin: 0 auto;
max-width: 950px;
}
.api-help .contents {
padding: 0 20px;
margin-left: 24em;
margin-top: 20px;
}
.api-help .contents .section {
margin-bottom: 2em;
}
.api-help .sidebar {
float: left;
margin-left: 10px;
}
.api-help .sidebar .head {
position: relative;
background: url(../xray-snoo-head.png) top center no-repeat;
height: 188px;
margin-bottom: -78px;
z-index: 2;
}
.api-help .sidebar .feet {
position: relative;
background: url(../xray-snoo-feet.png) top center no-repeat;
height: 75px;
margin-top: -42px;
z-index: 2;
}
.api-help .toc {
background: #181818 url(../xray-snoo-body.png) center repeat-y;
border: 5px solid #959595;
border-radius: 8px;
padding: 15px 2em 0 2em;
width: 18em;
}
.api-help .contents .introduction {
position: relative;
border: 2px solid #ccc;
border-radius: 12px;
padding: 14px;
margin-bottom: -1em;
}
.api-help .introduction:before,
.api-help .introduction:after {
position: absolute;
display: block;
content: '';
border: 15px solid;
border-style: solid solid outset; /* mitigates firefox drawing a thicker arrow */
}
.api-help .introduction:before {
border-color: transparent #ccc transparent transparent;
left: -31px;
top: 58px;
}
.api-help .introduction:after {
border-color: transparent white transparent transparent;
left: -28px;
top: 58px;
}
.api-help .toc ul {
position: relative;
margin-top: .5em;
margin-bottom: 1.5em;
z-index: 10;
}
.api-help .toc > ul > li > strong {
color: #aaa;
}
.api-help .toc a.section {
color: #888;
font-weight: bold;
}
.api-help .toc a {
color: #8EB0D2;
}
.api-help .toc a:hover, .api-help .endpoint a:hover {
text-decoration: underline;
}
.api-help em.placeholder {
font-style: italic;
font-weight: normal;
}
.api-help .toc em.placeholder {
color: #8EB0D2;
}
.api-help .endpoint em.placeholder {
color: #369;
}
.api-help .endpoint, .api-help .section .description {
margin-bottom: 1.5em;
}
.api-help .methods h2 {
color: black;
font-size: 1.45em;
text-align: middle;
margin-top: 1.5em;
margin-bottom: 1em;
border-bottom: 1px solid #aaa;
}
.api-help .endpoint .info {
padding-left: 1em;
border-left: 1px solid #ddd;
}
.api-help .endpoint h3, .api-help .endpoint .uri-variants {
color: #369;
margin-bottom: .5em;
}
.api-help .endpoint .uri-variants {
opacity: .85;
font-weight: bold;
margin-top: -.5em;
margin-left: 3em;
}
.api-help .endpoint .method, .api-help .endpoint .extensions {
font-weight: normal;
color: gray;
}
.api-help .endpoint .extensions {
margin-left: .5em;
}
.api-help .endpoint .links {
float: right;
}
.api-help .endpoint .links a {
margin-left: .85em;
opacity: .45;
}
.api-help .endpoint:hover .links a {
opacity: 1;
}
.api-help .parameters {
background: #f0f0f0;
border-collapse: separate;
border-radius: 3px;
padding: 5px 10px;
width: 100%;
}
.api-help .parameters .name {
font-family: 'Courier New', monospace;
width: 50%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,78 +1,122 @@
<%!
import re
from r2.lib.filters import safemarkdown
from r2.controllers.api_docs import section_info
%>
<div class="content apihelp">
<div class="introduction">
<p>This is the automatically-generated documentation for the Reddit API.</p>
<p>It's gathered from the docstrings in the code.</p>
<%def name="api_method_id(uri, method)">${method}_${uri.replace('/', '_').strip('_')}</%def>
<%def name="api_uri(uri)">${unsafe(re.sub(r'{(\w+)}', r'<em class="placeholder">\1</em>', uri))}</%def>
<%
api = thing.api_docs
%>
<div class="sidebar">
<div class="head"></div>
<div class="toc">
<ul>
<li>
<strong>API methods</strong>
<ul>
%for section in sorted(api):
<li>
<a href="#section_${section}" class="section">${section_info[section]['title']}</a>
<ul>
%for uri in sorted(api[section]):
<% methods = sorted(api[section][uri].keys()) %>
<li>
<a href="#${api_method_id(uri, methods[0])}">${api_uri(uri)}</a>
&nbsp; <span class="gray">(${', '.join(methods)})</span>
</li>
%endfor
</ul>
</li>
%endfor
</ul>
</li>
</ul>
</div>
<div class="feet"></div>
</div>
<div class="contents">
<h1>Contents</h1>
<div class="section introduction">
<p>This is automatically-generated documentation for the reddit API. It's gathered from docstrings and annotations in the code.</p>
<br/>
<p>The reddit API and code are&#32;<a href="/code">open source</a>. Found a mistake or interested in helping us improve? Have a gander at&#32;<a href="${thing.api_source_url}">api.py</a>&#32;and send us a pull request.</p>
</div>
<ul>
<li>
<strong>API methods</strong>
<ul>
%for uri in sorted(thing.api_methods.keys()):
<li>
<a href="#api_method_${uri.split('/')[-1]}">${uri}</a>
&nbsp; <span class="gray">(${', '.join(sorted(thing.api_methods[uri].keys()))})</span>
</li>
<div class="section methods">
%for section in sorted(api):
<h2 id="section_${section}">${section_info[section]['title']}</h2>
%if 'description' in section_info[section]:
<div class="description">
${unsafe(safemarkdown(section_info[section]['description']))}
</div>
%endif
%for uri in sorted(api[section]):
%for method in sorted(api[section][uri]):
<%
docs = api[section][uri][method]
# skip uri variants in the index
if docs['uri'] != uri:
continue
extends = docs.get('extends')
%>
<div class="endpoint" id="${api_method_id(uri, method)}">
<div class="links">
%if docs.get('lineno'):
<a href="${thing.api_source_url}#L${docs['lineno']}">view code</a>
%endif
<a href="#${api_method_id(uri, method)}">#</a>
</div>
<h3>
<span class="method">${method}&nbsp;</span>
${api_uri(uri)}
%if 'extensions' in docs:
<span class="extensions">
[ ${' | '.join('.'+extension for extension in docs['extensions'])} ]
</span>
%endif
</h3>
%if 'uri_variants' in docs:
<ul class="uri-variants">
%for variant in docs['uri_variants']:
<li id="${api_method_id(variant, method)}">&rarr; ${api_uri(variant)}</li>
%endfor
</ul>
%endif
<div class="info">
${unsafe(safemarkdown(docs.get('doc')))}
<%
params = docs.get('parameters')
base_params = extends.get('parameters') if extends else None
%>
%if params or base_params:
<table class="parameters">
%if params:
%for param in sorted(params):
<tr>
<td class="name">${param}</td>
<td class="desc">${params[param]}</td>
</tr>
%endfor
%endif
%if base_params:
%for param in sorted(base_params):
<tr class="base-param">
<td class="name">${param}</td>
<td class="desc">${base_params[param]}</td>
</tr>
%endfor
%endif
</table>
%endif
</div>
</div>
%endfor
%endfor
</ul>
<li>
<strong>OAuth scopes</strong>
<ul>
%for scope in sorted(thing.oauth2_scopes.keys()):
<li><a href="#oauth2_scope_${scope}">${scope}</a></li>
%endfor
</ul>
</li>
</ul>
</div>
<div class="methods">
<h1>API methods</h1>
%for uri in sorted(thing.api_methods.keys()):
<div class="endpoint">
<a name="api_method_${uri.split('/')[-1]}"></a>
<h2>${uri}</h2>
%for method in sorted(thing.api_methods[uri].keys()):
<div class="method">
<h3>${method}</h3>
${unsafe(safemarkdown(thing.api_methods[uri][method]))}
</div>
%endfor
</div>
%endfor
</div>
<div class="apihelp scopes">
<h1>OAuth scopes</h1>
<table class="oauth2-scopes">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>description</th>
</tr>
</thead>
<tbody>
%for scope in sorted(thing.oauth2_scopes.keys()):
<tr>
<td class="oauth2-scope-id">
<a name="oauth2_scope_${scope}"></a>
${scope}
</td>
<td class="oauth2-scope-name">${thing.oauth2_scopes[scope]['name']}</td>
<td class="oauth2-scope-description">${thing.oauth2_scopes[scope]['description']}</td>
</tr>
%endfor
</tbody>
</table>
</div>
</div>