Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9345505
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
38 KB
Subscribers
None
View Options
diff --git a/README-dev.md b/README-dev.md
index 1608149c..8807738b 100644
--- a/README-dev.md
+++ b/README-dev.md
@@ -1,53 +1,47 @@
README-dev
==========
# modules' description
## Main
swh.web.ui.main: Start the server or the dev server
## Layers
Folder swh/web/ui/:
- api main api endpoints definition (api)
- views main ui endpoints (web app)
- service Orchestration layer used by api/view module.
In charge of communication with `backend` to retrieve
information and conversion for the upper layer.
- backend Lower layer in charge of communication with swh storage.
Used by `service` module.
In short:
1. views -depends-> api -depends-> service -depends-> backend ----asks----> swh-storage
2. views <- api <- service <- backend <----rets---- swh-storage
## Utilities
Folder swh/web/ui/:
- apidoc Browsable api functions.
- exc Exception definitions.
- errorhandler Exception (defined in `exc`) handlers.
Use at route definition time (`api`, `views`).
- renderers Rendering utilities (html, json, yaml, data...).
Use at route definition time (`api`, `views`).
- converters conversion layer to transform swh data to serializable data.
Used by `service` to convert data before transmitting to `api` or `views`.
- query Utilities to parse data from http endpoints.
Used by `service`
- upload Utility module to deal with upload of data to webapp or api.
Used by `api`
- utils Utilities used throughout swh-web-ui.
-### About apidoc
-
-This is a 'decorator tower' that stores the data associated with the documentation upon loading
-the apidoc module. The top decorator of any tower should be @apidoc.route(). Apidoc raises an
-exception if this decorator is missing, and flask raises an exception if it is present but not at
-the top of the tower.
## Graphics summary

diff --git a/swh/web/ui/apidoc.py b/swh/web/ui/apidoc.py
index 0f273773..d0c8e217 100644
--- a/swh/web/ui/apidoc.py
+++ b/swh/web/ui/apidoc.py
@@ -1,342 +1,242 @@
# Copyright (C) 2015 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 = 'algo_hash: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):
"""
Add a route to the self-documenting API reference
"""
if route not in cls.apidoc_routes:
cls.apidoc_routes[route] = docstring
-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'}
- if f.__code__.co_argcount > 0 and (args, nargs) == ((), {}):
- rv = None # Documentation call
- else:
- rv = f(*args, **nargs)
- 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
+class route(object): # 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.
-
+ Caution: decorating a method with this requires to also decorate it
+ __at least__ with @returns, or breaks the decorated endpoint
Args:
route: the 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
"""
def __init__(self, route, noargs=False):
- super().__init__()
self.route = route
self.noargs = noargs
def __call__(self, f):
APIUrls.index_add_route(self.route, f.__doc__)
@wraps(f)
def doc_func(*args, **kwargs):
- kwargs['outer_decorator'] = self
- rv = self.process_rv(f, args, kwargs)
- return self.compute_return(f, rv)
+ return f(call_args=(args, kwargs),
+ doc_route=self.route,
+ noargs=self.noargs)
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 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__)
- other_urls = [url for url in endpoint_list if
- self.filter_api_url(url, route_re, data['noargs'])]
- data['urls'] = other_urls
-
- # Build example endpoint URL
- if 'args' in data:
- defaults = {arg['name']: arg['default'] for arg in data['args']}
- example = url_for(f.__name__, **defaults)
- data['example'] = re.sub(r'(.*)\?.*', r'\1', example)
-
- # 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
- else:
- raise APIDocException('Unknown API documentation decorator')
- doc_instance = doc_instance.inner_dec
-
- return data
-
-class arg(APIDocBase): # noqa: N801
+class arg(object): # 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_dict = {
'name': name,
'type': argtype.value,
'doc': argdoc,
'default': default
}
- 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)
+ def arg_fun(*args, **kwargs):
+ if 'args' in kwargs:
+ kwargs['args'].append(self.doc_dict)
+ else:
+ kwargs['args'] = [self.doc_dict]
+ return f(*args, **kwargs)
return arg_fun
- @property
- def data(self):
- return self.doc_dict
-
-class raises(APIDocBase): # noqa: N801
+class raises(object): # 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.exc_dict = {
'exc': exc.value,
'doc': doc
}
def __call__(self, f):
@wraps(f)
- def exc_fun(*args, outer_decorator=None, **kwargs):
- kwargs['outer_decorator'] = outer_decorator
- return self.maintain_stack(f, args, kwargs)
+ def exc_fun(*args, **kwargs):
+ if 'excs' in kwargs:
+ kwargs['excs'].append(self.exc_dict)
+ else:
+ kwargs['excs'] = [self.exc_dict]
+ return f(*args, **kwargs)
return exc_fun
- @property
- def data(self):
- return self.exc_dict
-
-class returns(APIDocBase): # noqa: N801
+class returns(object): # noqa: N801
"""
Decorate an API method to display information about its return value.
+ Caution: this MUST be the last decorator in the apidoc decorator stack,
+ or the decorated endpoint breaks
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.return_dict = {
'type': rettype.value,
'doc': retdoc
}
+ 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 __call__(self, f):
@wraps(f)
- def ret_fun(*args, outer_decorator=None, **kwargs):
- kwargs['outer_decorator'] = outer_decorator
- return self.maintain_stack(f, args, kwargs)
+ def ret_fun(*args, **kwargs):
+ # Build documentation
+ env = {
+ 'docstring': f.__doc__,
+ 'route': kwargs['doc_route'],
+ 'return': self.return_dict
+ }
+
+ for arg in ['args', 'excs']:
+ if arg in kwargs:
+ env[arg] = kwargs[arg]
+
+ route_re = re.compile('.*%s$' % kwargs['doc_route'])
+ endpoint_list = APIUrls.get_method_endpoints(f.__name__)
+ other_urls = [url for url in endpoint_list if
+ self.filter_api_url(url, route_re, kwargs['noargs'])]
+ env['urls'] = other_urls
+
+ # Build example endpoint URL
+ if 'args' in env:
+ defaults = {arg['name']: arg['default'] for arg in env['args']}
+ example = url_for(f.__name__, **defaults)
+ env['example'] = re.sub(r'(.*)\?.*', r'\1', example)
+
+ # Prepare and send to mimetype selector if it's not a doc request
+ if re.match(route_re, request.url) and not kwargs['noargs'] \
+ and request.method == 'GET':
+ return app.response_class(
+ render_template('apidoc.html', **env),
+ content_type='text/html')
+
+ cargs, ckwargs = kwargs['call_args']
+ g.doc_env = env # Store for response processing
+ return f(*cargs, **ckwargs)
return ret_fun
-
- @property
- def data(self):
- return self.return_dict
diff --git a/swh/web/ui/renderers.py b/swh/web/ui/renderers.py
index d617bd3d..09b00529 100644
--- a/swh/web/ui/renderers.py
+++ b/swh/web/ui/renderers.py
@@ -1,145 +1,147 @@
# Copyright (C) 2015 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 flask import request, Response, render_template
from flask import g
from swh.web.ui import utils
class SWHFilterEnricher():
"""Global filter on fields.
"""
def filter_by_fields(self, 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 SWHMultiResponse(Response, SWHFilterEnricher):
"""
A Flask Response subclass.
- Override force_type to transform dict/list responses into callable Flask
+ Override force_type to transform dict 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={}):
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']
rv = cls.filter_by_fields(cls, rv)
acc_mime = ['application/json', 'application/yaml', 'text/html']
best_match = request.accept_mimetypes.best_match(acc_mime)
-
+ # return a template render
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['request'] = request
rv = Response(render_template('apidoc.html', **env),
content_type='text/html',
**options)
+ # return formatted yaml
elif wants_yaml(best_match):
rv = Response(
yaml.dump(rv),
content_type='application/yaml',
**options)
+ # return formatted json
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(content):
"""Utility function for decorating api links in browsable api."""
return re.sub(r'"(/api/.*|/browse/.*)"', r'"<a href="\1">\1</a>"', content)
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)
diff --git a/swh/web/ui/tests/test_apidoc.py b/swh/web/ui/tests/test_apidoc.py
index 77930edc..3ce98372 100644
--- a/swh/web/ui/tests/test_apidoc.py
+++ b/swh/web/ui/tests/test_apidoc.py
@@ -1,125 +1,346 @@
# Copyright (C) 2015 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
-from nose.tools import istest, nottest
+from unittest.mock import MagicMock, patch
+from nose.tools import istest
from swh.web.ui import apidoc
-from swh.web.ui.main import app
-from swh.web.ui.tests.test_app import SWHApidocTestCase
+from swh.web.ui.tests import test_app
-class APIDocTestCase(SWHApidocTestCase):
+class APIDocTestCase(test_app.SWHApidocTestCase):
def setUp(self):
-
self.arg_dict = {
'name': 'my_pretty_arg',
'default': 'some default value',
'type': apidoc.argtypes.sha1,
'doc': 'this arg does things'
}
self.stub_excs = [{'exc': apidoc.excs.badinput,
'doc': 'My exception documentation'}]
self.stub_args = [{'name': 'stub_arg',
'default': 'some_default'}]
self.stub_rule_list = [
{'rule': 'some/route/with/args/',
'methods': {'GET', 'HEAD', 'OPTIONS'}},
{'rule': 'some/doc/route/',
'methods': {'GET', 'HEAD', 'OPTIONS'}},
{'rule': 'some/other/route/',
'methods': {'GET', 'HEAD', 'OPTIONS'}}
]
self.stub_return = {
'type': apidoc.rettypes.dict.value,
'doc': 'a dict with amazing properties'
}
- @apidoc.route('/my/nodoc/url/')
- @nottest
- def apidoc_nodoc_tester(arga, argb):
- return arga + argb
+ @patch('swh.web.ui.apidoc.APIUrls')
+ @patch('swh.web.ui.apidoc.app')
+ @istest
+ def apidoc_route(self, mock_app, mock_api_urls):
+ # given
+ decorator = apidoc.route('/some/url/for/doc/')
+ mock_fun = MagicMock(return_value=123)
+ mock_fun.__doc__ = 'Some documentation'
+ mock_fun.__name__ = 'some_fname'
+ decorated = decorator.__call__(mock_fun)
+
+ # when
+ decorated('some', 'value', kws='and a kw')
+
+ # then
+ mock_fun.assert_called_once_with(
+ call_args=(('some', 'value'), {'kws': 'and a kw'}),
+ doc_route='/some/url/for/doc/',
+ noargs=False
+ )
+ mock_api_urls.index_add_route.assert_called_once_with(
+ '/some/url/for/doc/',
+ 'Some documentation')
+ mock_app.add_url_rule.assert_called_once_with(
+ '/some/url/for/doc/', 'some_fname', decorated)
@istest
- def apidoc_nodoc_failure(self):
- with self.assertRaises(Exception):
- self.client.get('/my/nodoc/url/')
+ def apidoc_arg_noprevious(self):
+ # given
+ decorator = apidoc.arg('my_pretty_arg',
+ default='some default value',
+ argtype=apidoc.argtypes.sha1,
+ argdoc='this arg does things')
+ mock_fun = MagicMock(return_value=123)
+ decorated = decorator.__call__(mock_fun)
+ self.arg_dict['type'] = self.arg_dict['type'].value
+
+ # when
+ decorated(call_args=((), {}), doc_route='some/route/')
+
+ # then
+ mock_fun.assert_called_once_with(
+ call_args=((), {}),
+ doc_route='some/route/',
+ args=[self.arg_dict]
+ )
@istest
- def apidoc_badorder_failure(self):
- with self.assertRaises(AssertionError):
- @app.route('/my/badorder/url/<int:foo>/')
- @apidoc.arg('foo',
- default=True,
- argtype=apidoc.argtypes.int,
- argdoc='It\'s so fluffy!')
- @apidoc.route('/my/badorder/url/')
- @nottest
- def apidoc_badorder_tester(foo, bar=0):
- """
- Some irrelevant doc since the decorators are bad
- """
- return foo + bar
-
- @app.route('/some/<int:myarg>/<int:myotherarg>/')
- @apidoc.route('/some/doc/route/')
- @nottest
- def apidoc_route_tester(myarg, myotherarg, akw=0):
- """
- Sample doc
- """
- return {'result': myarg + myotherarg + akw}
+ def apidoc_arg_previous(self):
+ # given
+ decorator = apidoc.arg('my_other_arg',
+ default='some other value',
+ argtype=apidoc.argtypes.sha1,
+ argdoc='this arg is optional')
+ mock_fun = MagicMock(return_value=123)
+ decorated = decorator.__call__(mock_fun)
+
+ # when
+ decorated(call_args=((), {}),
+ doc_route='some/route/',
+ args=[self.arg_dict])
+
+ # then
+ mock_fun.assert_called_once_with(
+ call_args=((), {}),
+ doc_route='some/route/',
+ args=[self.arg_dict,
+ {'name': 'my_other_arg',
+ 'default': 'some other value',
+ 'type': apidoc.argtypes.sha1.value,
+ 'doc': 'this arg is optional'}])
@istest
- def apidoc_route_doc(self):
+ def apidoc_raises_noprevious(self):
+ # given
+ decorator = apidoc.raises(exc=apidoc.excs.badinput,
+ doc='My exception documentation')
+ mock_fun = MagicMock(return_value=123)
+ decorated = decorator.__call__(mock_fun)
+ self.stub_excs[0]['exc'] = self.stub_excs[0]['exc'].value
+
# when
- rv = self.client.get('/some/doc/route/')
+ decorated(call_args=((), {}), doc_route='some/route/')
# then
- self.assertEqual(rv.status_code, 200)
- self.assert_template_used('apidoc.html')
+ mock_fun.assert_called_once_with(
+ call_args=((), {}),
+ doc_route='some/route/',
+ excs=self.stub_excs
+ )
@istest
- def apidoc_route_fn(self):
+ def apidoc_raises_previous(self):
+ # given
+ decorator = apidoc.raises(exc=apidoc.excs.notfound,
+ doc='Another documentation')
+ mock_fun = MagicMock(return_value=123)
+ decorated = decorator.__call__(mock_fun)
+ expected_excs = self.stub_excs + [{
+ 'exc': apidoc.excs.notfound.value,
+ 'doc': 'Another documentation'}]
+ expected_excs[0]['exc'] = expected_excs[0]['exc'].value
# when
- rv = self.client.get('/some/1/1/')
+ decorated(call_args=((), {}),
+ doc_route='some/route/',
+ excs=self.stub_excs)
# then
- self.assertEqual(rv.status_code, 200)
-
- @app.route('/some/full/<int:myarg>/<int:myotherarg>/')
- @apidoc.route('/some/complete/doc/route/')
- @apidoc.arg('myarg',
- default=67,
- argtype=apidoc.argtypes.int,
- argdoc='my arg')
- @apidoc.raises(exc=apidoc.excs.badinput, doc='Oops')
- @apidoc.returns(rettype=apidoc.rettypes.dict,
- retdoc='sum of args')
- @nottest
- def apidoc_full_stack_tester(myarg, myotherarg, akw=0):
- """
- Sample doc
- """
- return {'result': myarg + myotherarg + akw}
+ mock_fun.assert_called_once_with(
+ call_args=((), {}),
+ doc_route='some/route/',
+ excs=expected_excs)
+ @patch('swh.web.ui.apidoc.render_template')
+ @patch('swh.web.ui.apidoc.url_for')
+ @patch('swh.web.ui.apidoc.APIUrls')
+ @patch('swh.web.ui.apidoc.request')
@istest
- def apidoc_full_stack_doc(self):
+ def apidoc_returns_doc_call(self,
+ mock_request,
+ mock_api_urls,
+ mock_url_for,
+ mock_render):
+ # given
+ decorator = apidoc.returns(rettype=apidoc.rettypes.dict,
+ retdoc='a dict with amazing properties')
+ mock_fun = MagicMock(return_value=123)
+ mock_fun.__name__ = 'some_fname'
+ mock_fun.__doc__ = 'Some documentation'
+ decorated = decorator.__call__(mock_fun)
+
+ mock_api_urls.get_method_endpoints.return_value = self.stub_rule_list
+
+ mock_request.url = 'http://my-domain.tld/some/doc/route/'
+ mock_request.method = 'GET'
+ mock_url_for.return_value = 'http://my-domain.tld/meaningful_route/'
+
+ expected_env = {
+ 'urls': [{'rule': 'some/route/with/args/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}},
+ {'rule': 'some/other/route/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}}],
+ 'docstring': 'Some documentation',
+ 'args': self.stub_args,
+ 'excs': self.stub_excs,
+ 'route': 'some/doc/route/',
+ 'example': 'http://my-domain.tld/meaningful_route/',
+ 'return': self.stub_return
+ }
+
# when
- rv = self.client.get('/some/complete/doc/route/')
+ decorated(
+ docstring='Some documentation',
+ call_args=(('some', 'args'), {'kw': 'kwargs'}),
+ args=self.stub_args,
+ excs=self.stub_excs,
+ doc_route='some/doc/route/',
+ noargs=False
+ )
# then
- self.assertEqual(rv.status_code, 200)
- self.assert_template_used('apidoc.html')
+ self.assertEqual(mock_fun.call_args_list, []) # function not called
+ mock_render.assert_called_once_with(
+ 'apidoc.html',
+ **expected_env
+ )
+ @patch('swh.web.ui.apidoc.g')
+ @patch('swh.web.ui.apidoc.url_for')
+ @patch('swh.web.ui.apidoc.APIUrls')
+ @patch('swh.web.ui.apidoc.request')
@istest
- def apidoc_full_stack_fn(self):
+ def apidoc_returns_noargs(self,
+ mock_request,
+ mock_api_urls,
+ mock_url_for,
+ mock_g):
+
+ # given
+ decorator = apidoc.returns(rettype=apidoc.rettypes.dict,
+ retdoc='a dict with amazing properties')
+ mock_fun = MagicMock(return_value=123)
+ mock_fun.__name__ = 'some_fname'
+ mock_fun.__doc__ = 'Some documentation'
+ decorated = decorator.__call__(mock_fun)
+
+ mock_api_urls.get_method_endpoints.return_value = [
+ {'rule': 'some/doc/route/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}}]
+ mock_request.url = 'http://my-domain.tld/some/doc/route/'
+ doc_dict = {
+ 'urls': [
+ {'rule': 'some/doc/route/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}}],
+ 'docstring': 'Some documentation',
+ 'route': 'some/doc/route/',
+ 'return': {'type': apidoc.rettypes.dict.value,
+ 'doc': 'a dict with amazing properties'}
+ }
+
+ # when
+ decorated(
+ call_args=((), {}),
+ doc_route='some/doc/route/',
+ noargs=True
+ )
+
+ # then
+ mock_fun.assert_called_once_with()
+ self.assertEqual(mock_g.doc_env, doc_dict)
+
+ @patch('swh.web.ui.apidoc.g')
+ @patch('swh.web.ui.apidoc.url_for')
+ @patch('swh.web.ui.apidoc.APIUrls')
+ @patch('swh.web.ui.apidoc.request')
+ @istest
+ def apidoc_returns_same_fun(self,
+ mock_request,
+ mock_api_urls,
+ mock_url_for,
+ mock_g):
+
+ # given
+ decorator = apidoc.returns(rettype=apidoc.rettypes.dict,
+ retdoc='a dict with amazing properties')
+ mock_fun = MagicMock(return_value=123)
+ mock_fun.__name__ = 'some_fname'
+ mock_fun.__doc__ = 'Some documentation'
+ decorated = decorator.__call__(mock_fun)
+
+ mock_api_urls.get_method_endpoints.return_value = [
+ {'rule': 'some/doc/route/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}},
+ {'rule': 'some/doc/route/',
+ 'methods': {'POST'}}]
+ mock_request.url = 'http://my-domain.tld/some/doc/route/'
+ mock_request.method = 'POST'
+ doc_dict = {
+ 'urls': [{'rule': 'some/doc/route/',
+ 'methods': {'POST'}}],
+ 'docstring': 'Some documentation',
+ 'route': 'some/doc/route/',
+ 'return': {'type': apidoc.rettypes.dict.value,
+ 'doc': 'a dict with amazing properties'}
+ }
+
+ # when
+ decorated(
+ call_args=(('my', 'args'), {'kw': 'andkwargs'}),
+ doc_route='some/doc/route/',
+ noargs=False
+ )
+
+ # then
+ mock_fun.assert_called_once_with('my', 'args', kw='andkwargs')
+ self.assertEqual(mock_g.doc_env, doc_dict)
+
+ @patch('swh.web.ui.apidoc.g')
+ @patch('swh.web.ui.apidoc.url_for')
+ @patch('swh.web.ui.apidoc.APIUrls')
+ @patch('swh.web.ui.apidoc.request')
+ @istest
+ def apidoc_return_endpoint_call(self,
+ mock_request,
+ mock_api_urls,
+ mock_url_for,
+ mock_g):
+ # given
+ decorator = apidoc.returns(rettype=apidoc.rettypes.dict,
+ retdoc='a dict with amazing properties')
+ mock_fun = MagicMock(return_value=123)
+ mock_fun.__name__ = 'some_fname'
+ mock_fun.__doc__ = 'Some documentation'
+ decorated = decorator.__call__(mock_fun)
+
+ mock_api_urls.get_method_endpoints.return_value = self.stub_rule_list
+
+ mock_request.url = 'http://my-domain.tld/some/arg/route/'
+ mock_url_for.return_value = 'http://my-domain.tld/some/arg/route'
+
+ doc_dict = {
+ 'urls': [{'rule': 'some/route/with/args/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}},
+ {'rule': 'some/other/route/',
+ 'methods': {'GET', 'HEAD', 'OPTIONS'}}],
+ 'docstring': 'Some documentation',
+ 'args': self.stub_args,
+ 'excs': self.stub_excs,
+ 'route': 'some/doc/route/',
+ 'example': 'http://my-domain.tld/some/arg/route',
+ 'return': self.stub_return
+ }
+
# when
- rv = self.client.get('/some/full/1/1/')
+ decorated(
+ docstring='Some documentation',
+ call_args=(('some', 'args'), {'kw': 'kwargs'}),
+ args=self.stub_args,
+ excs=self.stub_excs,
+ noargs=False,
+ doc_route='some/doc/route/',
+ )
# then
- self.assertEqual(rv.status_code, 200)
+ mock_fun.assert_called_once_with('some', 'args', kw='kwargs')
+ self.assertEqual(mock_g.doc_env, doc_dict)
diff --git a/swh/web/ui/tests/test_app.py b/swh/web/ui/tests/test_app.py
index c4c9f5f7..4a218b79 100644
--- a/swh/web/ui/tests/test_app.py
+++ b/swh/web/ui/tests/test_app.py
@@ -1,91 +1,95 @@
# Copyright (C) 2015 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
# Functions defined here are NOT DESIGNED FOR PRODUCTION
import unittest
from swh.storage.api.client import RemoteStorage as Storage
from swh.web.ui import main
from flask.ext.testing import TestCase
# Because the Storage's __init__ function does side effect at startup...
class RemoteStorageAdapter(Storage):
def __init__(self, base_url):
self.base_url = base_url
def _init_mock_storage(base_url='https://somewhere.org:4321'):
"""Instanciate a remote storage whose goal is to be mocked in a test
context.
NOT FOR PRODUCTION
Returns:
An instance of swh.storage.api.client.RemoteStorage destined to be
mocked (it does not do any rest call)
"""
return RemoteStorageAdapter(base_url) # destined to be used as mock
def create_app(base_url='https://somewhere.org:4321'):
"""Function to initiate a flask app with storage designed to be mocked.
Returns:
Tuple:
- app test client (for testing api, client decorator from flask)
- application's full configuration
- the storage instance to stub and mock
- the main app without any decoration
NOT FOR PRODUCTION
"""
storage = _init_mock_storage(base_url)
# inject the mock data
conf = {'storage': storage,
'max_log_revs': 25}
main.app.config.update({'conf': conf})
if not main.app.config['TESTING']: # HACK: install controllers only once!
main.app.config['TESTING'] = True
main.load_controllers()
return main.app.test_client(), main.app.config, storage, main.app
+class SWHApidocTestCase(unittest.TestCase):
+ """Testing APIDoc class.
+
+ """
+ @classmethod
+ def setUpClass(cls):
+ cls.app, cls.app_config, cls.storage, _ = create_app()
+ cls.maxDiff = None
+
+
class SWHApiTestCase(unittest.TestCase):
"""Testing API class.
"""
@classmethod
def setUpClass(cls):
cls.app, cls.app_config, cls.storage, _ = create_app()
cls.maxDiff = None
class SWHViewTestCase(TestCase):
"""Testing view class.
cf. http://pythonhosted.org/Flask-Testing/
"""
# This inhibits template rendering
# render_templates = False
def create_app(self):
"""Initialize a Flask-Testing application instance to test view
without template rendering
"""
_, _, _, appToDecorate = create_app()
return appToDecorate
-
-
-class SWHApidocTestCase(SWHViewTestCase, SWHApiTestCase):
- """Testing APIDoc class.
-
- """
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jul 4, 3:23 PM (5 d, 8 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3246632
Attached To
rDWAPPS Web applications
Event Timeline
Log In to Comment