Refactor call proxy class generation
Change-Id: I9d79d4fd07fcb455b9830d76965abacf409c0ab8
This commit is contained in:
parent
6526602c36
commit
7c235c006e
@ -85,12 +85,11 @@ get_operation = _operation.get_operation
|
|||||||
get_operation_name = _operation.get_operation_name
|
get_operation_name = _operation.get_operation_name
|
||||||
operation_config = _operation.operation_config
|
operation_config = _operation.operation_config
|
||||||
|
|
||||||
Protocol = _proxy.Protocol
|
|
||||||
list_protocols = _proxy.list_protocols
|
|
||||||
call_proxy = _proxy.call_proxy
|
call_proxy = _proxy.call_proxy
|
||||||
call_proxy_class = _proxy.call_proxy_class
|
call_proxy_class = _proxy.call_proxy_class
|
||||||
|
list_protocols = _proxy.list_protocols
|
||||||
|
protocol = _proxy.protocol
|
||||||
CallHandler = _proxy.CallHandler
|
CallHandler = _proxy.CallHandler
|
||||||
CallProxy = _proxy.CallProxy
|
|
||||||
|
|
||||||
retry = _retry.retry
|
retry = _retry.retry
|
||||||
retry_attempt = _retry.retry_attempt
|
retry_attempt = _retry.retry_attempt
|
||||||
|
@ -26,18 +26,15 @@ import tobiko
|
|||||||
from tobiko.actor import _request
|
from tobiko.actor import _request
|
||||||
|
|
||||||
|
|
||||||
|
T = typing.TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
class ActorRef(tobiko.CallHandler):
|
class ActorRef(tobiko.CallHandler):
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, actor_id: str, requests: _request.ActorRequestQueue):
|
||||||
actor_id: str,
|
super().__init__()
|
||||||
requests: _request.ActorRequestQueue,
|
|
||||||
protocols: typing.Iterable[typing.Type[tobiko.Protocol]]):
|
|
||||||
self.actor_id = actor_id
|
self.actor_id = actor_id
|
||||||
self._requests = requests
|
self._requests = requests
|
||||||
self._interfaces: typing.Dict[
|
|
||||||
typing.Type[tobiko.Protocol],
|
|
||||||
typing.Any] = {protocol: None
|
|
||||||
for protocol in protocols}
|
|
||||||
|
|
||||||
def send_request(self, method: str, **arguments):
|
def send_request(self, method: str, **arguments):
|
||||||
return self._requests.send_request(method=method, arguments=arguments)
|
return self._requests.send_request(method=method, arguments=arguments)
|
||||||
@ -48,19 +45,6 @@ class ActorRef(tobiko.CallHandler):
|
|||||||
arguments.pop('self', None)
|
arguments.pop('self', None)
|
||||||
return self.send_request(method.__name__, **arguments)
|
return self.send_request(method.__name__, **arguments)
|
||||||
|
|
||||||
def get_interface(self, protocol: typing.Type[tobiko.Protocol]):
|
|
||||||
try:
|
|
||||||
interface = self._interfaces[protocol]
|
|
||||||
except KeyError as ex:
|
|
||||||
raise TypeError(
|
|
||||||
f"Protocol '{protocol}' is not supported by actor "
|
|
||||||
f"'{self.actor_id}") from ex
|
|
||||||
|
|
||||||
if interface is None:
|
|
||||||
self._interfaces[protocol] = interface = tobiko.call_proxy(
|
|
||||||
protocol, self._handle_call)
|
|
||||||
return interface
|
|
||||||
|
|
||||||
|
|
||||||
def is_actor_method(obj):
|
def is_actor_method(obj):
|
||||||
return getattr(obj, '__tobiko_actor_method__', False)
|
return getattr(obj, '__tobiko_actor_method__', False)
|
||||||
@ -87,14 +71,14 @@ class Actor(tobiko.SharedFixture):
|
|||||||
log: logging.LoggerAdapter
|
log: logging.LoggerAdapter
|
||||||
loop: asyncio.AbstractEventLoop
|
loop: asyncio.AbstractEventLoop
|
||||||
|
|
||||||
ref: ActorRef
|
base_ref_class = ActorRef
|
||||||
|
|
||||||
|
_actor_methods: typing.Dict[str, typing.Callable]
|
||||||
|
_ref_class: type
|
||||||
|
_ref: ActorRef
|
||||||
_requests: _request.ActorRequestQueue
|
_requests: _request.ActorRequestQueue
|
||||||
_run_actor_task: asyncio.Task
|
_run_actor_task: asyncio.Task
|
||||||
|
|
||||||
_protocols: typing.Sequence[typing.Type[tobiko.Protocol]]
|
|
||||||
_actor_methods: typing.Dict[str, typing.Callable]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def actor_id(self) -> str:
|
def actor_id(self) -> str:
|
||||||
return self.fixture_name
|
return self.fixture_name
|
||||||
@ -103,13 +87,33 @@ class Actor(tobiko.SharedFixture):
|
|||||||
self.loop = self.get_loop()
|
self.loop = self.get_loop()
|
||||||
self.log = self.create_log()
|
self.log = self.create_log()
|
||||||
self._requests = self.create_request_queue()
|
self._requests = self.create_request_queue()
|
||||||
self._protocols = tobiko.list_protocols(type(self))
|
|
||||||
self.ref = ActorRef(actor_id=self.actor_id,
|
|
||||||
requests=self._requests,
|
|
||||||
protocols=self._protocols)
|
|
||||||
self._run_actor_task = self.loop.create_task(
|
self._run_actor_task = self.loop.create_task(
|
||||||
self._run_actor())
|
self._run_actor())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ref_class(cls) -> type:
|
||||||
|
try:
|
||||||
|
return cls._ref_class
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
name = cls.__name__ + 'Ref'
|
||||||
|
bases = cls.base_ref_class,
|
||||||
|
namespace = {'__module__': cls.__module__,
|
||||||
|
'protocol_class': cls}
|
||||||
|
return type(name, bases, namespace)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ref(self) -> ActorRef:
|
||||||
|
try:
|
||||||
|
return self._ref
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
ref_class = self.ref_class()
|
||||||
|
self._ref = ref = ref_class(actor_id=self.actor_id,
|
||||||
|
requests=self._requests)
|
||||||
|
return ref
|
||||||
|
|
||||||
def get_loop(self) -> asyncio.AbstractEventLoop:
|
def get_loop(self) -> asyncio.AbstractEventLoop:
|
||||||
return asyncio.get_event_loop()
|
return asyncio.get_event_loop()
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import abc
|
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import typing
|
import typing
|
||||||
@ -21,23 +20,17 @@ import typing
|
|||||||
import decorator
|
import decorator
|
||||||
|
|
||||||
|
|
||||||
class ProtocolMeta(abc.ABCMeta):
|
def protocol(cls: type) -> type:
|
||||||
|
name = cls.__name__
|
||||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
bases = inspect.getmro(cls)[1:]
|
||||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
namespace = dict(cls.__dict__,
|
||||||
cls._is_protocol = True
|
_is_protocol=True,
|
||||||
return cls
|
__module__=cls.__module__)
|
||||||
|
return type(name, bases, namespace)
|
||||||
|
|
||||||
class Protocol(abc.ABC, metaclass=ProtocolMeta):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
T = typing.TypeVar('T')
|
|
||||||
|
|
||||||
|
|
||||||
def is_protocol_class(cls):
|
def is_protocol_class(cls):
|
||||||
return cls.__dict__.get('_is_protocol', False)
|
return inspect.isclass(cls) and cls.__dict__.get('_is_protocol', False)
|
||||||
|
|
||||||
|
|
||||||
def is_public_function(obj):
|
def is_public_function(obj):
|
||||||
@ -45,60 +38,85 @@ def is_public_function(obj):
|
|||||||
getattr(obj, '__name__', '_')[0] != '_')
|
getattr(obj, '__name__', '_')[0] != '_')
|
||||||
|
|
||||||
|
|
||||||
class CallHandler(abc.ABC):
|
T = typing.TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class CallHandlerMeta(type):
|
||||||
|
|
||||||
|
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||||
|
protocol_class = namespace.get('protocol_class')
|
||||||
|
if protocol_class is not None:
|
||||||
|
proxy_class = call_proxy_class(protocol_class)
|
||||||
|
bases += proxy_class,
|
||||||
|
return super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CallHandler(metaclass=CallHandlerMeta):
|
||||||
|
|
||||||
|
protocol_class: type
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
handle_call: typing.Optional[typing.Callable] = None):
|
||||||
|
if handle_call is not None:
|
||||||
|
assert callable(handle_call)
|
||||||
|
setattr(self, '_handle_call', handle_call)
|
||||||
|
|
||||||
def _handle_call(self, method: typing.Callable, *args, **kwargs):
|
def _handle_call(self, method: typing.Callable, *args, **kwargs):
|
||||||
raise NotImplementedError
|
pass
|
||||||
|
|
||||||
|
def use_as(self, cls: typing.Type[T]) -> T:
|
||||||
|
assert isinstance(self, cls)
|
||||||
|
return typing.cast(T, self)
|
||||||
|
|
||||||
|
|
||||||
class CallProxy(CallHandler):
|
def call_proxy_class(
|
||||||
|
cls: type,
|
||||||
_handle_call: typing.Callable
|
*bases: type,
|
||||||
|
class_name: typing.Optional[str] = None,
|
||||||
def __init__(self, handle_call: typing.Callable):
|
namespace: typing.Optional[dict] = None) \
|
||||||
setattr(self, '_handle_call', handle_call)
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def call_proxy_class(protocol_class: type,
|
|
||||||
class_name: typing.Optional[str] = None,
|
|
||||||
handler_class: typing.Type[CallHandler] = CallProxy) \
|
|
||||||
-> type:
|
-> type:
|
||||||
if not is_protocol_class(protocol_class):
|
if not inspect.isclass(cls):
|
||||||
raise TypeError(f"{protocol_class} is not a subclass of {Protocol}")
|
raise TypeError(f"Object {cls} is not a class")
|
||||||
if class_name is None:
|
if class_name is None:
|
||||||
class_name = protocol_class.__name__ + 'Proxy'
|
class_name = cls.__name__ + 'Proxy'
|
||||||
namespace: typing.Dict[str, typing.Any] = {}
|
protocol_classes = list_protocols(cls)
|
||||||
for name, member in protocol_class.__dict__.items():
|
if not protocol_classes:
|
||||||
if is_public_function(member):
|
raise TypeError(f"Class {cls} doesn't implement any protocol")
|
||||||
method = call_proxy_method(member)
|
if namespace is None:
|
||||||
namespace[name] = method
|
namespace = {}
|
||||||
|
for protocol_class in reversed(protocol_classes):
|
||||||
return type(class_name, (handler_class, protocol_class), namespace)
|
for name, member in protocol_class.__dict__.items():
|
||||||
|
if is_public_function(member):
|
||||||
|
method = call_proxy_method(member)
|
||||||
|
namespace[name] = method
|
||||||
|
# Skip empty protocols
|
||||||
|
if not namespace:
|
||||||
|
raise TypeError(f"Class {cls} has any protocol specification")
|
||||||
|
namespace['__module__'] = cls.__module__
|
||||||
|
proxy_class = type(class_name, bases + protocol_classes, namespace)
|
||||||
|
assert not is_protocol_class(proxy_class)
|
||||||
|
assert not is_protocol_class(proxy_class)
|
||||||
|
return proxy_class
|
||||||
|
|
||||||
|
|
||||||
def call_proxy(protocol_class: typing.Type[T], handle_call: typing.Callable) \
|
def call_proxy(cls: type, handle_call: typing.Callable) -> CallHandler:
|
||||||
-> T:
|
proxy_class = call_proxy_class(cls, CallHandler)
|
||||||
proxy_class = call_proxy_class(typing.cast(type, protocol_class))
|
|
||||||
return proxy_class(handle_call)
|
return proxy_class(handle_call)
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache()
|
||||||
def stack_classes(name: str, cls: type, *classes) -> type:
|
def list_protocols(cls: type) -> typing.Tuple[type, ...]:
|
||||||
return type(name, (cls,) + classes, {})
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def list_protocols(cls: type) -> typing.Tuple[typing.Type[Protocol], ...]:
|
|
||||||
subclasses = inspect.getmro(cls)
|
subclasses = inspect.getmro(cls)
|
||||||
protocols = tuple(typing.cast(typing.Type[Protocol], cls)
|
protocols = tuple(cls
|
||||||
for cls in subclasses
|
for cls in subclasses
|
||||||
if is_protocol_class(cls))
|
if is_protocol_class(cls))
|
||||||
return tuple(protocols)
|
return tuple(protocols)
|
||||||
|
|
||||||
|
|
||||||
def call_proxy_method(func: typing.Callable) -> typing.Callable:
|
def call_proxy_method(func: typing.Callable) -> typing.Callable:
|
||||||
return decorator.decorate(func, _call_proxy_method)
|
method = decorator.decorate(func, _call_proxy_method)
|
||||||
|
assert method is not func
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
def _call_proxy_method(func, self: CallHandler, *args, **kwargs):
|
def _call_proxy_method(func, self: CallHandler, *args, **kwargs):
|
||||||
|
@ -22,22 +22,29 @@ from tobiko.tests import unit
|
|||||||
from tobiko import actor
|
from tobiko import actor
|
||||||
|
|
||||||
|
|
||||||
class Greeter(tobiko.Protocol):
|
@tobiko.protocol
|
||||||
|
class Greeter:
|
||||||
|
|
||||||
async def greet(self, whom: str, greeted: 'Greeted'):
|
async def greet(self, whom: str, greeted: 'Greeted'):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class Greeted(tobiko.Protocol):
|
@tobiko.protocol
|
||||||
|
class Greeted:
|
||||||
|
|
||||||
def greeted(self, whom: str, greeter: Greeter):
|
def greeted(self, whom: str, greeter: Greeter):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class GreeterRef(actor.ActorRef):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GreeterActor(Greeter, actor.Actor):
|
class GreeterActor(Greeter, actor.Actor):
|
||||||
|
|
||||||
setup_called = False
|
setup_called = False
|
||||||
cleanup_called = False
|
cleanup_called = False
|
||||||
|
base_ref_class = GreeterRef
|
||||||
|
|
||||||
async def setup_actor(self):
|
async def setup_actor(self):
|
||||||
self.setup_called = True
|
self.setup_called = True
|
||||||
@ -53,15 +60,21 @@ class GreeterActor(Greeter, actor.Actor):
|
|||||||
raise ValueError("'whom' parameter can't be empty")
|
raise ValueError("'whom' parameter can't be empty")
|
||||||
|
|
||||||
self.log.info(f"Hello {whom}!")
|
self.log.info(f"Hello {whom}!")
|
||||||
greeted.greeted(whom=whom, greeter=self.ref.get_interface(Greeter))
|
greeted.greeted(whom=whom, greeter=self.ref.use_as(Greeter))
|
||||||
|
|
||||||
|
|
||||||
class ActorTest(unit.TobikoUnitTest):
|
class ActorTest(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
def test_greeter_ref_class(self):
|
||||||
|
ref_class = GreeterActor.ref_class()
|
||||||
|
self.assertTrue(issubclass(ref_class, actor.ActorRef))
|
||||||
|
self.assertTrue(issubclass(ref_class, GreeterRef))
|
||||||
|
self.assertTrue(issubclass(ref_class, Greeter))
|
||||||
|
|
||||||
async def test_async_request(self):
|
async def test_async_request(self):
|
||||||
actor_ref = actor.create_actor(GreeterActor)
|
greeter = actor.create_actor(GreeterActor).use_as(Greeter)
|
||||||
self.assertIsInstance(actor_ref, actor.ActorRef)
|
self.assertIsInstance(greeter, actor.ActorRef)
|
||||||
greeter = actor_ref.get_interface(Greeter)
|
self.assertIsInstance(greeter, GreeterRef)
|
||||||
self.assertIsInstance(greeter, Greeter)
|
self.assertIsInstance(greeter, Greeter)
|
||||||
greeted = mock.MagicMock(spec=Greeted)
|
greeted = mock.MagicMock(spec=Greeted)
|
||||||
|
|
||||||
@ -70,9 +83,8 @@ class ActorTest(unit.TobikoUnitTest):
|
|||||||
greeter=greeter)
|
greeter=greeter)
|
||||||
|
|
||||||
async def test_async_request_failure(self):
|
async def test_async_request_failure(self):
|
||||||
actor_ref = actor.create_actor(GreeterActor)
|
greeter = actor.create_actor(GreeterActor).use_as(Greeter)
|
||||||
self.assertIsInstance(actor_ref, actor.ActorRef)
|
self.assertIsInstance(greeter, actor.ActorRef)
|
||||||
greeter = actor_ref.get_interface(Greeter)
|
|
||||||
self.assertIsInstance(greeter, Greeter)
|
self.assertIsInstance(greeter, Greeter)
|
||||||
greeted = mock.MagicMock(spec=Greeted)
|
greeted = mock.MagicMock(spec=Greeted)
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ from __future__ import absolute_import
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import typing
|
import typing
|
||||||
|
from unittest import mock
|
||||||
import mock
|
|
||||||
|
|
||||||
import tobiko
|
import tobiko
|
||||||
from tobiko.tests import unit
|
from tobiko.tests import unit
|
||||||
|
|
||||||
|
|
||||||
class MyProto1(tobiko.Protocol):
|
@tobiko.protocol
|
||||||
|
class MyProto:
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
def call_one(self, arg='a') -> int:
|
def call_one(self, arg='a') -> int:
|
||||||
@ -35,31 +35,50 @@ class MyProto1(tobiko.Protocol):
|
|||||||
return 42
|
return 42
|
||||||
|
|
||||||
|
|
||||||
|
class MyProtoHandler(tobiko.CallHandler):
|
||||||
|
|
||||||
|
protocol_class = MyProto
|
||||||
|
|
||||||
|
|
||||||
class ProxyTest(unit.TobikoUnitTest):
|
class ProxyTest(unit.TobikoUnitTest):
|
||||||
|
|
||||||
def test_call_proxy(self):
|
def handle_call(self, method: typing.Callable, *_args, **_kwargs):
|
||||||
|
self.assertTrue(inspect.isfunction(method))
|
||||||
|
|
||||||
def handle_call(method: typing.Callable, *_args, **_kwargs):
|
def mock_handler(self) -> typing.Callable:
|
||||||
self.assertTrue(inspect.isfunction(method))
|
return typing.cast(typing.Callable,
|
||||||
|
mock.MagicMock(side_effect=self.handle_call))
|
||||||
|
|
||||||
handler = mock.MagicMock(side_effect=handle_call)
|
def test_call_handler(self):
|
||||||
proxy = tobiko.call_proxy(MyProto1,
|
handler = self.mock_handler()
|
||||||
typing.cast(typing.Callable, handler))
|
proxy = MyProtoHandler(handler).use_as(MyProto)
|
||||||
self.assertIsInstance(proxy, MyProto1)
|
self.assertIsInstance(proxy, MyProto)
|
||||||
self.assertTrue(callable(proxy.call_one))
|
self.assertTrue(callable(proxy.call_one))
|
||||||
self.assertEqual(inspect.signature(MyProto1.call_one),
|
self.assertEqual(inspect.signature(MyProto.call_one),
|
||||||
inspect.signature(type(proxy).call_one))
|
inspect.signature(type(proxy).call_one))
|
||||||
self.assertIsNot(MyProto1.call_one,
|
self.assertIsNot(MyProto.call_one,
|
||||||
proxy.call_one)
|
proxy.call_one)
|
||||||
proxy.call_one()
|
proxy.call_one()
|
||||||
handler.assert_called_with(MyProto1.call_one, 'a')
|
handler.assert_called_with(MyProto.call_one, 'a')
|
||||||
|
|
||||||
|
def test_call_proxy(self):
|
||||||
|
handler = self.mock_handler()
|
||||||
|
proxy = tobiko.call_proxy(MyProto, handler).use_as(MyProto)
|
||||||
|
self.assertIsInstance(proxy, MyProto)
|
||||||
|
self.assertTrue(callable(proxy.call_one))
|
||||||
|
self.assertEqual(inspect.signature(MyProto.call_one),
|
||||||
|
inspect.signature(type(proxy).call_one))
|
||||||
|
self.assertIsNot(MyProto.call_one,
|
||||||
|
proxy.call_one)
|
||||||
|
proxy.call_one()
|
||||||
|
handler.assert_called_with(MyProto.call_one, 'a')
|
||||||
proxy.call_one('b')
|
proxy.call_one('b')
|
||||||
handler.assert_called_with(MyProto1.call_one, 'b')
|
handler.assert_called_with(MyProto.call_one, 'b')
|
||||||
proxy.call_one(arg='c')
|
proxy.call_one(arg='c')
|
||||||
handler.assert_called_with(MyProto1.call_one, 'c')
|
handler.assert_called_with(MyProto.call_one, 'c')
|
||||||
|
|
||||||
proxy.call_two(1, 2, 3)
|
proxy.call_two(1, 2, 3)
|
||||||
handler.assert_called_with(MyProto1.call_two, 1, 2, 3)
|
handler.assert_called_with(MyProto.call_two, 1, 2, 3)
|
||||||
|
|
||||||
proxy.call_three(a='a', b='b')
|
proxy.call_three(a='a', b='b')
|
||||||
handler.assert_called_with(MyProto1.call_three, a='a', b='b')
|
handler.assert_called_with(MyProto.call_three, a='a', b='b')
|
||||||
|
Loading…
Reference in New Issue
Block a user