Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/swh/web/ui/apidoc.py b/swh/web/ui/apidoc.py
index 5f795907..a0358a0f 100644
--- a/swh/web/ui/apidoc.py
+++ b/swh/web/ui/apidoc.py
@@ -1,423 +1,425 @@
# Copyright (C) 2015-2017 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import re
from functools import wraps
from enum import Enum
from flask import request, render_template, url_for
from flask import g
from swh.web.ui.main import app
class argtypes(Enum): # noqa: N801
"""Class for centralizing argument type descriptions
"""
ts = 'timestamp'
int = 'integer'
str = 'string'
path = 'path'
sha1 = 'sha1'
uuid = 'uuid'
sha1_git = 'sha1_git'
algo_and_hash = 'hash_type:hash'
class rettypes(Enum): # noqa: N801
"""Class for centralizing return type descriptions
"""
octet_stream = 'octet stream'
list = 'list'
dict = 'dict'
class excs(Enum): # noqa: N801
"""Class for centralizing exception type descriptions
"""
badinput = 'BadInputExc'
notfound = 'NotFoundExc'
class APIUrls(object):
"""
Class to manage API documentation URLs.
* Indexes all routes documented using apidoc's decorators.
* Tracks endpoint/request processing method relationships for use
in generating related urls in API documentation
Relies on the load_controllers logic in main.py for initialization.
"""
apidoc_routes = {}
method_endpoints = {}
@classmethod
def get_app_endpoints(cls):
return cls.apidoc_routes
@classmethod
def get_method_endpoints(cls, fname):
if len(cls.method_endpoints) == 0:
cls.method_endpoints = cls.group_routes_by_method()
return cls.method_endpoints[fname]
@classmethod
def group_routes_by_method(cls):
"""
Group URL endpoints according to their processing method.
Returns:
A dict where keys are the processing method names, and values
are the routes that are bound to the key method.
"""
endpoints = {}
for rule in app.url_map.iter_rules():
rule_dict = {'rule': rule.rule,
'methods': rule.methods}
if rule.endpoint not in endpoints:
endpoints[rule.endpoint] = [rule_dict]
else:
endpoints[rule.endpoint].append(rule_dict)
return endpoints
@classmethod
def index_add_route(cls, route, docstring, **kwargs):
"""
Add a route to the self-documenting API reference
"""
if route not in cls.apidoc_routes:
d = {'docstring': docstring}
for k, v in kwargs.items():
d[k] = v
cls.apidoc_routes[route] = d
class APIDocException(Exception):
"""
Custom exception to signal errors in the use of the APIDoc decorators
"""
class APIDocBase(object):
"""
The API documentation decorator base class, responsible for the
operations that link the decorator stack together:
* manages the _inner_dec property, which represents the
decorator directly below self in the decorator tower
* contains the logic used to return appropriately if self is the last
decorator to be applied to the API function
"""
def __init__(self):
self._inner_dec = None
@property
def inner_dec(self):
return self._inner_dec
@inner_dec.setter
def inner_dec(self, instance):
self._inner_dec = instance
@property
def data(self):
raise NotImplementedError
def process_rv(self, f, args, kwargs):
"""
From the arguments f has, determine whether or not it is the last
decorator in the stack, and return the appropriate call to f.
"""
rv = None
if 'outer_decorator' in f.__code__.co_varnames:
rv = f(*args, **kwargs)
else:
nargs = {k: v for k, v in kwargs.items() if k != 'outer_decorator'}
try:
rv = f(*args, **nargs)
except (TypeError, KeyError): # documentation call
rv = None
return rv
def maintain_stack(self, f, args, kwargs):
"""
From the arguments f is called with, determine whether or not the
stack link was made by @apidoc.route, and maintain the linking for
the next call to f.
"""
if 'outer_decorator' not in kwargs:
raise APIDocException('Apidoc %s: expected an apidoc'
' route decorator first'
% self.__class__.__name__)
kwargs['outer_decorator'].inner_dec = self
kwargs['outer_decorator'] = self
return self.process_rv(f, args, kwargs)
class route(APIDocBase): # noqa: N801
"""Decorate an API method to register it in the API doc route index
and create the corresponding Flask route.
This decorator is responsible for bootstrapping the linking of subsequent
decorators, as well as traversing the decorator stack to obtain the
documentation data from it.
Args:
route: documentation page's route
noargs: set to True if the route has no arguments, and its
result should be displayed anytime its documentation
is requested. Default to False
hidden: set to True to remove the endpoint from being listed
in the /api endpoints. Default to False.
tags: Further information on api endpoints. Two values are
possibly expected:
- hidden: remove the entry points from the listing
- upcoming: display the entry point but it is not followable
"""
def __init__(self, route, noargs=False, tags=[]):
super().__init__()
self.route = route
self.noargs = noargs
self.tags = set(tags)
def __call__(self, f):
options = {}
if 'hidden' not in self.tags:
if self.tags:
options['tags'] = self.tags
APIUrls.index_add_route(self.route, f.__doc__, **options)
@wraps(f)
def doc_func(*args, **kwargs):
kwargs['outer_decorator'] = self
rv = self.process_rv(f, args, kwargs)
return self.compute_return(f, rv)
if not self.noargs:
app.add_url_rule(self.route, f.__name__, doc_func)
return doc_func
def filter_api_url(self, endpoint, route_re, noargs):
doc_methods = {'GET', 'HEAD', 'OPTIONS'}
if re.match(route_re, endpoint['rule']):
if endpoint['methods'] == doc_methods and not noargs:
return False
return True
def build_examples(self, f, urls, args):
"""Build example documentation.
Args:
f: function
urls: information relative to url for that function
args: information relative to arguments for that function
Yields:
example based on default parameter value if any
"""
s = set()
r = []
for data_url in urls:
url = data_url['rule']
defaults = {arg['name']: arg['default']
for arg in args
if arg['name'] in url}
if defaults:
url = url_for(f.__name__, **defaults)
if url in s:
continue
s.add(url)
r.append(url)
return r
def compute_return(self, f, rv):
"""Build documentation"""
data = self.data
if not f.__doc__:
raise APIDocException('Apidoc %s: expected a docstring'
' for function %s'
% (self.__class__.__name__, f.__name__))
data['docstring'] = f.__doc__
route_re = re.compile('.*%s$' % data['route'])
endpoint_list = APIUrls.get_method_endpoints(f.__name__)
data['urls'] = [url for url in endpoint_list if
self.filter_api_url(url, route_re, data['noargs'])]
if 'args' in data:
data['examples'] = self.build_examples(
f, data['urls'], data['args'])
+ data['heading'] = '%s Documentation' % data['route']
+
# Prepare and send to mimetype selector if it's not a doc request
if re.match(route_re, request.url) and not data['noargs'] \
and request.method == 'GET':
return app.response_class(
render_template('apidoc.html', **data),
content_type='text/html')
g.doc_env = data # Store for response processing
return rv
@property
def data(self):
data = {'route': self.route, 'noargs': self.noargs}
doc_instance = self.inner_dec
while doc_instance:
if isinstance(doc_instance, arg):
if 'args' not in data:
data['args'] = []
data['args'].append(doc_instance.data)
elif isinstance(doc_instance, raises):
if 'excs' not in data:
data['excs'] = []
data['excs'].append(doc_instance.data)
elif isinstance(doc_instance, returns):
data['return'] = doc_instance.data
elif isinstance(doc_instance, header):
if 'headers' not in data:
data['headers'] = []
data['headers'].append(doc_instance.data)
elif isinstance(doc_instance, param):
if 'params' not in data:
data['params'] = []
data['params'].append(doc_instance.data)
else:
raise APIDocException('Unknown API documentation decorator')
doc_instance = doc_instance.inner_dec
return data
class BaseDescribeDocBase(APIDocBase):
"""Base description of optional input/output setup for a route.
"""
def __init__(self):
self.doc_data = None
self.inner_dec = None
def __call__(self, f):
@wraps(f)
def arg_fun(*args, outer_decorator=None, **kwargs):
kwargs['outer_decorator'] = outer_decorator
return self.maintain_stack(f, args, kwargs)
return arg_fun
@property
def data(self):
return self.doc_data
class arg(BaseDescribeDocBase): # noqa: N801
"""
Decorate an API method to display an argument's information on the doc
page specified by @route above.
Args:
name: the argument's name. MUST match the method argument's name to
create the example request URL.
default: the argument's default value
argtype: the argument's type as an Enum value from apidoc.argtypes
argdoc: the argument's documentation string
"""
def __init__(self, name, default, argtype, argdoc):
super().__init__()
self.doc_data = {
'name': name,
'type': argtype.value,
'doc': argdoc,
'default': default
}
class header(BaseDescribeDocBase): # noqa: N801
"""
Decorate an API method to display header information the api can
potentially return in the response.
Args:
name: the header name
doc: the information about that header
"""
def __init__(self, name, doc):
super().__init__()
self.doc_data = {
'name': name,
'doc': doc,
}
class param(BaseDescribeDocBase): # noqa: N801
"""Decorate an API method to display query parameter information the
api can potentially accept.
Args:
name: parameter's name
default: parameter's default value
argtype: parameter's type as an Enum value from apidoc.argtypes
doc: the information about that header
"""
def __init__(self, name, default, argtype, doc):
super().__init__()
self.doc_data = {
'name': name,
'type': argtype.value,
'default': default,
'doc': doc,
}
class raises(BaseDescribeDocBase): # noqa: N801
"""Decorate an API method to display information pertaining to an exception
that can be raised by this method.
Args:
exc: the exception name as an Enum value from apidoc.excs
doc: the exception's documentation string
"""
def __init__(self, exc, doc):
super().__init__()
self.doc_data = {
'exc': exc.value,
'doc': doc
}
class returns(BaseDescribeDocBase): # noqa: N801
"""Decorate an API method to display information about its return value.
Args:
rettype: the return value's type as an Enum value from
apidoc.rettypes retdoc: the return value's documentation
string
"""
def __init__(self, rettype=None, retdoc=None):
super().__init__()
self.doc_data = {
'type': rettype.value,
'doc': retdoc
}
diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py
index c4f36b3f..aee3cb46 100644
--- a/swh/web/ui/renderers.py
+++ b/swh/web/ui/renderers.py
@@ -1,286 +1,286 @@
# Copyright (C) 2015-2017 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import re
import yaml
import json
from docutils.core import publish_parts
from docutils.writers.html4css1 import Writer, HTMLTranslator
from inspect import cleandoc
from jinja2 import escape, Markup
from flask import request, Response, render_template
from flask import g
from pygments import highlight
from pygments.lexers import guess_lexer
from pygments.formatters import HtmlFormatter
from swh.web.ui import utils
class SWHFilterEnricher():
"""Global filter on fields.
"""
@classmethod
def filter_by_fields(cls, data):
"""Extract a request parameter 'fields' if it exists to permit the
filtering on the data dict's keys.
If such field is not provided, returns the data as is.
"""
fields = request.args.get('fields')
if fields:
fields = set(fields.split(','))
data = utils.filter_field_keys(data, fields)
return data
class SWHComputeLinkHeader:
"""Add link header to response.
Mixin intended to be used for example in SWHMultiResponse
"""
@classmethod
def compute_link_header(cls, rv, options):
"""Add Link header in returned value results.
Expects rv to be a dict with 'results' and 'headers' key:
'results': the returned value expected to be shown
'headers': dictionary with link-next and link-prev
Args:
rv (dict): with keys:
- 'headers': potential headers with 'link-next'
and 'link-prev' keys
- 'results': containing the result to return
options (dict): the initial dict to update with result if any
Returns:
Dict with optional keys 'link-next' and 'link-prev'.
"""
link_headers = []
if 'headers' not in rv:
return {}
rv_headers = rv['headers']
if 'link-next' in rv_headers:
link_headers.append('<%s>; rel="next"' % (
rv_headers['link-next']))
if 'link-prev' in rv_headers:
link_headers.append('<%s>; rel="previous"' % (
rv_headers['link-prev']))
if link_headers:
link_header_str = ','.join(link_headers)
headers = options.get('headers', {})
headers.update({
'Link': link_header_str
})
return headers
return {}
class SWHTransformProcessor:
"""Transform an eventual returned value with multiple layer of
information with only what's necessary.
If the returned value rv contains the 'results' key, this is the
associated value which is returned.
Otherwise, return the initial dict without the potential 'headers'
key.
"""
@classmethod
def transform(cls, rv):
if 'results' in rv:
return rv['results']
if 'headers' in rv:
rv.pop('headers')
return rv
class SWHMultiResponse(Response, SWHFilterEnricher,
SWHComputeLinkHeader, SWHTransformProcessor):
"""
A Flask Response subclass.
Override force_type to transform dict/list responses into callable Flask
response objects whose mimetype matches the request's Accept header: HTML
template render, YAML dump or default to a JSON dump.
"""
@classmethod
def make_response_from_mimetype(cls, rv, options={}):
options = options.copy()
if not (isinstance(rv, list) or isinstance(rv, dict)):
return rv
def wants_html(best_match):
return best_match == 'text/html' and \
request.accept_mimetypes[best_match] > \
request.accept_mimetypes['application/json']
def wants_yaml(best_match):
return best_match == 'application/yaml' and \
request.accept_mimetypes[best_match] > \
request.accept_mimetypes['application/json']
acc_mime = ['application/json', 'application/yaml', 'text/html']
best_match = request.accept_mimetypes.best_match(acc_mime)
options['headers'] = cls.compute_link_header(rv, options)
rv = cls.transform(rv)
rv = cls.filter_by_fields(rv)
if wants_html(best_match):
data = json.dumps(rv, sort_keys=True,
indent=4, separators=(',', ': '))
env = g.get('doc_env', {})
env['response_data'] = data
env['headers_data'] = None
if options and 'headers' in options:
env['headers_data'] = options['headers']
env['request'] = request
- env['short_path'] = utils.shorten_path(str(request.path))
+ env['heading'] = utils.shorten_path(str(request.path))
env['status_code'] = options.get('status', 200)
rv = Response(render_template('apidoc.html', **env),
content_type='text/html',
**options)
elif wants_yaml(best_match):
rv = Response(
yaml.dump(rv),
content_type='application/yaml',
**options)
else:
# jsonify is unhappy with lists in Flask 0.10.1, use json.dumps
rv = Response(
json.dumps(rv),
content_type='application/json',
**options)
return rv
@classmethod
def force_type(cls, rv, environ=None):
if isinstance(rv, dict) or isinstance(rv, list):
rv = cls.make_response_from_mimetype(rv)
return super().force_type(rv, environ)
def error_response(error_code, error):
"""Private function to create a custom error response.
"""
error_opts = {'status': error_code}
error_data = {'error': str(error)}
return SWHMultiResponse.make_response_from_mimetype(error_data,
options=error_opts)
def urlize_api_links(text):
"""Utility function for decorating api links in browsable api.
Args:
text: whose content matching links should be transformed into
contextual API or Browse html links.
Returns
The text transformed if any link is found.
The text as is otherwise.
"""
return re.sub(r'(/api/.*/|/browse/.*/)',
r'<a href="\1">\1</a>',
str(escape(text)))
def urlize_header_links(text):
"""Utility function for decorating headers links in browsable api.
Args
text: Text whose content contains Link header value
Returns:
The text transformed with html link if any link is found.
The text as is otherwise.
"""
return re.sub(r'<(/api/.*|/browse/.*)>', r'<<a href="\1">\1</a>>',
text)
class NoHeaderHTMLTranslator(HTMLTranslator):
"""
Docutils translator subclass to customize the generation of HTML
from reST-formatted docstrings
"""
def __init__(self, document):
super().__init__(document)
self.body_prefix = []
self.body_suffix = []
def visit_bullet_list(self, node):
self.context.append((self.compact_simple, self.compact_p))
self.compact_p = None
self.compact_simple = self.is_compactable(node)
self.body.append(self.starttag(node, 'ul', CLASS='docstring'))
DOCSTRING_WRITER = Writer()
DOCSTRING_WRITER.translator_class = NoHeaderHTMLTranslator
def safe_docstring_display(docstring):
"""
Utility function to htmlize reST-formatted documentation in browsable
api.
"""
docstring = cleandoc(docstring)
return publish_parts(docstring, writer=DOCSTRING_WRITER)['html_body']
def revision_id_from_url(url):
"""Utility function to obtain a revision's ID from its browsing URL."""
return re.sub(r'/browse/revision/([0-9a-f]{40}|[0-9a-f]{64})/.*',
r'\1', url)
def highlight_source(source_code_as_text):
"""Leverage pygments to guess and highlight source code.
Args
source_code_as_text (str): source code in plain text
Returns:
Highlighted text if possible or plain text otherwise
"""
try:
maybe_lexer = guess_lexer(source_code_as_text)
if maybe_lexer:
r = highlight(
source_code_as_text, maybe_lexer,
HtmlFormatter(linenos=True,
lineanchors='l',
anchorlinenos=True))
else:
r = '<pre>%s</pre>' % source_code_as_text
except:
r = '<pre>%s</pre>' % source_code_as_text
return Markup(r)
diff --git a/swh/web/ui/templates/apidoc.html b/swh/web/ui/templates/apidoc.html
index 48d874de..5688ca1a 100644
--- a/swh/web/ui/templates/apidoc.html
+++ b/swh/web/ui/templates/apidoc.html
@@ -1,128 +1,128 @@
{% extends "layout.html" %}
-{% block title %}{{ short_path }} &ndash; Software Heritage API {% endblock %}
+{% block title %}{{ heading }} &ndash; Software Heritage API {% endblock %}
{% block content %}
<nav class="bread-crumbs">
<ul>
<li><a href="/">API</a></li>
<li>/</li>
<li><a href="/api/1/">endpoints</a></li>
<li>/</li>
<li>{{ request.path }}</li>
</ul>
</nav>
{% if docstring %}
<div class="docstring">
<h2> Description </h2>
{{ docstring | safe_docstring_display | safe }}
</div>
{% endif %}
{% if response_data and response_data is not none %}
<div class="response-data">
<h2>Request</h2>
<pre><strong>{{ request.method }}</strong> {{ request.url }}</pre>
<h2>Response</h2>
{% if status_code != 200 %}
<h3>Status Code</h3>
<pre class="error">{{ status_code }}</pre>
{% endif %}
{% if headers_data and headers_data is not none %}
<h3>Headers</h3>
{% for header_name, header_value in headers_data.items() %}
<pre><strong>{{ header_name }}</strong> {{ header_value | urlize_header_links | safe }}</pre>
{% endfor %}
{% endif %}
<h3>Body</h3>
<pre>{{ response_data | urlize_api_links | safe }}</pre>
</div>
{% endif %}
<hr/>
<div class="doc-urls">
<table class="m-x-auto table">
<thead>
<tr>
<th>URL</th>
<th>Allowed Methods</th>
</tr>
</thead>
<tbody>
{% for url in urls %}
<tr>
<td>
{{ url['rule'] }}
</td>
<td>
{{ url['methods'] | sort | join(', ') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<hr/>
{% if args and args|length > 0 %}
<div class="doc-args">
<h2> Args </h2>
<dl class="doc-argslist dl-horizontal">
{% for arg in args %}
<dt> {{ arg['name'] }}: {{ arg['type'] }} </dt>
<dd> {{ arg['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if params and params|length > 0 %}
<div class="doc-params">
<h2> Params </h2>
<dl class="doc-paramslist dl-horizontal">
{% for param in params %}
<dt> {{ param['name'] }}: {{ param['type'] }} </dt>
<dd> {{ param['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if excs and excs|length > 0 %}
<div class="doc-excs">
<h2> Raises </h2>
<dl class="doc-excslist dl-horizontal">
{% for exc in excs %}
<dt> {{ exc['exc'] }} </dt>
<dd> {{ exc['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if headers %}
<div class="doc-headers">
<h2> Headers </h2>
<dl class="doc-headers dl-horizontal">
{% for header in headers %}
<dt> {{ header['name'] }}: string </dt>
<dd> {{ header['doc'] | safe_docstring_display | safe }} </dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% if return %}
<div class="doc-return">
<h2> Returns </h2>
<dl class="doc-return dl-horizontal">
<dt> {{ return['type'] }} </dt>
<dd> {{ return['doc'] | safe_docstring_display | safe }} </dd>
</dl>
</div>
{% endif %}
{% if examples %}
<div class="doc-example">
<h2> Examples </h2>
<dl class="doc-example dl-horizontal">
{% for example in examples %}
<dd>
<a href="{{ example }}">{{ example }}</a>
</dd>
{% endfor %}
</dl>
</div>
{% endif %}
{% endblock %}
diff --git a/swh/web/ui/tests/test_renderers.py b/swh/web/ui/tests/test_renderers.py
index c21a8625..2a6b8543 100644
--- a/swh/web/ui/tests/test_renderers.py
+++ b/swh/web/ui/tests/test_renderers.py
@@ -1,326 +1,326 @@
# Copyright (C) 2015-2017 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import json
import unittest
import yaml
from flask import Response
from nose.tools import istest
from unittest.mock import patch, MagicMock
from swh.web.ui import renderers
class SWHComputeLinkHeaderTest(unittest.TestCase):
@istest
def compute_link_header(self):
rv = {
'headers': {'link-next': 'foo', 'link-prev': 'bar'},
'results': [1, 2, 3]
}
options = {}
# when
headers = renderers.SWHComputeLinkHeader.compute_link_header(
rv, options)
self.assertEquals(headers, {
'Link': '<foo>; rel="next",<bar>; rel="previous"',
})
@istest
def compute_link_header_nothing_changed(self):
rv = {}
options = {}
# when
headers = renderers.SWHComputeLinkHeader.compute_link_header(
rv, options)
self.assertEquals(headers, {})
@istest
def compute_link_header_nothing_changed_2(self):
rv = {'headers': {}}
options = {}
# when
headers = renderers.SWHComputeLinkHeader.compute_link_header(
rv, options)
self.assertEquals(headers, {})
class SWHTransformProcessorTest(unittest.TestCase):
@istest
def transform_only_return_results_1(self):
rv = {'results': {'some-key': 'some-value'}}
self.assertEquals(renderers.SWHTransformProcessor.transform(rv),
{'some-key': 'some-value'})
@istest
def transform_only_return_results_2(self):
rv = {'headers': {'something': 'do changes'},
'results': {'some-key': 'some-value'}}
self.assertEquals(renderers.SWHTransformProcessor.transform(rv),
{'some-key': 'some-value'})
@istest
def transform_do_remove_headers(self):
rv = {'headers': {'something': 'do changes'},
'some-key': 'some-value'}
self.assertEquals(renderers.SWHTransformProcessor.transform(rv),
{'some-key': 'some-value'})
@istest
def transform_do_nothing(self):
rv = {'some-key': 'some-value'}
self.assertEquals(renderers.SWHTransformProcessor.transform(rv),
{'some-key': 'some-value'})
class RendererTestCase(unittest.TestCase):
@patch('swh.web.ui.renderers.g')
@patch('swh.web.ui.renderers.json')
@patch('swh.web.ui.renderers.request')
@patch('swh.web.ui.renderers.render_template')
@patch('swh.web.ui.renderers.SWHMultiResponse.filter_by_fields')
@patch('swh.web.ui.utils.shorten_path')
@istest
def swh_multi_response_mimetype_html(self, mock_shorten_path, mock_filter,
mock_render, mock_request, mock_json,
mock_g):
# given
data = {
'data': [12, 34],
'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'
}
mock_g.get.return_value = {'my_key': 'my_display_value'}
# mock_enricher.return_value = (data, {})
mock_filter.return_value = data
mock_shorten_path.return_value = 'my_short_path'
expected_env = {
'my_key': 'my_display_value',
'response_data': json.dumps(data),
'request': mock_request,
'headers_data': {},
- 'short_path': 'my_short_path',
+ 'heading': 'my_short_path',
'status_code': 200,
}
def mock_mimetypes(key):
mimetypes = {
'text/html': 10,
'application/json': 0.1,
'application/yaml': 0.1
}
return mimetypes[key]
accept_mimetypes = MagicMock()
accept_mimetypes.__getitem__.side_effect = mock_mimetypes
accept_mimetypes.best_match = MagicMock(return_value='text/html')
mock_request.accept_mimetypes = accept_mimetypes
mock_json.dumps.return_value = json.dumps(data)
# when
rv = renderers.SWHMultiResponse.make_response_from_mimetype(data)
# then
# mock_enricher.assert_called_once_with(data, {})
mock_filter.assert_called_once_with(data)
mock_render.assert_called_with('apidoc.html', **expected_env)
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'text/html')
@patch('swh.web.ui.renderers.g')
@patch('swh.web.ui.renderers.yaml')
@patch('swh.web.ui.renderers.request')
@patch('swh.web.ui.renderers.SWHMultiResponse.filter_by_fields')
@istest
def swh_multi_response_mimetype_yaml(self, mock_filter,
mock_request, mock_yaml, mock_g):
# given
data = {'data': [12, 34],
'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'}
def mock_mimetypes(key):
mimetypes = {
'application/yaml': 10,
'application/json': 0.1,
'text/html': 0.1
}
return mimetypes[key]
accept_mimetypes = MagicMock()
accept_mimetypes.__getitem__.side_effect = mock_mimetypes
accept_mimetypes.best_match = MagicMock(
return_value='application/yaml')
mock_request.accept_mimetypes = accept_mimetypes
mock_yaml.dump.return_value = yaml.dump(data)
mock_filter.return_value = data
# when
rv = renderers.SWHMultiResponse.make_response_from_mimetype(data)
# then
mock_filter.assert_called_once_with(data)
mock_yaml.dump.assert_called_once_with(data)
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'application/yaml')
self.assertEqual(data, yaml.load(rv.data.decode('utf-8')))
@patch('swh.web.ui.renderers.g')
@patch('swh.web.ui.renderers.json')
@patch('swh.web.ui.renderers.request')
@patch('swh.web.ui.renderers.SWHMultiResponse.filter_by_fields')
@istest
def swh_multi_response_mimetype_json(self, mock_filter,
mock_request, mock_json, mock_g):
# given
data = {'data': [12, 34],
'id': 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc'}
def mock_mimetypes(key):
mimetypes = {
'application/json': 10,
'text/html': 0.1,
'application/yaml': 0.1
}
return mimetypes[key]
accept_mimetypes = MagicMock()
accept_mimetypes.__getitem__.side_effect = mock_mimetypes
accept_mimetypes.best_match = MagicMock(
return_value='application/json')
mock_request.accept_mimetypes = accept_mimetypes
mock_json.dumps.return_value = json.dumps(data)
mock_filter.return_value = data
# when
rv = renderers.SWHMultiResponse.make_response_from_mimetype(data)
# then
mock_filter.assert_called_once_with(data)
mock_json.dumps.assert_called_once_with(data)
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'application/json')
self.assertEqual(data, json.loads(rv.data.decode('utf-8')))
@patch('swh.web.ui.renderers.request')
@istest
def swh_multi_response_make_response_not_list_dict(self, mock_request):
# given
incoming = Response()
# when
rv = renderers.SWHMultiResponse.make_response_from_mimetype(incoming)
# then
self.assertEqual(rv, incoming)
@patch('swh.web.ui.renderers.request')
@istest
def swh_filter_renderer_do_nothing(self, mock_request):
# given
mock_request.args = {}
swh_filter_renderer = renderers.SWHFilterEnricher()
input_data = {'a': 'some-data'}
# when
actual_data = swh_filter_renderer.filter_by_fields(input_data)
# then
self.assertEquals(actual_data, input_data)
@patch('swh.web.ui.renderers.utils')
@patch('swh.web.ui.renderers.request')
@istest
def swh_filter_renderer_do_filter(self, mock_request, mock_utils):
# given
mock_request.args = {'fields': 'a,c'}
mock_utils.filter_field_keys.return_value = {'a': 'some-data'}
swh_filter_user = renderers.SWHMultiResponse()
input_data = {'a': 'some-data',
'b': 'some-other-data'}
# when
actual_data = swh_filter_user.filter_by_fields(input_data)
# then
self.assertEquals(actual_data, {'a': 'some-data'})
mock_utils.filter_field_keys.assert_called_once_with(input_data,
{'a', 'c'})
@istest
def urlize_api_links_api(self):
# update api link with html links content with links
content = '{"url": "/api/1/abc/"}'
expected_content = ('{&#34;url&#34;: '
'&#34;<a href="/api/1/abc/">/api/1/abc/</a>&#34;}')
self.assertEquals(renderers.urlize_api_links(content),
expected_content)
@istest
def urlize_api_links_browse(self):
# update /browse link with html links content with links
content = '{"url": "/browse/def/"}'
expected_content = ('{&#34;url&#34;: '
'&#34;<a href="/browse/def/">'
'/browse/def/</a>&#34;}')
self.assertEquals(renderers.urlize_api_links(content),
expected_content)
@istest
def urlize_header_links(self):
# update api link with html links content with links
content = """</api/1/abc/>; rel="next"
</api/1/def/>; rel="prev"
"""
expected_content = """<<a href="/api/1/abc/">/api/1/abc/</a>>; rel="next"
<<a href="/api/1/def/">/api/1/def/</a>>; rel="prev"
"""
self.assertEquals(renderers.urlize_header_links(content),
expected_content)
@istest
def revision_id_from_url(self):
url = ('/browse/revision/9ba4bcb645898d562498ea66a0df958ef0e7a68c/'
'prev/9ba4bcb645898d562498ea66a0df958ef0e7aaaa/')
expected_id = '9ba4bcb645898d562498ea66a0df958ef0e7a68c'
self.assertEqual(renderers.revision_id_from_url(url), expected_id)
@istest
def safe_docstring_display(self):
# update api link with html links content with links
docstring = """This is my list header:
- Here is item 1, with a continuation
line right here
- Here is item 2
Here is something that is not part of the list"""
expected_docstring = """<p>This is my list header:</p>
<ul class="docstring">
<li>Here is item 1, with a continuation
line right here</li>
<li>Here is item 2</li>
</ul>
<p>Here is something that is not part of the list</p>
"""
self.assertEquals(renderers.safe_docstring_display(docstring),
expected_docstring)

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jul 4, 3:35 PM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3323175

Event Timeline