Repository: mosquito/aiohttp-xmlrpc

Issue:
kwargs parsing seems incorrect
Hi,

This is a great project and exactly what I am looking for. However when I did some test on server code, it responds inconsistently with python native xmlrpc library. Here is my parameter:
```
<param>
    <value>
        <i4>100</i4>
    </value>
</param>
<param>
    <value>
        <i4>200</i4>
    </value>
</param>
<param>
    <value>
        <struct>
            <member>
                <name>key1</name>
                <value>
                    <i4>1</i4>
                </value>
            </member>
            <member>
                <name>key2</name>
                <value>
                    <i4>2</i4>
                </value>
            </member>
        </struct>
    </value>
</param>
```
aiohttp-xmlrpc will parse it as one argument list with 2 elements(100 and 200) and one named argument with 2 keys(`key1` and `key2`), while python3's native xmlrpc will parse it as one argument list with 3 elements (`(100, 200, {'key1': 1, 'key2': 2})`)
Here is the code I use for native xmlrpc:
```
from xmlrpc.server import SimpleXMLRPCServer

def ping():
    return 'Pong'

def echo( *args, **kwargs):
    return (args, kwargs)

def test():
    return None

def args(*args):
    return len(args)

def kwargs(**kwargs):
    return len(kwargs)

def args_kwargs(*args, **kwargs):
    print(args)
    print(kwargs)
    return len(args) + len(kwargs)

def exception():
    raise Exception("YEEEEEE!!!")

server = SimpleXMLRPCServer(("localhost", 8080),allow_none=True)
print("Listening on port 8080...")
server.register_function(ping, "ping")
server.register_function(echo, "echo")
server.register_function(test, "test")
server.register_function(args, "args")
server.register_function(kwargs, "kwargs")
server.register_function(args_kwargs, "args_kwargs")

server.serve_forever()
```

server on aiohttp-xmlrpc one:
```
from aiohttp import web
from aiohttp_xmlrpc import handler

class XMLRPCExample(handler.XMLRPCView):
    def rpc_ping(self):
        return 'Pong'

    def rpc_echo(self, *args, **kwargs):
        return (args, kwargs)

    def rpc_test(self):
        return None

    def rpc_args(self, *args):
        return len(args)

    def rpc_kwargs(self, **kwargs):
        return len(kwargs)

    def rpc_args_kwargs(self, *args, **kwargs):
        print(args)
        print(kwargs)
        return len(args) + len(kwargs)

    def rpc_exception(self):
        raise Exception("YEEEEEE!!!")

def start_server():
    app = web.Application()
    app.router.add_route('*', '/', XMLRPCExample)
    web.run_app(app)


if __name__ == "__main__":
    start_server()
```
I called the same method of `args_kwargs` on client side. But results are 4 for aiohttp-xmlrpc and 3 for xmlrpc.
(BTW, I use https://xmlrpc.devzing.com/ tool to call rpc service which is running locally and tunnelled by ngork). 

I think `xmlrpc` is doing the right way. Maybe there are some configurations I missed or some tests I did incorrectly? 

Comments:

Patch:
diff --git a/aiohttp_xmlrpc/__init__.py b/aiohttp_xmlrpc/__init__.py
index 9aa8c62..8aa33bb 100644
--- a/aiohttp_xmlrpc/__init__.py
+++ b/aiohttp_xmlrpc/__init__.py
@@ -11,7 +11,7 @@ author_info = [
     ("Dmitry Orlov", "me@mosquito.su"),
 ]
 
-version_info = (0, 9, 0)
+version_info = (1, 0, 0)
 description = "aiohttp XML-RPC server handler and client"
 
 __version__ = ".".join(map(str, version_info))
diff --git a/aiohttp_xmlrpc/handler.py b/aiohttp_xmlrpc/handler.py
index 0be170e..f0d55db 100644
--- a/aiohttp_xmlrpc/handler.py
+++ b/aiohttp_xmlrpc/handler.py
@@ -1,4 +1,8 @@
+import asyncio
+import inspect
 import logging
+from abc import ABCMeta
+from types import MappingProxyType
 
 from aiohttp.web import HTTPBadRequest, HTTPError, Response, View
 from lxml import etree
@@ -10,9 +14,54 @@ from .common import awaitable, py2xml, schema, xml2py
 log = logging.getLogger(__name__)
 
 
-class XMLRPCView(View):
+# noinspection PyUnresolvedReferences
+class XMLRPCViewMeta(ABCMeta):
+    def __new__(cls, clsname, superclasses, attributedict):
+        mapping_key = "__method_arg_mapping__"
+        allowed_key = "__allowed_methods__"
+        attributedict[mapping_key] = dict()
+        attributedict[allowed_key] = dict()
+
+        for superclass in superclasses:
+            attributedict[mapping_key].update(
+                getattr(superclass, mapping_key, {}),
+            )
+
+        instance = super(XMLRPCViewMeta, cls).__new__(
+            cls, clsname, superclasses, attributedict,
+        )
+
+        argmapping = getattr(instance, mapping_key)
+        allowed_methods = getattr(instance, allowed_key)
+
+        for key in attributedict.keys():
+            if not key.startswith(instance.METHOD_PREFIX):
+                continue
+
+            value = getattr(instance, key)
+            method_name = key.replace(instance.METHOD_PREFIX, "", 1)
+            allowed_methods[method_name] = key
+            argmapping[method_name] = inspect.getargspec(value)
+
+        setattr(
+            instance,
+            mapping_key,
+            MappingProxyType(argmapping),
+        )
+
+        setattr(
+            instance,
+            allowed_key,
+            MappingProxyType(allowed_methods),
+        )
+
+        return instance
+
+
+class XMLRPCView(View, metaclass=XMLRPCViewMeta):
     METHOD_PREFIX = "rpc_"
     DEBUG = False
+    THREAD_POOL_EXECUTOR = None
 
     async def post(self, *args, **kwargs):
         try:
@@ -37,25 +86,25 @@ class XMLRPCView(View):
         response.body = xml_data
         return response
 
-    def _parse_body(self, body):
+    async def _parse_body(self, body):
+        loop = asyncio.get_event_loop()
         try:
-            return self._parse_xml(body)
+            return await loop.run_in_executor(
+                self.THREAD_POOL_EXECUTOR,
+                self._parse_xml,
+                body,
+            )
         except etree.DocumentInvalid:
             raise HTTPBadRequest
 
+    # noinspection PyUnresolvedReferences
     def _lookup_method(self, method_name):
-        method = getattr(self, "{0}{1}".format(self.METHOD_PREFIX, method_name), None)
-
-        if not callable(method):
-            log.warning(
-                "Can't find method %s%s in %r",
-                self.METHOD_PREFIX,
-                method_name,
-                self.__class__.__name__,
+        if method_name not in self.__allowed_methods__:
+            raise exceptions.ApplicationError(
+                "Method %r not found" % method_name,
             )
 
-            raise exceptions.ApplicationError("Method %r not found" % method_name)
-        return method
+        return awaitable(getattr(self, self.__allowed_methods__[method_name]))
 
     def _check_request(self):
         if "xml" not in self.request.headers.get("Content-Type", ""):
@@ -65,7 +114,7 @@ class XMLRPCView(View):
         self._check_request()
 
         body = await self.request.read()
-        xml_request = self._parse_body(body)
+        xml_request = await self._parse_body(body)
 
         method_name = xml_request.xpath("//methodName[1]")[0].text
         method = self._lookup_method(method_name)
@@ -87,12 +136,12 @@ class XMLRPCView(View):
             ),
         )
 
-        if args and isinstance(args[-1], dict):
+        kwargs = {}
+        argspec = self.__method_arg_mapping__[method_name]
+        if argspec.keywords and isinstance(args[-1], dict):
             kwargs = args.pop(-1)
-        else:
-            kwargs = {}
 
-        result = await awaitable(method)(*args, **kwargs)
+        result = await method(*args, **kwargs)
         return self._format_success(result)
 
     @staticmethod


Test patch:
diff --git a/tests/test_handler.py b/tests/test_handler.py
index 9bc8ef4..741adc5 100644
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -43,6 +43,12 @@ class XMLRPCMain(handler.XMLRPCView):
         loop.call_soon(f.set_result, 42)
         return f
 
+    def rpc_dict_args(self, a, b, d):
+        return (a, b, d)
+
+    def rpc_dict_kwargs(self, d, **kw):
+        return (d, kw)
+
 
 def create_app(loop):
     app = web.Application()
@@ -174,3 +180,11 @@ async def test_9_datetime(test_client):
 async def test_10_future(client):
     result = await client.future()
     assert result == 42
+
+
+async def test_11_dict_args(client):
+    result = await client.dict_args(41, 42, {"foo": "bar"})
+    assert result == [41, 42, {"foo": "bar"}]
+
+    result = await client.dict_kwargs({"foo": "bar"}, spam="egg")
+    assert result == [{"foo": "bar"}, {"spam": "egg"}]