Skip to content

Commit

Permalink
Added better parameter checking, async decorator, and unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
joshmarshall committed Nov 9, 2010
1 parent ceffff6 commit ce6272b
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 64 deletions.
82 changes: 71 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ Batch support for both specifications. The JSON-RPC handler supports
both the original 1.0 specification, as well as the new (proposed)
2.0 spec, which includes batch submission, keyword arguments, etc.

It is licensed under the Apache License, Version 2.0
Asynchronous request support has been added for methods which require
the use of asynchronous libraries (like Tornado's AsyncHTTPClient
library.)

TornadoRPC is licensed under the Apache License, Version 2.0
(http://www.apache.org/licenses/LICENSE-2.0.html).

Mailing List
------------
If you have any questions, issues, or just use the library please feel
free to send a message to the mailing list at:

http://groups.google.com/group/tornadorpc

Installation
------------
To install:
Expand Down Expand Up @@ -51,23 +62,28 @@ Tornado's website (http://www.tornadoweb.org). After installing Tornado
to use the XML-RPC handler without any other libraries.

The JSON-RPC handler requires my jsonrpclib library, which you can get
at http://github.com/joshmarshall/jsonrpclib/ It also requires a JSON
at http://github.com/joshmarshall/jsonrpclib . It also requires a JSON
library, although any distribution of Python past 2.5 should have it by
default. (Note: Some Linuxes only include a base Python install. On Ubuntu,
for instance, you may need to run `sudo apt-get install python-json` or
`sudo apt-get python-cjson` to get one of the libraries.)

Usage
-----
The library is designed to be mostly transparent in usage. You simply extend
the XML/JSON RPCHandler class from either the tornadorpc.xml or the
tornado.json library, resepectively, and pass that handler in to the Tornado
framework just like any other handler. You treat parameters and responses just
like a normal method -- no need to worry about any formatting yourself.
The library is designed to be mostly transparent in usage. You simply
extend the XML/JSON RPCHandler class from either the tornadorpc.xml or
the tornado.json library, resepectively, and pass that handler in to
the Tornado framework just like any other handler.

For any synchronous (normal) operation, you can just return the value
you want sent to the client. However, if you use any asynchronous
library (like Tornado's AsyncHTTPClient) you will want to call
self.result(RESULT) in your callback. See the Asynchronous section
below for examples.

XML-RPC Example
---------------
For example, to set up a simple XML RPC server, this is all you need:
To set up a simple XML RPC server, this is all you need:

from tornadorpc.xml import XMLRPCHandler
from tornadorpc import private, start_server
Expand Down Expand Up @@ -105,7 +121,8 @@ client with "dot-attribute" support:
class Tree(object):

def power(self, base, power, modulo=None):
return pow(base, power, modulo)
result = pow(base, power, modulo)
return result

def _private(self):
# Won't be callable
Expand All @@ -129,6 +146,39 @@ work anyway.) One of the benefits of the jsonrpclib is designed to be a
parallel implementation to the xmlrpclib, so syntax should be very similar
and it should be easy to experiment with existing apps.

An example of client usage would be:

from jsonrpclib import Server
server = Server('http://localhost:8080')
result = server.tree.power(2, 6)
# result should equal 64

Asynchronous Example
--------------------
To indicate that a request is asynchronous, simply use the "async"
decorator, and call "self.result(RESULT)" in your callback. Please note
that this will only work in the RPCHandler methods, not in any sub-tree
methods since they do not have access to the handler's result() method.

Here is an example that uses Tornado's AsyncHTTPClient with a callback:

from tornadorpc import async
from tornadorpc.xml import XMLRPCHandler
from tornado.httpclient import AsyncHTTPClient

class Handler(XMLRPCHandler):
@async
def external(self, url):
client = AsyncHTTPClient()
client.fetch(url, self._handle_response)
def _handle_response(self, response):
# The underscore will make it private automatically
# You could also use @private if you wished
# This returns the status code of the request
self.result(response.code)

Debugging
---------
There is a `config` object that is available -- it will be expanded as time
Expand Down Expand Up @@ -161,12 +211,22 @@ To change the configuration, look over the following:
from tornadorpc import config
config.verbose = False
config.short_errors = False

Tests
-----
To run some basic tests, enter the following in the same directory that
this README is in:


python run_tests.py

This will test a few basic utilites and the XMLRPC system. If you wish
to test the JSONRPC system, run the following:

python run_tests.py --json

TODO
----
* Add unit tests
* Explore non-blocking techniques
* Add logging mechanism
* Add proper HTTP codes for failures
* Optimize
12 changes: 12 additions & 0 deletions run_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import unittest
from tests.utils import *
import sys

if __name__ == "__main__":
if '--json' in sys.argv:
sys.argv.pop(sys.argv.index('--json'))
from tests.json import *
else:
from tests.xml import *

unittest.main()
Empty file modified setup.py
100755 → 100644
Empty file.
13 changes: 13 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
""" The TornadoRPC tests """

from threading import Thread
from tornadorpc import start_server
import time

def start_server(handler, port):
""" Starts a background server thread """
thread = Thread(target=start_server, args=[handler,], kwargs={'port':port})
thread.daemon = True
thread.start()
# time to start server
time.sleep(2)
20 changes: 20 additions & 0 deletions tests/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from tests.xml import TestHandler, RPCTests
from tornadorpc.json import JSONRPCHandler
import jsonrpclib
import unittest

class TestJSONHandler(TestHandler, JSONRPCHandler):
pass

class JSONRPCTests(RPCTests, unittest.TestCase):
port = 8003
handler = TestJSONHandler

def get_client(self):
client = jsonrpclib.Server('http://localhost:%d' % self.port)
return client

def test_private(self):
client = self.get_client()
self.assertRaises(jsonrpclib.ProtocolError, client.private)

71 changes: 71 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import unittest
from tornadorpc.utils import getcallargs

class TestCallArgs(unittest.TestCase):
""" Tries various argument settings """

def test_no_args(self):
def test():
pass
kwargs, xtra = getcallargs(test)
self.assertTrue(kwargs == {})
self.assertTrue(xtra == [])

def test_bad_no_args(self):
def test():
pass
self.assertRaises(TypeError, getcallargs, test, 5)

def test_positional_args(self):
def test(a, b):
pass
kwargs, xtra = getcallargs(test, 5, 6)
self.assertTrue(kwargs == {'a':5, 'b': 6})
self.assertTrue(xtra == [])

def test_extra_positional_args(self):
def test(a, b, *args):
pass
kwargs, xtra = getcallargs(test, 5, 6, 7, 8)
self.assertTrue(kwargs == {'a': 5, 'b': 6})
self.assertTrue(xtra == [7, 8])

def test_bad_positional_args(self):
def test(a, b):
pass
self.assertRaises(TypeError, getcallargs, test, 5)

def test_keyword_args(self):
def test(a, b):
pass
kwargs, xtra = getcallargs(test, a=5, b=6)
self.assertTrue(kwargs == {'a': 5, 'b':6})
self.assertTrue(xtra == [])

def test_extra_keyword_args(self):
def test(a, b, **kwargs):
pass
kwargs, xtra = getcallargs(test, a=5, b=6, c=7, d=8)
self.assertTrue(kwargs == {'a':5, 'b':6, 'c':7, 'd':8})
self.assertTrue(xtra == [])

def test_bad_keyword_args(self):
def test(a, b):
pass
self.assertRaises(TypeError, getcallargs, test, a=1, b=2, c=5)

def test_method(self):
class Foo(object):
def test(myself, a, b):
pass
foo = Foo()
kwargs, xtra = getcallargs(foo.test, 5, 6)
self.assertTrue(kwargs == {'a':5, 'b':6})
self.assertTrue(xtra == [])

def test_default(self):
def test(a, b, default=None):
pass
kwargs, xtra = getcallargs(test, a=5, b=6)
self.assertTrue(kwargs == {'a':5, 'b':6, 'default':None})
self.assertTrue(xtra == [])
94 changes: 94 additions & 0 deletions tests/xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import unittest
import xmlrpclib
import time
import threading
from tornadorpc.xml import XMLRPCHandler
from tornado.httpclient import AsyncHTTPClient
from tornadorpc import start_server, private, async

class Tree(object):

def power(self, base, power, modulo=None):
result = pow(base, power, modulo)
return result

def _private(self):
# Should not be callable
return False

class TestHandler(object):

tree = Tree()

def add(self, x, y):
return x+y

@async
def async(self, url):
async_client = AsyncHTTPClient()
async_client.fetch(url, self._handle_response)

def _handle_response(self, response):
self.result(response.code)

@private
def private(self):
# Should not be callable
return False

class TestXMLHandler(XMLRPCHandler, TestHandler):
pass

class TestServer(object):

threads = {}

@classmethod
def start(cls, handler, port):
if not cls.threads.get(port):
cls.threads[port] = threading.Thread(
target=start_server,
args=[handler,],
kwargs={'port':port}
)
cls.threads[port].daemon = True
cls.threads[port].start()
# Giving it time to start up
time.sleep(1)

class RPCTests(object):

server = None
handler = TestXMLHandler
port = 8002

def setUp(self):
server = TestServer.start(self.handler, self.port)

def get_client(self):
client = xmlrpclib.ServerProxy('http://localhost:%d' % self.port)
return client

def test_tree(self):
client = self.get_client()
result = client.tree.power(2, 6)
self.assertTrue(result == 64)

def test_add(self):
client = self.get_client()
result = client.add(5, 6)
self.assertTrue(result == 11)

def test_async(self):
url = 'http://www.google.com'
client = self.get_client()
result = client.async(url)
self.assertTrue(result == 200)

class XMLRPCTests(RPCTests, unittest.TestCase):

def test_private(self):
client = self.get_client()
result = client.private()
self.assertTrue(result['faultCode'] == -32601)

2 changes: 1 addition & 1 deletion tornadorpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
limitations under the License.
"""

from base import private, start_server, config
from base import private, async, start_server, config
Loading

0 comments on commit ce6272b

Please sign in to comment.