Implement call proxy handling
- Implement call proxy handling. Example: import tobiko class MyProtocol(tobiko.Protocol): def do_something(some='argument'): raise NotImplementedError def handle_call(method, *args, **kwargs) print(f'do something with some={some}') proxy = tobiko.call_proxy(MyProtocol, handle_call) proxy.do_something('aple') It will print: do something with some=apple - Add new depenency from decorator library Change-Id: I4a933e779739cfd953cafb275b074a308822aaac
This commit is contained in:
parent
84d8592c82
commit
c1fcde26b0
@ -1,5 +1,6 @@
|
|||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
|
|
||||||
|
decorator===4.4.2
|
||||||
docker==4.4.1
|
docker==4.4.1
|
||||||
fixtures==3.0.0
|
fixtures==3.0.0
|
||||||
Jinja2==2.11.2
|
Jinja2==2.11.2
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Tobiko framework requirements
|
# Tobiko framework requirements
|
||||||
|
|
||||||
|
decorator>=4.4.2 # BSD
|
||||||
docker>=4.4.1 # Apache-2.0
|
docker>=4.4.1 # Apache-2.0
|
||||||
fixtures>=3.0.0 # Apache-2.0/BSD
|
fixtures>=3.0.0 # Apache-2.0/BSD
|
||||||
Jinja2>=2.11.2 # BSD
|
Jinja2>=2.11.2 # BSD
|
||||||
|
@ -23,6 +23,7 @@ from tobiko.common import _logging
|
|||||||
from tobiko.common.managers import loader as loader_manager
|
from tobiko.common.managers import loader as loader_manager
|
||||||
from tobiko.common import _operation
|
from tobiko.common import _operation
|
||||||
from tobiko.common import _os
|
from tobiko.common import _os
|
||||||
|
from tobiko.common import _proxy
|
||||||
from tobiko.common import _retry
|
from tobiko.common import _retry
|
||||||
from tobiko.common import _select
|
from tobiko.common import _select
|
||||||
from tobiko.common import _skip
|
from tobiko.common import _skip
|
||||||
@ -83,6 +84,12 @@ 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
|
||||||
|
call_proxy = _proxy.call_proxy
|
||||||
|
call_proxy_class = _proxy.call_proxy_class
|
||||||
|
CallHandler = _proxy.CallHandler
|
||||||
|
CallProxy = _proxy.CallProxy
|
||||||
|
|
||||||
retry = _retry.retry
|
retry = _retry.retry
|
||||||
retry_attempt = _retry.retry_attempt
|
retry_attempt = _retry.retry_attempt
|
||||||
retry_on_exception = _retry.retry_on_exception
|
retry_on_exception = _retry.retry_on_exception
|
||||||
|
106
tobiko/common/_proxy.py
Normal file
106
tobiko/common/_proxy.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Copyright 2021 Red Hat
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
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 is_protocol_class(cls):
|
||||||
|
return cls.__dict__.get('_is_protocol', False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_public_function(obj):
|
||||||
|
return (inspect.isfunction(obj) and
|
||||||
|
getattr(obj, '__name__', '_')[0] != '_')
|
||||||
|
|
||||||
|
|
||||||
|
class CallHandler(abc.ABC):
|
||||||
|
|
||||||
|
def _handle_call(self, method: typing.Callable, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
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) \
|
||||||
|
-> type:
|
||||||
|
if not is_protocol_class(protocol_class):
|
||||||
|
raise TypeError(f"{protocol_class} is not a subclass of {Protocol}")
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def call_proxy(protocol_class: typing.Type[T], handle_call: typing.Callable) \
|
||||||
|
-> T:
|
||||||
|
proxy_class = call_proxy_class(typing.cast(type, protocol_class))
|
||||||
|
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], ...]:
|
||||||
|
subclasses = inspect.getmro(cls)
|
||||||
|
protocols = tuple(typing.cast(typing.Type[Protocol], 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)
|
||||||
|
|
||||||
|
|
||||||
|
def _call_proxy_method(func, self: CallHandler, *args, **kwargs):
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
return self._handle_call(func, *args, **kwargs)
|
65
tobiko/tests/unit/test_proxy.py
Normal file
65
tobiko/tests/unit/test_proxy.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Copyright 2021 Red Hat
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
import tobiko
|
||||||
|
from tobiko.tests import unit
|
||||||
|
|
||||||
|
|
||||||
|
class MyProto1(tobiko.Protocol):
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
def call_one(self, arg='a') -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def call_two(self, *args) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def call_three(self, **kwargs) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyTest(unit.TobikoUnitTest):
|
||||||
|
|
||||||
|
def test_call_proxy(self):
|
||||||
|
|
||||||
|
def handle_call(method: typing.Callable, *_args, **_kwargs):
|
||||||
|
self.assertTrue(inspect.isfunction(method))
|
||||||
|
|
||||||
|
handler = mock.MagicMock(side_effect=handle_call)
|
||||||
|
proxy = tobiko.call_proxy(MyProto1,
|
||||||
|
typing.cast(typing.Callable, handler))
|
||||||
|
self.assertIsInstance(proxy, MyProto1)
|
||||||
|
self.assertTrue(callable(proxy.call_one))
|
||||||
|
self.assertEqual(inspect.signature(MyProto1.call_one),
|
||||||
|
inspect.signature(type(proxy).call_one))
|
||||||
|
self.assertIsNot(MyProto1.call_one,
|
||||||
|
proxy.call_one)
|
||||||
|
proxy.call_one()
|
||||||
|
handler.assert_called_with(MyProto1.call_one, 'a')
|
||||||
|
proxy.call_one('b')
|
||||||
|
handler.assert_called_with(MyProto1.call_one, 'b')
|
||||||
|
proxy.call_one(arg='c')
|
||||||
|
handler.assert_called_with(MyProto1.call_one, 'c')
|
||||||
|
|
||||||
|
proxy.call_two(1, 2, 3)
|
||||||
|
handler.assert_called_with(MyProto1.call_two, 1, 2, 3)
|
||||||
|
|
||||||
|
proxy.call_three(a='a', b='b')
|
||||||
|
handler.assert_called_with(MyProto1.call_three, a='a', b='b')
|
Loading…
x
Reference in New Issue
Block a user