Skip to content

Commit

Permalink
Added support asyncio methods fixes hugapi#241
Browse files Browse the repository at this point in the history
  • Loading branch information
rodcloutier committed Apr 7, 2016
1 parent 71b2238 commit 7245ad8
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Expand Up @@ -4,4 +4,4 @@ exclude_lines = def hug
def serve
sys.stdout.buffer.write
class Socket

pragma: no cover
2 changes: 1 addition & 1 deletion hug/directives.py
Expand Up @@ -111,7 +111,7 @@ def __getattr__(self, name):
if not function:
raise AttributeError('API Function {0} not found'.format(name))

accepts = introspect.arguments(function.interface.function)
accepts = function.interface.arguments
if 'hug_api_version' in accepts:
function = partial(function, hug_api_version=self.api_version)
if 'hug_current_api' in accepts:
Expand Down
56 changes: 49 additions & 7 deletions hug/interface.py
Expand Up @@ -22,6 +22,9 @@
from __future__ import absolute_import

import argparse


import inspect
import os
import sys
from collections import OrderedDict
Expand All @@ -40,12 +43,41 @@
from hug.types import MarshmallowSchema, Multiple, OneOf, SmartBoolean, Text, text


try:
import asyncio

if sys.version >= (3, 4, 4):
ensure_future = asyncio.ensure_future
else:
ensure_future = asyncio.async

def asyncio_call(function, *args, **kwargs):
loop = asyncio.get_event_loop()
f = ensure_future(function(*args, **kwargs), loop=loop)
loop.run_until_complete(f)
return f.result()

asyncio_iscoroutinefunction = asyncio.iscoroutinefunction

except ImportError: # pragma: no cover

def asyncio_iscoroutinefunction(function):
return False

def asyncio_call(*args, **kwargs):
raise NotImplementedError()


class Interfaces(object):
"""Defines the per-function singleton applied to hugged functions defining common data needed by all interfaces"""

def __init__(self, function):
self.spec = getattr(function, 'original', function)
self.function = function
self._function = function

self.iscoroutine = asyncio_iscoroutinefunction(self.spec)
if self.iscoroutine:
self.spec = getattr(self.spec, '__wrapped__', self.spec)

self.takes_kargs = introspect.takes_kargs(self.spec)
self.takes_kwargs = introspect.takes_kwargs(self.spec)
Expand All @@ -59,7 +91,7 @@ def __init__(self, function):
self.defaults[self.parameters[-(index + 1)]] = default

self.required = self.parameters[:-(len(self.spec.__defaults__ or ())) or None]
if introspect.is_method(self.spec):
if introspect.is_method(self.spec) or introspect.is_method(function):
self.required = self.required[1:]
self.parameters = self.parameters[1:]

Expand All @@ -80,6 +112,16 @@ def __init__(self, function):

self.input_transformations[name] = transformer

@property
def arguments(self):
return introspect.arguments(self._function)

def __call__(self, *args, **kwargs):
if not self.iscoroutine:
return self._function(*args, **kwargs)

return asyncio_call(self._function, *args, **kwargs)


class Interface(object):
"""Defines the basic hug interface object, which is responsible for wrapping a user defined function and providing
Expand Down Expand Up @@ -158,7 +200,7 @@ def validate(self, input_parameters):

for require in self.interface.required:
if not require in input_parameters:
errors[require] = "Required parameter not supplied"
errors[require] = "Required parameter '{}' not supplied".format(require)
if not errors and getattr(self, 'validate_function', False):
errors = self.validate_function(input_parameters)
return errors
Expand Down Expand Up @@ -256,7 +298,7 @@ def __call__(self, *kargs, **kwargs):
outputs = getattr(self, 'invalid_outputs', self.outputs)
return outputs(errors) if outputs else errors

result = self.interface.function(**kwargs)
result = self.interface(**kwargs)
if self.transform:
result = self.transform(result)
return self.outputs(result) if self.outputs else result
Expand Down Expand Up @@ -366,9 +408,9 @@ def __call__(self):

if hasattr(self.interface, 'karg'):
karg_values = pass_to_function.pop(self.interface.karg, ())
result = self.interface.function(*karg_values, **pass_to_function)
result = self.interface(*karg_values, **pass_to_function)
else:
result = self.interface.function(**pass_to_function)
result = self.interface(**pass_to_function)

return self.output(result)

Expand Down Expand Up @@ -492,7 +534,7 @@ def call_function(self, **parameters):
if not self.interface.takes_kwargs:
parameters = {key: value for key, value in parameters.items() if key in self.parameters}

return self.interface.function(**parameters)
return self.interface(**parameters)

def render_content(self, content, request, response, **kwargs):
if hasattr(content, 'interface') and (content.interface is True or hasattr(content.interface, 'http')):
Expand Down
29 changes: 29 additions & 0 deletions hug/introspect.py
Expand Up @@ -21,6 +21,17 @@
"""
from __future__ import absolute_import

try: # pragma: no cover
import asyncio

asyncio_iscoroutinefunction = asyncio.iscoroutinefunction

except ImportError: # pragma: no cover

def asyncio_iscoroutinefunction(function):
return False

import inspect
from types import MethodType


Expand All @@ -34,16 +45,34 @@ def arguments(function, extra_arguments=0):
if not hasattr(function, '__code__'):
return ()

if asyncio_iscoroutinefunction(function):
signature = inspect.signature(function)
if extra_arguments:
excluded_types = ()
else:
excluded_types = (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL)
return [p.name for p in signature.parameters.values() if p.kind not in excluded_types]

return function.__code__.co_varnames[:function.__code__.co_argcount + extra_arguments]


def takes_kwargs(function):
"""Returns True if the supplied function takes keyword arguments"""
if asyncio_iscoroutinefunction(function):
signature = inspect.signature(function)
return any(p for p in signature.parameters.values()
if p.kind == inspect.Parameter.VAR_KEYWORD)

return bool(function.__code__.co_flags & 0x08)


def takes_kargs(function):
"""Returns True if the supplied functions takes extra non-keyword arguments"""
if asyncio_iscoroutinefunction(function):
signature = inspect.signature(function)
return any(p for p in signature.parameters.values()
if p.kind == inspect.Parameter.VAR_POSITIONAL)

return bool(function.__code__.co_flags & 0x04)


Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
@@ -0,0 +1,10 @@

import sys

collect_ignore = []

if sys.version_info < (3, 5):
collect_ignore.append("test_async.py")

if sys.version_info < (3, 4):
collect_ignore.append("test_coroutines.py")
86 changes: 86 additions & 0 deletions tests/test_async.py
@@ -0,0 +1,86 @@
"""hug/test.py.
Tests the support for asynchronous method using asyncio async def
Copyright (C) 2016 Timothy Edmund Crosley
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""

import asyncio

import hug


loop = asyncio.get_event_loop()
api = hug.API(__name__)


def test_basic_call_async():
""" The most basic Happy-Path test for Hug APIs using async """
@hug.call()
async def hello_world():
return "Hello World!"

assert loop.run_until_complete(hello_world()) == "Hello World!"


def test_basic_call_on_method_async():

"""Test to ensure the most basic call still works if applied to a method"""
class API(object):

@hug.call()
async def hello_world(self=None):
return "Hello World!"

api_instance = API()
assert api_instance.hello_world.interface.http
assert loop.run_until_complete(api_instance.hello_world()) == "Hello World!"
assert hug.test.get(api, '/hello_world').data == "Hello World!"


def test_basic_call_on_method_through_api_instance_async():

class API(object):

def hello_world(self):
return "Hello World!"

api_instance = API()

@hug.call()
async def hello_world():
return api_instance.hello_world()

assert api_instance.hello_world() == "Hello World!"
assert hug.test.get(api, '/hello_world').data == "Hello World!"


def test_basic_call_on_method_registering_without_decorator_async():

class API(object):

def __init__(self):
hug.call()(self.hello_world_method)

async def hello_world_method(self):
return "Hello World!"

api_instance = API()

assert loop.run_until_complete(api_instance.hello_world_method()) == "Hello World!"
assert hug.test.get(api, '/hello_world_method').data == "Hello World!"
89 changes: 89 additions & 0 deletions tests/test_coroutines.py
@@ -0,0 +1,89 @@
"""hug/test.py.
Tests the support for asynchronous method using asyncio coroutines
Copyright (C) 2016 Timothy Edmund Crosley
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
import asyncio

import hug


loop = asyncio.get_event_loop()
api = hug.API(__name__)


def test_basic_call_coroutine():
""" The most basic Happy-Path test for Hug APIs using async """
@hug.call()
@asyncio.coroutine
def hello_world():
return "Hello World!"

assert loop.run_until_complete(hello_world()) == "Hello World!"


def test_basic_call_on_method_coroutine():

"""Test to ensure the most basic call still works if applied to a method"""
class API(object):

@hug.call()
@asyncio.coroutine
def hello_world(self=None):
return "Hello World!"

api_instance = API()
assert api_instance.hello_world.interface.http
assert loop.run_until_complete(api_instance.hello_world()) == "Hello World!"
assert hug.test.get(api, '/hello_world').data == "Hello World!"


def test_basic_call_on_method_through_api_instance_coroutine():

class API(object):

def hello_world(self):
return "Hello World!"

api_instance = API()

@hug.call()
@asyncio.coroutine
def hello_world():
return api_instance.hello_world()

assert api_instance.hello_world() == "Hello World!"
assert hug.test.get(api, '/hello_world').data == "Hello World!"


def test_basic_call_on_method_registering_without_decorator_coroutine():

class API(object):

def __init__(self):
hug.call()(self.hello_world_method)

@asyncio.coroutine
def hello_world_method(self):
return "Hello World!"

api_instance = API()

assert loop.run_until_complete(api_instance.hello_world_method()) == "Hello World!"
assert hug.test.get(api, '/hello_world_method').data == "Hello World!"
1 change: 1 addition & 0 deletions tests/test_decorators.py
Expand Up @@ -255,6 +255,7 @@ def wont_appear_in_kwargs(**kwargs):
assert hug.test.get(api, 'wont_appear_in_kwargs').data == 'success'



def test_method_routing():
"""Test that all hugs HTTP routers correctly route methods to the correct handler"""
@hug.get()
Expand Down
7 changes: 4 additions & 3 deletions tox.ini
@@ -1,11 +1,12 @@
[tox]
envlist=py33, py34, py35
envlist= py33, py34, py35

[testenv]
deps=-rrequirements/build.txt
whitelist_externals=flake8
commands=flake8 hug
py.test --cov hug tests
commands=
flake8 hug
py.test --cov-report term-missing --cov hug tests
coverage html

[tox:travis]
Expand Down

0 comments on commit 7245ad8

Please sign in to comment.