Skip to content

Commit

Permalink
Fix #28, decorators that change Response (make_response)
Browse files Browse the repository at this point in the history
  • Loading branch information
nycholas committed Aug 9, 2015
1 parent 2294fbe commit 60d6cbf
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 25 deletions.
26 changes: 26 additions & 0 deletions examples/decorator/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,29 @@ Testing your service
"id": "1",
"jsonrpc": "2.0"
}


::
$ curl -i -X POST -H "Content-Type: application/json; indent=4" \
-d '{
"jsonrpc": "2.0",
"method": "App.decorators",
"params": {},
"id": "1",
"terminal_id": 1
}' http://localhost:5000/api
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 231
X-JSONRPC-Tag: JSONRPC 2.0
Server: Werkzeug/0.10.4 Python/3.4.3
Date: Sun, 09 Aug 2015 17:00:16 GMT

{
"id": "1",
"jsonrpc": "2.0",
"result": {
"headers": "Host: localhost:5000\r\nUser-Agent: curl/7.43.0\r\nAccept: */*\r\nContent-Length: 134\r\nContent-Type: application/json; indent=4\r\n\r\n",
"terminal_id": 1
}
}
25 changes: 21 additions & 4 deletions examples/decorator/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import os
import sys

from flask import Flask, request
from flask import Flask, request, json

PROJECT_DIR, PROJECT_MODULE_NAME = os.path.split(
os.path.dirname(os.path.realpath(__file__))
Expand All @@ -40,25 +40,42 @@
and not FLASK_JSONRPC_PROJECT_DIR in sys.path:
sys.path.append(FLASK_JSONRPC_PROJECT_DIR)

from flask_jsonrpc import JSONRPC
from flask_jsonrpc import JSONRPC, make_response
from flask_jsonrpc.exceptions import OtherError

app = Flask(__name__)
jsonrpc = JSONRPC(app, '/api')

def check_terminal_id(fn):
def wrapped():
def wrapped(*args, **kwargs):
terminal_id = int(request.get_json(silent=True).get('terminal_id', 0))
if terminal_id <= 0:
raise OtherError('Invalid terminal ID')
return fn()
rv = fn(*args, **kwargs)
return rv
return wrapped

def jsonrcp_headers(fn):
def wrapped(*args, **kwargs):
response = make_response(fn(*args, **kwargs))
response.headers['X-JSONRPC-Tag'] = 'JSONRPC 2.0'
return response
return wrapped

@jsonrpc.method('App.index')
@check_terminal_id
def index():
return u'Terminal ID: {0}'.format(request.get_json(silent=True).get('terminal_id', 0))

@jsonrpc.method('App.decorators')
@check_terminal_id
@jsonrcp_headers
def decorators():
return {
'terminal_id': request.get_json(silent=True).get('terminal_id', 0),
'headers': str(request.headers)
}


if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
13 changes: 8 additions & 5 deletions flask_jsonrpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@
from functools import wraps
from inspect import getargspec

from flask.wrappers import Response
from flask import current_app, request, jsonify

from flask_jsonrpc.site import jsonrpc_site
from flask_jsonrpc._compat import (b, u, text_type, string_types,
OrderedDict, NativeStringIO)
from flask_jsonrpc.types import (Object, Number, Boolean, String, Array,
Nil, Any, Type)
from flask_jsonrpc.helpers import (jsonify_status_code,
from flask_jsonrpc.helpers import (make_response, jsonify_status_code,
extract_raw_data_request, authenticate)
from flask_jsonrpc.exceptions import (Error, ParseError, InvalidRequestError,
MethodNotFoundError, InvalidParamsError,
Expand Down Expand Up @@ -159,12 +160,14 @@ def _inject_args(sig, types):

def _site_api(site):
def wrapper(method=''):
response_dict, status_code = site.dispatch(request, method)
is_batch = type(response_dict) is list
response_obj, status_code = site.dispatch(request, method)
if isinstance(response_obj, Response):
return response_obj, response_obj.status_code
is_batch = type(response_obj) is list
if current_app.config['DEBUG']:
logging.debug('request: %s', extract_raw_data_request(request))
logging.debug('response: %s, %s', status_code, response_dict)
return jsonify_status_code(status_code, response_dict, is_batch=is_batch), status_code
logging.debug('response: %s, %s', status_code, response_obj)
return jsonify_status_code(status_code, response_obj, is_batch=is_batch), status_code
return wrapper


Expand Down
15 changes: 13 additions & 2 deletions flask_jsonrpc/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,22 @@
from __future__ import unicode_literals
from functools import wraps

from flask import current_app, request, jsonify, make_response, json
from flask import current_app, request, jsonify, json
from flask import make_response as flask_make_response

from flask_jsonrpc._compat import b, u, text_type
from flask_jsonrpc.exceptions import InvalidCredentialsError, InvalidParamsError

def make_response(*args):
"""Sometimes it is necessary to set additional headers in a view. Because
views do not have to return response objects but can return a value that
is converted into a response object by Flask itself, it becomes tricky to
add headers to it. This function can be called instead of using a return
and you will get a response object which you can use to attach headers.
"""
from flask_jsonrpc.site import jsonrpc_site
return jsonrpc_site.make_response(args)

def jsonify_status_code(status_code, *args, **kw):
"""Returns a jsonified response with the specified HTTP status code.
Expand All @@ -41,7 +52,7 @@ def jsonify_status_code(status_code, *args, **kw):
"""
is_batch = kw.pop('is_batch', False)
if is_batch:
response = make_response(json.dumps(*args, **kw))
response = flask_make_response(json.dumps(*args, **kw))
response.mimetype = 'application/json'
response.status_code = status_code
return response
Expand Down
60 changes: 50 additions & 10 deletions flask_jsonrpc/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from werkzeug.exceptions import HTTPException

from flask.wrappers import Response
from flask import json, jsonify, current_app
from flask import json, jsonify, request, make_response, current_app

from flask_jsonrpc.types import Object, Array, Any
from flask_jsonrpc.helpers import extract_raw_data_request
Expand Down Expand Up @@ -181,7 +181,7 @@ def apply_version_1_1(self, f, p):
def apply_version_1_0(self, f, p):
return f(*p)

def response_dict(self, request, D, version_hint=JSONRPC_VERSION_DEFAULT):
def response_obj(self, request, D, version_hint=JSONRPC_VERSION_DEFAULT):
version = version_hint
response = self.empty_response(version=version)
apply_version = {
Expand Down Expand Up @@ -236,6 +236,8 @@ def response_dict(self, request, D, version_hint=JSONRPC_VERSION_DEFAULT):
return None, 204

if isinstance(R, Response):
if R.status_code == 200:
return R, R.status_code
if R.status_code == 401:
raise InvalidCredentialsError(R.status)
raise OtherError(R.status, R.status_code)
Expand Down Expand Up @@ -283,9 +285,9 @@ def response_dict(self, request, D, version_hint=JSONRPC_VERSION_DEFAULT):

return response, status

def batch_response_dict(self, request, D):
def batch_response_obj(self, request, D):
try:
responses = [self.response_dict(request, d)[0] for d in D]
responses = [self.response_obj(request, d)[0] for d in D]
status = 200
if not responses:
raise InvalidRequestError('Empty array')
Expand Down Expand Up @@ -320,6 +322,40 @@ def batch_response_dict(self, request, D):

return responses, status

def make_response(self, rv):
"""Converts the return value from a view function to a real
response object that is an instance of :attr:`response_class`.
"""
status_or_headers = headers = None
if isinstance(rv, tuple):
rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))

if rv is None:
raise ValueError('View function did not return a response')

if isinstance(status_or_headers, (dict, list)):
headers, status_or_headers = status_or_headers, None

D = json.loads(extract_raw_data_request(request))
if type(D) is list:
raise InvalidRequestError('JSON-RPC batch with decorator (make_response) not is supported')
else:
response_obj = self.empty_response(version=D['jsonrpc'])
response_obj['id'] = D['id']
response_obj['result'] = rv
response_obj.pop('error', None)
rv = jsonify(response_obj)

if status_or_headers is not None:
if isinstance(status_or_headers, string_types):
rv.status = status_or_headers
else:
rv.status_code = status_or_headers
if headers:
rv.headers.extend(headers)

return rv

@csrf_exempt
def dispatch(self, request, method=''):
# in case we do something json doesn't like, we always get back valid
Expand All @@ -342,12 +378,16 @@ def dispatch(self, request, method=''):
raise ParseError(getattr(e, 'message', e.args[0] if len(e.args) > 0 else None))

if type(D) is list:
return self.batch_response_dict(request, D)
else:
response, status = self.response_dict(request, D)
if response is None and (not 'id' in D or D['id'] is None): # a notification
response = ''
return response, status
return self.batch_response_obj(request, D)

response, status = self.response_obj(request, D)

if isinstance(response, Response):
return response, status

if response is None and (not 'id' in D or D['id'] is None): # a notification
response = ''
return response, status
except Error as e:
response.pop('result', None)
response['error'] = e.json_rpc_format
Expand Down
14 changes: 13 additions & 1 deletion tests/apptest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
if os.path.exists(FLASK_JSONRPC_PROJECT_DIR) and not FLASK_JSONRPC_PROJECT_DIR in sys.path:
sys.path.append(FLASK_JSONRPC_PROJECT_DIR)

from flask_jsonrpc import JSONRPC
from flask_jsonrpc import JSONRPC, make_response

SERVER_HOSTNAME = 'localhost'
SERVER_PORT = 5001
Expand All @@ -53,6 +53,13 @@
def check_auth(username, password):
return True

def jsonrcp_headers(fn):
def wrapped(*args, **kwargs):
response = make_response(fn(*args, **kwargs))
response.headers['X-JSONRPC-Tag'] = 'JSONRPC 2.0'
return response
return wrapped

@jsonrpc.method('jsonrpc.echo')
def echo(name='Flask JSON-RPC'):
return 'Hello {0}'.format(name)
Expand Down Expand Up @@ -121,6 +128,11 @@ def subtract(a, b):
def divide(a, b):
return a / float(b)

@jsonrpc.method('jsonrpc.decorators(String) -> String')
@jsonrcp_headers
def decorators(string):
return 'Hello {0}'.format(string)


class FlaskTestServer(object):

Expand Down
2 changes: 1 addition & 1 deletion tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@

class JSONRPCHelpersTestCase(unittest.TestCase):

def test_exceptions(self):
def test_(self):
pass
2 changes: 1 addition & 1 deletion tests/test_jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@

class FlaskJSONRPCTestCase(unittest.TestCase):

def test_positional_args(self):
def test_(self):
pass
9 changes: 9 additions & 0 deletions tests/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ def test_my_str(self):
]
]


def test_decorators(self):
for version in ['1.0', '1.1', '2.0']:
proxy = ServiceProxy(self.service_url, version=version)
[self.assertEquals(resp, req['result']) for req, resp in [
(proxy.jsonrpc.decorators('Flask JSON-RPC'), 'Hello Flask JSON-RPC')
]
]

def test_method_repr(self):
proxy = ServiceProxy(self.service_url)
self.assertEqual('{"jsonrpc": "2.0", "method": "jsonrpc.echo"}', repr(proxy.jsonrpc.echo))
6 changes: 6 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,9 @@ def test_divide(self):
(self._make_payload('jsonrpc.divide', [-5, 1], version=v), -5.0),
] for v in ['1.0', '1.1', '2.0']]
[[self._assert_equals(self._call(req)['result'], resp) for req, resp in t] for t in T]

def test_decorators(self):
T = [[
(self._make_payload('jsonrpc.decorators', ['Flask JSON-RPC'], version=v), 'Hello Flask JSON-RPC'),
] for v in ['1.0', '1.1', '2.0']]
[[self._assert_equals(self._call(req)['result'], resp) for req, resp in t] for t in T]
2 changes: 1 addition & 1 deletion tests/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@

class JSONRPCSiteTestCase(unittest.TestCase):

def test_exceptions(self):
def test_(self):
pass

0 comments on commit 60d6cbf

Please sign in to comment.