Skip to content

Commit

Permalink
Add helpers.resolver for resolve objects by dotted notations names
Browse files Browse the repository at this point in the history
  • Loading branch information
zzzsochi committed May 23, 2015
1 parent 6f0f4ac commit 685ab78
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 10 deletions.
33 changes: 26 additions & 7 deletions aiotraversal/app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import types
from types import MethodType, ModuleType
import logging
import warnings

from aiohttp.web import Application as BaseApplication
from zope.dottedname.resolve import resolve

from .exceptions import ViewNotResolved
from .helpers import resolver
from .router import Router
from .resources import Root

Expand Down Expand Up @@ -49,7 +50,7 @@ def include(self, name_or_func, module=None):
else:
func = resolve(name_or_func, module=module)

if isinstance(func, types.ModuleType):
if isinstance(func, ModuleType):
if not hasattr(func, 'includeme'):
raise ImportError("{}.includeme".format(func.__name__))

Expand All @@ -61,20 +62,27 @@ def include(self, name_or_func, module=None):

func(_ApplicationIncludeWrapper(self, func.__module__))

@resolver('func')
def add_method(self, name, func):
""" Add method to application
Usage from configuration process.
"""
assert isinstance(name, str), 'name is not a string!'

if hasattr(self, name):
if hasattr(self, '_app'):
app = self._app
else:
app = self

if hasattr(app, name):
warnings.warn("Method {} is already exist, replacing it"
"".format(name))

meth = types.MethodType(func, self)
setattr(self, name, meth)
meth = MethodType(func, app)
setattr(app, name, meth)

@resolver('root_class')
def set_root_class(self, root_class):
""" Set root resource class
Expand All @@ -87,10 +95,14 @@ def get_root(self, request):
"""
return self._root_class(request)

@resolver('resource')
def resolve_view(self, resource, tail=()):
""" Resolve view for resource and tail
"""
resource_class = resource.__class__
if isinstance(resource, type):
resource_class = resource
else:
resource_class = resource.__class__

for rc in resource_class.__mro__[:-1]:
if rc in self['resources']:
Expand All @@ -109,6 +121,7 @@ def resolve_view(self, resource, tail=()):

return view(resource)

@resolver('resource', 'view')
def bind_view(self, resource, view, tail=()):
""" Bind view for resource
"""
Expand All @@ -118,6 +131,7 @@ def bind_view(self, resource, view, tail=()):
setup = self._get_resource_setup(resource)
setup['views'][tail] = view

@resolver('resource')
def _get_resource_setup(self, resource):
return self['resources'].setdefault(resource, {'views': {}})

Expand All @@ -132,7 +146,12 @@ def __init__(self, app, module):
self._module = module

def __getattr__(self, name):
return getattr(self._app, name)
attr = getattr(self._app, name)

if isinstance(attr, MethodType):
return MethodType(attr.__func__, self)
else:
return attr

def __getitem__(self, name):
return self._app[name]
Expand Down
48 changes: 48 additions & 0 deletions aiotraversal/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import inspect

from zope.dottedname.resolve import resolve


def resolver(*for_resolve):
""" Resolve dotted names
Usage:
@resolver('klass1', 'klass2')
def method(app, klass1, param, klass2):
"klass1 and klass2 may be object or dotted notation path"
assert not isinstance(klass1, str)
assert not isinstance(klass2, str)
assert isinstance(param, str)
"""
def decorator(func):
spec = inspect.getargspec(func).args[1:]
if set(for_resolve) - set(spec):
raise ValueError('bad arguments')

def wrapper(app, *args, **kwargs):
module = getattr(app, '_module', None)

args = list(args)

for item in for_resolve:
n = spec.index(item)
if n >= len(args):
continue

if n is not None and isinstance(args[n], str):
args[n] = resolve(args[n], module)

for kw, value in kwargs.copy().items():
if kw in for_resolve and isinstance(value, str):
kwargs[kw] = resolve(value, module)

return func(app, *args, **kwargs)

wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__annotations__ = func.__annotations__

return wrapper

return decorator
2 changes: 2 additions & 0 deletions aiotraversal/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .abc import AbstractResource
from .traversal import Traverser
from .helpers import resolver

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,6 +82,7 @@ def __init__(self, request):
self.setup = self.app['resources'].get(self.__class__)


@resolver('parent', 'child')
def add_child(app, parent, name, child):
""" Add child resource for dispatch-resources
"""
Expand Down
2 changes: 2 additions & 0 deletions aiotraversal/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from aiohttp.web import Response, HTTPNotFound

from .helpers import resolver
from .resources import Resource
from .views import View

Expand Down Expand Up @@ -59,6 +60,7 @@ def __call__(self):
)


@resolver('parent', 'resource_class')
def add_static(app, parent, name, path, resource_class=StaticResource):
""" Add resource for serve static
"""
Expand Down
4 changes: 2 additions & 2 deletions tests/for_include.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
def includeme(app):
app['test_include_info'] = ('includeme', app)
assert app is not app._app
assert app.start == app._app.start
assert app.start.__func__ == app._app.start.__func__


def func(app):
app['test_include_info'] = ('func', app)
assert app is not app._app
assert app.start == app._app.start
assert app.start.__func__ == app._app.start.__func__


not_callable = 'not callable data'
60 changes: 60 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from unittest.mock import Mock

import pytest

from aiotraversal.helpers import resolver


def test_resolver():
class Obj:
@resolver('obj1', 'obj2')
def meth(self, obj1, ar, obj2=2, kw=3):
return (self, obj1, ar, obj2, kw)

obj = Obj()
res = obj.meth('os.path.isdir', 'str', obj2='sys.exit')

from os.path import isdir
from sys import exit

assert res[0] is obj
assert res[1] is isdir
assert res[2] == 'str'
assert res[3] is exit
assert res[4] == 3

res = obj.meth(isdir, 'str', obj2=exit)

assert res[0] is obj
assert res[1] is isdir
assert res[2] == 'str'
assert res[3] is exit
assert res[4] == 3


def test_resolver__relative():
@resolver('obj')
def func(app, obj):
return (app, obj)

res = func(Mock(name='app', _module='tests'), '.for_include.func')

from .for_include import func as func_imported
assert res[0]._module == 'tests'
assert res[1] is func_imported


def test_resolver__import_error():
@resolver('obj')
def func(app, obj):
return (app, obj)

with pytest.raises(ImportError):
func('app', 'not_found')


def test_resolver__declare_error():
with pytest.raises(ValueError):
@resolver('not_exist')
def func(app, obj):
return (app, obj)
2 changes: 1 addition & 1 deletion tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def __init_coro__(self):
self.calls_init_coro += 1

app.include('aiotraversal.resources')
app.add_child(Root, 'simple', Res)
app.add_child('aiotraversal.resources.Root', 'simple', Res)
app.add_child(Root, 'coro', CoroRes)

request = MagicMock(name='request')
Expand Down

0 comments on commit 685ab78

Please sign in to comment.