Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support asyncio methods #289

Merged
merged 1 commit into from Apr 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
25 changes: 25 additions & 0 deletions README.md
Expand Up @@ -307,6 +307,31 @@ def not_found_handler():
```


Asyncio support
===============

When using the `get` and `cli` method decorator on coroutines, hug will schedule
the execution of the coroutine.

Using asyncio coroutine decorator
```py
@hug.get()
@asyncio.coroutine
def hello_world():
return "Hello"
```

Using Python 3.5 async keyword.
```py
@hug.get()
async def hello_world():
return "Hello"
```

NOTE: Hug is running on top Falcon which is not an asynchronous server. Even if using
asyncio, requests will still be processed synchronously.


Why hug?
===================

Expand Down
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
54 changes: 47 additions & 7 deletions hug/interface.py
Expand Up @@ -22,6 +22,7 @@
from __future__ import absolute_import

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


try:
import asyncio

if sys.version_info >= (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 +89,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 +110,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 +198,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 +296,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 +406,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 +532,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!"