From 1890d24ba0d362b69bd55e1e9c617af922647705 Mon Sep 17 00:00:00 2001 From: Max Goodman Date: Wed, 21 Mar 2012 11:49:00 -0700 Subject: [PATCH] Add api_docs decorator and parameter info. Revamp templates. Remove OAuth2 api documentation until it lands. --- r2/r2/controllers/api_docs.py | 152 ++++++++++++++------ r2/r2/controllers/reddit_base.py | 2 + r2/r2/controllers/validator/validator.py | 23 ++- r2/r2/lib/pages/pages.py | 6 +- r2/r2/lib/utils/utils.py | 6 + r2/r2/public/static/css/reddit.css | 176 +++++++++++++++++++++++ r2/r2/public/static/xray-snoo-body.png | Bin 0 -> 236 bytes r2/r2/public/static/xray-snoo-feet.png | Bin 0 -> 4794 bytes r2/r2/public/static/xray-snoo-head.png | Bin 0 -> 11449 bytes r2/r2/templates/apihelp.html | 176 ++++++++++++++--------- 10 files changed, 428 insertions(+), 113 deletions(-) create mode 100644 r2/r2/public/static/xray-snoo-body.png create mode 100644 r2/r2/public/static/xray-snoo-feet.png create mode 100644 r2/r2/public/static/xray-snoo-head.png diff --git a/r2/r2/controllers/api_docs.py b/r2/r2/controllers/api_docs.py index 77ec8d52f..4216d1004 100644 --- a/r2/r2/controllers/api_docs.py +++ b/r2/r2/controllers/api_docs.py @@ -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() diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index 709a13bbc..4da039e05 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -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): diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 185a9c4e7..f19a4792c 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -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 diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 880596a3f..f1f12b64f 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -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) diff --git a/r2/r2/lib/utils/utils.py b/r2/r2/lib/utils/utils.py index 4866fb40f..7a4d7cb3b 100644 --- a/r2/r2/lib/utils/utils.py +++ b/r2/r2/lib/utils/utils.py @@ -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',)) diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index c8ef49069..e6866c176 100644 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -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%; +} diff --git a/r2/r2/public/static/xray-snoo-body.png b/r2/r2/public/static/xray-snoo-body.png new file mode 100644 index 0000000000000000000000000000000000000000..2b8b5acba6669a5169609352b7dbca7ae1010da7 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^0YJ>f!3HFiS#H_@DVAa<&kznEsNqQI0P;BtJR*x3 z7*vBnm@(+phbW++WQl7;NpOBzNqJ&XDnogBxn5>oc5!lIL8@MUQTpt6Hc~)EnVv3= zAs(G?uWsaRFyLW1z`uJ(6K_Jz4Te?n$F@vd93m#z_0Q{c>*t~;kKT2h$zd-DTiX8Q znu*_;GkFZzp;jJo8}lX2FU_f9JhoLQp8u2c$Fpz4Tg?o78PzlosIbmh!Z5j{^><0D adj(@bx1ivIe!B>ur3{{~elF{r5}E)_u232P literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/xray-snoo-feet.png b/r2/r2/public/static/xray-snoo-feet.png new file mode 100644 index 0000000000000000000000000000000000000000..d384355c387c9d0e042a902e2515b7c177dd629d GIT binary patch literal 4794 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 AR{#J2 literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/xray-snoo-head.png b/r2/r2/public/static/xray-snoo-head.png new file mode 100644 index 0000000000000000000000000000000000000000..0c817033d6b22d514e1c0fa47cf9cf4146955e5d GIT binary patch literal 11449 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 z -
-
-

This is the automatically-generated documentation for the Reddit API.

-

It's gathered from the docstrings in the code.

+<%def name="api_method_id(uri, method)">${method}_${uri.replace('/', '_').strip('_')} +<%def name="api_uri(uri)">${unsafe(re.sub(r'{(\w+)}', r'\1', uri))} + +<% + api = thing.api_docs +%> + +
-

Contents

+
+

This is automatically-generated documentation for the reddit API. It's gathered from docstrings and annotations in the code.

+
+

The reddit API and code are open source. Found a mistake or interested in helping us improve? Have a gander at api.py and send us a pull request.

+
-
    -
  • - API methods -
      - %for uri in sorted(thing.api_methods.keys()): -
    • - ${uri} -   (${', '.join(sorted(thing.api_methods[uri].keys()))}) -
    • +
      + %for section in sorted(api): +

      ${section_info[section]['title']}

      + %if 'description' in section_info[section]: +
      + ${unsafe(safemarkdown(section_info[section]['description']))} +
      + %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') + %> +
      + +

      + ${method}  + ${api_uri(uri)} + %if 'extensions' in docs: + + [ ${' | '.join('.'+extension for extension in docs['extensions'])} ] + + %endif +

      + %if 'uri_variants' in docs: +
        + %for variant in docs['uri_variants']: +
      • → ${api_uri(variant)}
      • + %endfor +
      + %endif +
      + ${unsafe(safemarkdown(docs.get('doc')))} + <% + params = docs.get('parameters') + base_params = extends.get('parameters') if extends else None + %> + %if params or base_params: + + %if params: + %for param in sorted(params): + + + + + %endfor + %endif + %if base_params: + %for param in sorted(base_params): + + + + + %endfor + %endif +
      ${param}${params[param]}
      ${param}${base_params[param]}
      + %endif +
      +
      + %endfor %endfor -
    -
  • - OAuth scopes -
      - %for scope in sorted(thing.oauth2_scopes.keys()): -
    • ${scope}
    • - %endfor -
    -
  • -
-
- -
-

API methods

- -%for uri in sorted(thing.api_methods.keys()): -
- -

${uri}

- %for method in sorted(thing.api_methods[uri].keys()): -
-

${method}

- ${unsafe(safemarkdown(thing.api_methods[uri][method]))} -
%endfor
-%endfor -
- -
-

OAuth scopes

- - - - - - - - - - - %for scope in sorted(thing.oauth2_scopes.keys()): - - - - - - %endfor - -
idnamedescription
- - ${scope} - ${thing.oauth2_scopes[scope]['name']}${thing.oauth2_scopes[scope]['description']}
-