Permalink
Browse files

Added better parameter checking, async decorator, and unit tests.

  • Loading branch information...
1 parent ceffff6 commit ce6272b66b014f9213bffe8f0d2bb08264258420 @joshmarshall committed Nov 9, 2010
Showing with 441 additions and 64 deletions.
  1. +71 −11 README.md
  2. +12 −0 run_tests.py
  3. 0 setup.py
  4. +13 −0 tests/__init__.py
  5. +20 −0 tests/json.py
  6. +71 −0 tests/utils.py
  7. +94 −0 tests/xml.py
  8. +1 −1 tornadorpc/__init__.py
  9. +98 −49 tornadorpc/base.py
  10. +60 −0 tornadorpc/utils.py
  11. +1 −3 tornadorpc/xml.py
View
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
View
@@ -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()
View
0 setup.py 100755 → 100644
No changes.
View
@@ -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)
View
@@ -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)
+
View
@@ -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 == [])
View
@@ -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)
+
@@ -13,4 +13,4 @@
limitations under the License.
"""
-from base import private, start_server, config
+from base import private, async, start_server, config
Oops, something went wrong.

0 comments on commit ce6272b

Please sign in to comment.