diff --git a/tobiko/__init__.py b/tobiko/__init__.py index 7db9295df..89b38695a 100644 --- a/tobiko/__init__.py +++ b/tobiko/__init__.py @@ -85,12 +85,11 @@ get_operation = _operation.get_operation get_operation_name = _operation.get_operation_name operation_config = _operation.operation_config -Protocol = _proxy.Protocol -list_protocols = _proxy.list_protocols call_proxy = _proxy.call_proxy call_proxy_class = _proxy.call_proxy_class +list_protocols = _proxy.list_protocols +protocol = _proxy.protocol CallHandler = _proxy.CallHandler -CallProxy = _proxy.CallProxy retry = _retry.retry retry_attempt = _retry.retry_attempt diff --git a/tobiko/actor/_actor.py b/tobiko/actor/_actor.py index 5fb599141..c9ec99765 100644 --- a/tobiko/actor/_actor.py +++ b/tobiko/actor/_actor.py @@ -26,18 +26,15 @@ import tobiko from tobiko.actor import _request +T = typing.TypeVar('T') + + class ActorRef(tobiko.CallHandler): - def __init__(self, - actor_id: str, - requests: _request.ActorRequestQueue, - protocols: typing.Iterable[typing.Type[tobiko.Protocol]]): + def __init__(self, actor_id: str, requests: _request.ActorRequestQueue): + super().__init__() self.actor_id = actor_id 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): return self._requests.send_request(method=method, arguments=arguments) @@ -48,19 +45,6 @@ class ActorRef(tobiko.CallHandler): arguments.pop('self', None) 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): return getattr(obj, '__tobiko_actor_method__', False) @@ -87,14 +71,14 @@ class Actor(tobiko.SharedFixture): log: logging.LoggerAdapter loop: asyncio.AbstractEventLoop - ref: ActorRef + base_ref_class = ActorRef + _actor_methods: typing.Dict[str, typing.Callable] + _ref_class: type + _ref: ActorRef _requests: _request.ActorRequestQueue _run_actor_task: asyncio.Task - _protocols: typing.Sequence[typing.Type[tobiko.Protocol]] - _actor_methods: typing.Dict[str, typing.Callable] - @property def actor_id(self) -> str: return self.fixture_name @@ -103,13 +87,33 @@ class Actor(tobiko.SharedFixture): self.loop = self.get_loop() self.log = self.create_log() 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()) + @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: return asyncio.get_event_loop() diff --git a/tobiko/common/_proxy.py b/tobiko/common/_proxy.py index a4c81e510..0f833750d 100644 --- a/tobiko/common/_proxy.py +++ b/tobiko/common/_proxy.py @@ -13,7 +13,6 @@ # under the License. from __future__ import absolute_import -import abc import functools import inspect import typing @@ -21,23 +20,17 @@ import typing import decorator -class ProtocolMeta(abc.ABCMeta): - - def __new__(mcls, name, bases, namespace, **kwargs): - cls = super().__new__(mcls, name, bases, namespace, **kwargs) - cls._is_protocol = True - return cls - - -class Protocol(abc.ABC, metaclass=ProtocolMeta): - pass - - -T = typing.TypeVar('T') +def protocol(cls: type) -> type: + name = cls.__name__ + bases = inspect.getmro(cls)[1:] + namespace = dict(cls.__dict__, + _is_protocol=True, + __module__=cls.__module__) + return type(name, bases, namespace) 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): @@ -45,60 +38,85 @@ def is_public_function(obj): 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): - raise NotImplementedError + pass + + def use_as(self, cls: typing.Type[T]) -> T: + assert isinstance(self, cls) + return typing.cast(T, self) -class CallProxy(CallHandler): - - _handle_call: typing.Callable - - def __init__(self, handle_call: typing.Callable): - 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) \ +def call_proxy_class( + cls: type, + *bases: type, + class_name: typing.Optional[str] = None, + namespace: typing.Optional[dict] = None) \ -> type: - if not is_protocol_class(protocol_class): - raise TypeError(f"{protocol_class} is not a subclass of {Protocol}") + if not inspect.isclass(cls): + raise TypeError(f"Object {cls} is not a class") if class_name is None: - class_name = protocol_class.__name__ + 'Proxy' - namespace: typing.Dict[str, typing.Any] = {} - for name, member in protocol_class.__dict__.items(): - if is_public_function(member): - method = call_proxy_method(member) - namespace[name] = method - - return type(class_name, (handler_class, protocol_class), namespace) + class_name = cls.__name__ + 'Proxy' + protocol_classes = list_protocols(cls) + if not protocol_classes: + raise TypeError(f"Class {cls} doesn't implement any protocol") + if namespace is None: + namespace = {} + for protocol_class in reversed(protocol_classes): + 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) \ - -> T: - proxy_class = call_proxy_class(typing.cast(type, protocol_class)) +def call_proxy(cls: type, handle_call: typing.Callable) -> CallHandler: + proxy_class = call_proxy_class(cls, CallHandler) return proxy_class(handle_call) @functools.lru_cache() -def stack_classes(name: str, cls: type, *classes) -> type: - return type(name, (cls,) + classes, {}) - - -@functools.lru_cache() -def list_protocols(cls: type) -> typing.Tuple[typing.Type[Protocol], ...]: +def list_protocols(cls: type) -> typing.Tuple[type, ...]: subclasses = inspect.getmro(cls) - protocols = tuple(typing.cast(typing.Type[Protocol], cls) + protocols = tuple(cls for cls in subclasses if is_protocol_class(cls)) return tuple(protocols) 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): diff --git a/tobiko/tests/unit/actor/test_actor.py b/tobiko/tests/unit/actor/test_actor.py index d4a4b4f0a..ecd9d3e1d 100644 --- a/tobiko/tests/unit/actor/test_actor.py +++ b/tobiko/tests/unit/actor/test_actor.py @@ -22,22 +22,29 @@ from tobiko.tests import unit from tobiko import actor -class Greeter(tobiko.Protocol): +@tobiko.protocol +class Greeter: async def greet(self, whom: str, greeted: 'Greeted'): raise NotImplementedError -class Greeted(tobiko.Protocol): +@tobiko.protocol +class Greeted: def greeted(self, whom: str, greeter: Greeter): raise NotImplementedError +class GreeterRef(actor.ActorRef): + pass + + class GreeterActor(Greeter, actor.Actor): setup_called = False cleanup_called = False + base_ref_class = GreeterRef async def setup_actor(self): self.setup_called = True @@ -53,15 +60,21 @@ class GreeterActor(Greeter, actor.Actor): raise ValueError("'whom' parameter can't be empty") 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): + 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): - actor_ref = actor.create_actor(GreeterActor) - self.assertIsInstance(actor_ref, actor.ActorRef) - greeter = actor_ref.get_interface(Greeter) + greeter = actor.create_actor(GreeterActor).use_as(Greeter) + self.assertIsInstance(greeter, actor.ActorRef) + self.assertIsInstance(greeter, GreeterRef) self.assertIsInstance(greeter, Greeter) greeted = mock.MagicMock(spec=Greeted) @@ -70,9 +83,8 @@ class ActorTest(unit.TobikoUnitTest): greeter=greeter) async def test_async_request_failure(self): - actor_ref = actor.create_actor(GreeterActor) - self.assertIsInstance(actor_ref, actor.ActorRef) - greeter = actor_ref.get_interface(Greeter) + greeter = actor.create_actor(GreeterActor).use_as(Greeter) + self.assertIsInstance(greeter, actor.ActorRef) self.assertIsInstance(greeter, Greeter) greeted = mock.MagicMock(spec=Greeted) diff --git a/tobiko/tests/unit/test_proxy.py b/tobiko/tests/unit/test_proxy.py index c57b5fc5e..d4c6a87bd 100644 --- a/tobiko/tests/unit/test_proxy.py +++ b/tobiko/tests/unit/test_proxy.py @@ -15,14 +15,14 @@ from __future__ import absolute_import import inspect import typing - -import mock +from unittest import mock import tobiko from tobiko.tests import unit -class MyProto1(tobiko.Protocol): +@tobiko.protocol +class MyProto: # pylint: disable=unused-argument def call_one(self, arg='a') -> int: @@ -35,31 +35,50 @@ class MyProto1(tobiko.Protocol): return 42 +class MyProtoHandler(tobiko.CallHandler): + + protocol_class = MyProto + + 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): - self.assertTrue(inspect.isfunction(method)) + def mock_handler(self) -> typing.Callable: + return typing.cast(typing.Callable, + mock.MagicMock(side_effect=self.handle_call)) - handler = mock.MagicMock(side_effect=handle_call) - proxy = tobiko.call_proxy(MyProto1, - typing.cast(typing.Callable, handler)) - self.assertIsInstance(proxy, MyProto1) + def test_call_handler(self): + handler = self.mock_handler() + proxy = MyProtoHandler(handler).use_as(MyProto) + self.assertIsInstance(proxy, MyProto) 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)) - self.assertIsNot(MyProto1.call_one, + self.assertIsNot(MyProto.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') - handler.assert_called_with(MyProto1.call_one, 'b') + handler.assert_called_with(MyProto.call_one, 'b') 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) - 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') - handler.assert_called_with(MyProto1.call_three, a='a', b='b') + handler.assert_called_with(MyProto.call_three, a='a', b='b')