Simplify actor sub-class declaration
- Make actor ref generic classes referencing to actors - Create actor ref interface directly with actor methods - Create a BaseActor abstract class for remote actors - Add ActorManager class - Implement the logic that allow to use a specialized Fixture manager for Actors Change-Id: Idba2b903c1259dc924dd4d83fd7773c79bbb1e12
This commit is contained in:
parent
5e7ab1f6cf
commit
5177c1239e
@ -22,8 +22,8 @@ from tobiko.common import _deprecation
|
||||
from tobiko.common import _detail
|
||||
from tobiko.common import _exception
|
||||
from tobiko.common import _fixture
|
||||
from tobiko.common import _loader
|
||||
from tobiko.common import _logging
|
||||
from tobiko.common.managers import loader as loader_manager
|
||||
from tobiko.common import _operation
|
||||
from tobiko.common import _os
|
||||
from tobiko.common import _retry
|
||||
@ -84,8 +84,8 @@ RequiredFixture = _fixture.RequiredFixture
|
||||
|
||||
CaptureLogFixture = _logging.CaptureLogFixture
|
||||
|
||||
load_object = loader_manager.load_object
|
||||
load_module = loader_manager.load_module
|
||||
load_object = _loader.load_object
|
||||
load_module = _loader.load_module
|
||||
|
||||
makedirs = _os.makedirs
|
||||
open_output_file = _os.open_output_file
|
||||
|
@ -16,17 +16,19 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from tobiko.actors import _actor
|
||||
from tobiko.actors import _manager
|
||||
from tobiko.actors import _proxy
|
||||
|
||||
actor_method = _actor.actor_method
|
||||
Actor = _actor.Actor
|
||||
ActorRef = _actor.ActorRef
|
||||
|
||||
cleanup_actor = _manager.cleanup_actor
|
||||
setup_actor = _manager.setup_actor
|
||||
start_actor = _manager.start_actor
|
||||
stop_actor = _manager.stop_actor
|
||||
|
||||
call_proxy = _proxy.create_call_proxy
|
||||
call_proxy_class = _proxy.create_call_proxy_class
|
||||
CallProxy = _proxy.CallProxy
|
||||
CallProxyBase = _proxy.CallProxyBase
|
||||
|
||||
actor_method = _actor.actor_method
|
||||
cleanup_actor = _actor.cleanup_actor
|
||||
setup_actor = _actor.setup_actor
|
||||
start_actor = _actor.start_actor
|
||||
stop_actor = _actor.stop_actor
|
||||
Actor = _actor.Actor
|
||||
ActorRef = _actor.ActorRef
|
||||
|
@ -29,31 +29,52 @@ from tobiko.actors import _proxy
|
||||
from tobiko.actors import _request
|
||||
|
||||
|
||||
P = typing.TypeVar('P', bound=abc.ABC)
|
||||
A = typing.TypeVar('A', bound='ActorBase')
|
||||
|
||||
|
||||
class ActorRef(_proxy.CallProxyBase, typing.Generic[P], abc.ABC):
|
||||
class ActorRef(_proxy.CallProxyBase, _proxy.Generic[A]):
|
||||
|
||||
def __init__(self, actor_id: str,
|
||||
requests: _request.ActorRequestQueue):
|
||||
_actor_class: typing.Type[A]
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
# pylint: disable=too-many-function-args
|
||||
if isinstance(item, type):
|
||||
if issubclass(item, ActorBase):
|
||||
actor_class: typing.Type[ActorBase] = item
|
||||
base_name = cls.__name__.split('[', 1)[0]
|
||||
ref_class = _proxy.create_call_proxy_class(
|
||||
protocols=(actor_class,),
|
||||
class_name=f"{base_name}[{actor_class.__name__}]",
|
||||
bases=(cls,),
|
||||
namespace=dict(_actor_class=item),
|
||||
predicate=is_actor_method)
|
||||
ref_class.__module__ = cls.__module__
|
||||
return ref_class
|
||||
else:
|
||||
raise TypeError(f'{item} is not subclass of {ActorBase}')
|
||||
|
||||
ref_class = cls
|
||||
getitem = getattr(super(), '__class_getitem__')
|
||||
if callable(getitem):
|
||||
if inspect.ismethod(getitem):
|
||||
ref_class = getitem(item)
|
||||
else:
|
||||
ref_class = getitem(cls, item)
|
||||
return ref_class
|
||||
|
||||
def __init__(self, actor: A):
|
||||
super().__init__()
|
||||
self.actor_id = actor_id
|
||||
self._requests = requests
|
||||
|
||||
def send_request(self, method: str, **arguments):
|
||||
return self._requests.send_request(actor_id=self.actor_id,
|
||||
method=method,
|
||||
arguments=arguments)
|
||||
self._actor = tobiko.check_valid_type(actor, self._actor_class)
|
||||
|
||||
def _handle_call(self, method: typing.Callable, *args, **kwargs) \
|
||||
-> asyncio.Future:
|
||||
arguments = inspect.signature(method).bind(
|
||||
None, *args, **kwargs).arguments
|
||||
arguments.pop('self', None)
|
||||
return self.send_request(method.__name__, **arguments)
|
||||
return self._actor.send_request(method.__name__, **arguments)
|
||||
|
||||
def ping_actor(self, data: typing.Any = None) -> typing.Any:
|
||||
return self.send_request(method='ping_actor', data=data)
|
||||
def __repr__(self):
|
||||
return f'{type(self).__name__}({self._actor})'
|
||||
|
||||
|
||||
def is_actor_method(obj):
|
||||
@ -67,6 +88,9 @@ def actor_method(obj):
|
||||
if not inspect.iscoroutinefunction(obj):
|
||||
raise TypeError(f"Actor method {obj} is not async")
|
||||
|
||||
if not _proxy.is_public_function(obj):
|
||||
raise TypeError(f"Actor method name {obj} can't start with '_'")
|
||||
|
||||
name = getattr(obj, '__name__', None)
|
||||
if name is None or hasattr(ActorRef, name) or hasattr(Actor, name):
|
||||
raise TypeError(f"Invalid method name: '{name}'")
|
||||
@ -79,77 +103,111 @@ class _DummyActorProtocol(abc.ABC):
|
||||
pass
|
||||
|
||||
|
||||
class Actor(tobiko.SharedFixture, typing.Generic[P],
|
||||
metaclass=_proxy.GenericMeta):
|
||||
class ActorBase(tobiko.SharedFixture):
|
||||
max_queue_size: int = 0
|
||||
|
||||
_actor_protocol = _DummyActorProtocol
|
||||
_cancel_actor = False
|
||||
_run_actor_task: asyncio.Task
|
||||
|
||||
# Class methods ----------------------------------------------------------
|
||||
|
||||
def __init_subclass__(cls,
|
||||
*args,
|
||||
**kwargs):
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
cls._actor_methods = dict(inspect.getmembers(cls, is_actor_method))
|
||||
cls._actor_ref_class: typing.Type[ActorRef[P]] = (
|
||||
ActorRef[cls._actor_protocol])
|
||||
|
||||
def __class_getitem__(cls, item: typing.Type[P]):
|
||||
if isinstance(item, type):
|
||||
return type(cls.__name__, (cls, item), dict(_actor_protocol=item))
|
||||
else:
|
||||
return cls
|
||||
|
||||
# Public instance methods ------------------------------------------------
|
||||
cls._actor_ref_class = ActorRef[cls]
|
||||
|
||||
def __init__(self,
|
||||
actor_id: str = None,
|
||||
event_loop: asyncio.AbstractEventLoop = None,
|
||||
loop: asyncio.AbstractEventLoop = None,
|
||||
log: logging.LoggerAdapter = None,
|
||||
requests: _request.ActorRequestQueue = None,
|
||||
ref: ActorRef[P] = None):
|
||||
requests: _request.ActorRequestQueue = None):
|
||||
# pylint: disable=redefined-outer-name
|
||||
super().__init__()
|
||||
if actor_id is None:
|
||||
actor_id = self._init_actor_id()
|
||||
self.actor_id = actor_id
|
||||
|
||||
if event_loop is None:
|
||||
event_loop = self._init_event_loop()
|
||||
self.event_loop = event_loop
|
||||
|
||||
if log is None:
|
||||
log = self._init_log()
|
||||
self.log = log
|
||||
|
||||
if loop is None:
|
||||
loop = self._init_loop()
|
||||
self.loop = loop
|
||||
if requests is None:
|
||||
requests = self._init_actor_request_queue()
|
||||
requests = self._init_requests()
|
||||
self.requests = requests
|
||||
self.setup_actor_future = self.loop.create_future()
|
||||
self.cleanup_actor_future = self.loop.create_future()
|
||||
self.cleanup_actor_future.set_result(None)
|
||||
|
||||
if ref is None:
|
||||
ref = self._init_actor_ref()
|
||||
self.ref = typing.cast(P, ref)
|
||||
self.setup_future = event_loop.create_future()
|
||||
self.cleanup_future = event_loop.create_future()
|
||||
def setup_fixture(self):
|
||||
if self.actor_id is None:
|
||||
self.actor_id = self._setup_actor_id()
|
||||
if self.setup_actor_future.done():
|
||||
self.setup_actor_future.cancel()
|
||||
self.setup_actor_future = self.loop.create_future()
|
||||
if self.cleanup_actor_future.done():
|
||||
self.cleanup_actor_future.cancel()
|
||||
self.cleanup_actor_future = self.loop.create_future()
|
||||
|
||||
@property
|
||||
def ref(self) -> 'ActorRef':
|
||||
return self._actor_ref_class(actor=self)
|
||||
|
||||
@property
|
||||
def actor_name(self) -> str:
|
||||
return tobiko.get_fixture_name(self)
|
||||
|
||||
def send_request(self, method: str, **arguments) -> asyncio.Future:
|
||||
if self.actor_id is None:
|
||||
raise ValueError("Actor not set up yet")
|
||||
return self.requests.send_request(actor_id=self.actor_id,
|
||||
method=method,
|
||||
arguments=arguments)
|
||||
|
||||
# Private instance methods -----------------------------------------------
|
||||
|
||||
def _init_log(self):
|
||||
return log.getLogger(self.actor_name)
|
||||
|
||||
@staticmethod
|
||||
def _init_loop() -> asyncio.AbstractEventLoop:
|
||||
return asyncio.get_event_loop()
|
||||
|
||||
def _init_requests(self) -> _request.ActorRequestQueue:
|
||||
return _request.create_request_queue(max_size=self.max_queue_size,
|
||||
loop=self.loop)
|
||||
|
||||
@staticmethod
|
||||
def _setup_actor_id() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class Actor(ActorBase):
|
||||
|
||||
_stop_actor = False
|
||||
_run_actor_task: asyncio.Task
|
||||
|
||||
# Class methods ----------------------------------------------------------
|
||||
|
||||
def __init_subclass__(cls, *args, **kwargs):
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
cls._actor_methods = dict(inspect.getmembers(cls, is_actor_method))
|
||||
|
||||
@classmethod
|
||||
def get_fixture_manager(cls) -> tobiko.FixtureManager:
|
||||
from tobiko.actors import _manager
|
||||
return _manager.actor_manager()
|
||||
|
||||
# Public methods ---------------------------------------------------------
|
||||
|
||||
def setup_fixture(self):
|
||||
self.setup_future.cancel()
|
||||
self.setup_future = self.event_loop.create_future()
|
||||
self._run_actor_task = self.event_loop.create_task(
|
||||
super().setup_fixture()
|
||||
self._run_actor_task = self.loop.create_task(
|
||||
self._run_actor())
|
||||
|
||||
def cleanup_fixture(self):
|
||||
self.cleanup_future.cancel()
|
||||
self.cleanup_future = self.event_loop.create_future()
|
||||
self._cancel_actor = True
|
||||
self.ref.ping_actor('cleanup') # must weak up the actor with a message
|
||||
super().cleanup_fixture()
|
||||
self._stop_actor = True
|
||||
if hasattr(self, '_run_actor_task'):
|
||||
if not self._run_actor_task.done():
|
||||
# must weak up the actor with a message
|
||||
self.ref.ping_actor('cleanup')
|
||||
|
||||
async def setup_actor(self):
|
||||
pass
|
||||
@ -157,51 +215,20 @@ class Actor(tobiko.SharedFixture, typing.Generic[P],
|
||||
async def cleanup_actor(self):
|
||||
pass
|
||||
|
||||
async def on_request_error(
|
||||
self, request: typing.Optional[_request.ActorRequest]):
|
||||
pass
|
||||
|
||||
async def on_cleanup_error(self):
|
||||
pass
|
||||
|
||||
async def ping_actor(self, data: typing.Any = None) -> typing.Any:
|
||||
return data
|
||||
ping_actor.__tobiko_actor_method__ = True # type: ignore[attr-defined]
|
||||
|
||||
# Private instance methods -----------------------------------------------
|
||||
@staticmethod
|
||||
def _init_actor_id() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def _init_log(self):
|
||||
return log.getLogger(self.actor_name)
|
||||
|
||||
@staticmethod
|
||||
def _init_event_loop() -> asyncio.AbstractEventLoop:
|
||||
return asyncio.get_event_loop()
|
||||
|
||||
def _init_actor_request_queue(self) -> _request.ActorRequestQueue:
|
||||
return _request.create_request_queue(max_size=self.max_queue_size,
|
||||
loop=self.event_loop)
|
||||
|
||||
def _init_actor_ref(self) -> ActorRef[P]:
|
||||
return self._actor_ref_class(actor_id=self.actor_id,
|
||||
requests=self.requests)
|
||||
# Private methods --------------------------------------------------------
|
||||
|
||||
async def _run_actor(self):
|
||||
try:
|
||||
await self._setup_actor()
|
||||
self._cancel_actor = False
|
||||
while not self._cancel_actor:
|
||||
request = None
|
||||
try:
|
||||
request = await self.requests.receive_request()
|
||||
await self._receive_request(request)
|
||||
except Exception:
|
||||
await self.on_request_error(request=request)
|
||||
finally:
|
||||
with tobiko.exc_info(reraise=True):
|
||||
await self._cleanup_actor()
|
||||
await self._setup_actor()
|
||||
self._stop_actor = False
|
||||
while not self._stop_actor:
|
||||
request = await self.requests.receive_request()
|
||||
await self._receive_request(request)
|
||||
with tobiko.exc_info(reraise=True):
|
||||
await self._cleanup_actor()
|
||||
|
||||
async def _setup_actor(self):
|
||||
try:
|
||||
@ -211,9 +238,9 @@ class Actor(tobiko.SharedFixture, typing.Generic[P],
|
||||
self.log.exception(
|
||||
f'Failed Setting up actor: {self.actor_name} '
|
||||
f'({self.actor_id})')
|
||||
self.setup_future.set_exception(ex)
|
||||
self.setup_actor_future.set_exception(ex)
|
||||
else:
|
||||
self.setup_future.set_result(self.ref)
|
||||
self.setup_actor_future.set_result(None)
|
||||
self.log.debug(f'Actor setup succeeded: {self.actor_name} '
|
||||
f'({self.actor_id}).')
|
||||
|
||||
@ -223,12 +250,12 @@ class Actor(tobiko.SharedFixture, typing.Generic[P],
|
||||
f'({self.actor_id}).')
|
||||
await self.cleanup_actor()
|
||||
except Exception as ex:
|
||||
self.cleanup_future.set_exception(ex)
|
||||
self.cleanup_actor_future.set_exception(ex)
|
||||
self.log.exception(
|
||||
f'Actor cleanup failed: {self.actor_name} '
|
||||
f'({self.actor_id}).')
|
||||
else:
|
||||
self.cleanup_future.set_result(self.ref)
|
||||
self.cleanup_actor_future.set_result(None)
|
||||
self.log.debug(f'Actor cleanup succeeded: {self.actor_name} '
|
||||
f'({self.actor_id}).')
|
||||
finally:
|
||||
@ -252,46 +279,3 @@ class Actor(tobiko.SharedFixture, typing.Generic[P],
|
||||
if method is None:
|
||||
raise ValueError(f"Invalid request method name: {name}")
|
||||
return method
|
||||
|
||||
|
||||
ActorType = typing.Union[Actor[P], typing.Type[Actor[P]]]
|
||||
|
||||
|
||||
def start_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None) -> ActorRef[P]:
|
||||
return tobiko.setup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager).ref
|
||||
|
||||
|
||||
async def setup_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None,
|
||||
timeout: tobiko.Seconds = None) -> ActorRef[P]:
|
||||
actor = tobiko.setup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager)
|
||||
await asyncio.wait_for(actor.setup_future,
|
||||
timeout=timeout)
|
||||
return actor.ref
|
||||
|
||||
|
||||
async def stop_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None) -> ActorRef[P]:
|
||||
return tobiko.cleanup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager).ref
|
||||
|
||||
|
||||
async def cleanup_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None,
|
||||
timeout: tobiko.Seconds = None) -> ActorRef[P]:
|
||||
actor = tobiko.cleanup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager)
|
||||
await asyncio.wait_for(actor.cleanup_future,
|
||||
timeout=timeout)
|
||||
return actor.ref
|
||||
|
76
tobiko/actors/_manager.py
Normal file
76
tobiko/actors/_manager.py
Normal file
@ -0,0 +1,76 @@
|
||||
# Copyright 2022 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 asyncio
|
||||
import typing
|
||||
|
||||
import tobiko
|
||||
from tobiko.actors import _actor
|
||||
|
||||
|
||||
A = typing.TypeVar('A', bound=_actor.ActorBase)
|
||||
|
||||
|
||||
class ActorManager(tobiko.SharedFixture, tobiko.FixtureManager):
|
||||
pass
|
||||
|
||||
|
||||
def actor_manager(obj: ActorManager = None) -> ActorManager:
|
||||
if obj is None:
|
||||
return tobiko.get_fixture(ActorManager)
|
||||
return tobiko.check_valid_type(obj, ActorManager)
|
||||
|
||||
|
||||
ActorType = typing.Union[A, typing.Type[A]]
|
||||
|
||||
|
||||
def start_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager: ActorManager = None) -> _actor.ActorRef[A]:
|
||||
return tobiko.setup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager).ref
|
||||
|
||||
|
||||
async def setup_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None,
|
||||
timeout: tobiko.Seconds = None) -> _actor.ActorRef[A]:
|
||||
actor = tobiko.setup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager)
|
||||
await asyncio.wait_for(actor.setup_actor_future,
|
||||
timeout=timeout)
|
||||
return actor.ref
|
||||
|
||||
|
||||
async def stop_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None) -> _actor.ActorRef[A]:
|
||||
return tobiko.cleanup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager).ref
|
||||
|
||||
|
||||
async def cleanup_actor(obj: ActorType,
|
||||
fixture_id: typing.Optional[str] = None,
|
||||
manager=None,
|
||||
timeout: tobiko.Seconds = None) -> _actor.ActorRef[A]:
|
||||
actor = tobiko.cleanup_fixture(obj,
|
||||
fixture_id=fixture_id,
|
||||
manager=manager)
|
||||
await asyncio.wait_for(actor.cleanup_actor_future,
|
||||
timeout=timeout)
|
||||
return actor.ref
|
@ -14,15 +14,15 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import abc
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import types
|
||||
import typing
|
||||
|
||||
import decorator
|
||||
|
||||
|
||||
P = typing.TypeVar('P', bound=abc.ABC)
|
||||
P = typing.TypeVar('P')
|
||||
|
||||
GenericMetaBase = abc.ABCMeta
|
||||
if hasattr(typing, 'GenericMeta'):
|
||||
@ -34,6 +34,8 @@ if hasattr(typing, 'GenericMeta'):
|
||||
|
||||
|
||||
class GenericMeta(GenericMetaBase):
|
||||
|
||||
@functools.lru_cache(maxsize=4096)
|
||||
def __getitem__(self, item):
|
||||
# pylint: disable=not-callable
|
||||
cls = self
|
||||
@ -49,11 +51,23 @@ class GenericMeta(GenericMetaBase):
|
||||
return cls
|
||||
|
||||
|
||||
def is_public_function(obj):
|
||||
class Generic(typing.Generic[P], metaclass=GenericMeta):
|
||||
pass
|
||||
|
||||
|
||||
PredicateFunction = typing.Callable[[typing.Any], bool]
|
||||
|
||||
|
||||
def is_public_function(obj) -> bool:
|
||||
return (inspect.isfunction(obj) and
|
||||
getattr(obj, '__name__', '_')[0] != '_')
|
||||
|
||||
|
||||
def is_public_abstract_method(obj) -> bool:
|
||||
return (is_public_function(obj) and
|
||||
getattr(obj, "__isabstractmethod__", False))
|
||||
|
||||
|
||||
class CallHandler(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
@ -62,29 +76,26 @@ class CallHandler(abc.ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CallProxyBase(CallHandler, typing.Generic[P], abc.ABC,
|
||||
metaclass=GenericMeta):
|
||||
class CallProxyBase(CallHandler, Generic[P], abc.ABC):
|
||||
|
||||
def __class_getitem__(cls, item: typing.Type[P]):
|
||||
if isinstance(item, type):
|
||||
return create_call_proxy_class(protocols=(item,),
|
||||
class_name=cls.__name__,
|
||||
bases=(cls,))
|
||||
bases=(cls, item),
|
||||
predicate=cls._is_proxy_method)
|
||||
else:
|
||||
return cls
|
||||
|
||||
def __init_subclass__(cls,
|
||||
*args,
|
||||
**kwargs):
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
# On python < 3.8 must ensure __class_getitem__ is there
|
||||
if sys.version_info < (3, 8):
|
||||
cls.__class_getitem__ = CallProxyBase.__class_getitem__
|
||||
@classmethod
|
||||
def _is_proxy_method(cls, obj) -> bool:
|
||||
return is_public_abstract_method(obj)
|
||||
|
||||
|
||||
class CallProxy(CallProxyBase, typing.Generic[P]):
|
||||
class CallProxy(CallProxyBase, Generic[P]):
|
||||
|
||||
def __init__(self, handle_call: typing.Callable):
|
||||
super(CallProxy, self).__init__()
|
||||
assert callable(handle_call)
|
||||
self._handle_call = handle_call # type: ignore
|
||||
|
||||
@ -93,33 +104,35 @@ class CallProxy(CallProxyBase, typing.Generic[P]):
|
||||
|
||||
|
||||
def create_call_proxy_class(
|
||||
protocols: typing.Tuple[typing.Type[P], ...],
|
||||
protocols: typing.Tuple[type, ...],
|
||||
class_name: str,
|
||||
bases: typing.Tuple[typing.Type, ...] = None,
|
||||
namespace: dict = None) -> typing.Type[P]:
|
||||
bases: typing.Tuple[type, ...] = None,
|
||||
namespace: dict = None,
|
||||
predicate: PredicateFunction = None) -> type:
|
||||
if bases is None:
|
||||
bases = tuple()
|
||||
if predicate is None:
|
||||
predicate = is_public_abstract_method
|
||||
|
||||
def exec_body(ns: typing.Dict[str, typing.Any]):
|
||||
if namespace is not None:
|
||||
ns.update(namespace)
|
||||
for cls in protocols:
|
||||
for member_name, member in list_abstract_methods(cls):
|
||||
if member_name not in ns and is_public_function(member):
|
||||
for member_name, member in inspect.getmembers(cls, predicate):
|
||||
if member_name not in ns:
|
||||
method = create_call_proxy_method(member)
|
||||
ns[member_name] = method
|
||||
|
||||
proxy_class = types.new_class(name=class_name,
|
||||
bases=bases + protocols,
|
||||
exec_body=exec_body)
|
||||
return typing.cast(typing.Type[P], proxy_class)
|
||||
return types.new_class(name=class_name,
|
||||
bases=bases,
|
||||
exec_body=exec_body)
|
||||
|
||||
|
||||
def create_call_proxy(handle_call: typing.Callable,
|
||||
*protocols: typing.Type[P]) -> P:
|
||||
cls = create_call_proxy_class(protocols=protocols,
|
||||
class_name='CallProxy',
|
||||
bases=(CallProxy,))
|
||||
bases=(CallProxy,) + protocols)
|
||||
return cls(handle_call) # type: ignore[call-arg]
|
||||
|
||||
|
||||
@ -135,10 +148,9 @@ def list_abstract_classes(cls: typing.Type) \
|
||||
def list_abstract_methods(cls: typing.Type) \
|
||||
-> typing.List[typing.Tuple[str, typing.Callable]]:
|
||||
methods: typing.List[typing.Tuple[str, typing.Callable]] = []
|
||||
if inspect.isabstract(cls):
|
||||
for name, member in inspect.getmembers(cls, inspect.isfunction):
|
||||
if getattr(member, "__isabstractmethod__", False):
|
||||
methods.append((name, member))
|
||||
for name, member in inspect.getmembers(cls, inspect.isfunction):
|
||||
if getattr(member, "__isabstractmethod__", False):
|
||||
methods.append((name, member))
|
||||
return methods
|
||||
|
||||
|
||||
|
@ -23,10 +23,10 @@ import fixtures
|
||||
from oslo_log import log
|
||||
import testtools
|
||||
|
||||
import tobiko
|
||||
from tobiko.common import _detail
|
||||
from tobiko.common import _exception
|
||||
from tobiko.common import _testcase
|
||||
from tobiko.common import _loader
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
@ -93,9 +93,8 @@ def get_fixture(obj: FixtureType,
|
||||
"""
|
||||
if isinstance(obj, fixtures.Fixture):
|
||||
return typing.cast(F, obj)
|
||||
if manager is None:
|
||||
manager = FIXTURES
|
||||
return manager.get_fixture(obj, fixture_id=fixture_id)
|
||||
return fixture_manager(obj, manager).get_fixture(
|
||||
obj, fixture_id=fixture_id)
|
||||
|
||||
|
||||
def get_fixture_name(obj) -> str:
|
||||
@ -113,7 +112,7 @@ def get_fixture_name(obj) -> str:
|
||||
def get_fixture_class(obj: FixtureType) -> typing.Type[fixtures.Fixture]:
|
||||
"""It gets fixture class"""
|
||||
if isinstance(obj, str):
|
||||
obj = tobiko.load_object(obj)
|
||||
obj = _loader.load_object(obj)
|
||||
|
||||
if not inspect.isclass(obj):
|
||||
obj = type(obj)
|
||||
@ -145,8 +144,8 @@ def remove_fixture(obj: FixtureType,
|
||||
fixture_id: typing.Any = None,
|
||||
manager: 'FixtureManager' = None) -> typing.Optional[F]:
|
||||
"""Unregister fixture identified by given :param obj: if any"""
|
||||
manager = manager or FIXTURES
|
||||
return manager.remove_fixture(obj, fixture_id=fixture_id)
|
||||
return fixture_manager(obj, manager).remove_fixture(
|
||||
obj, fixture_id=fixture_id)
|
||||
|
||||
|
||||
@typing.overload
|
||||
@ -277,7 +276,7 @@ def use_fixture(obj: FixtureType,
|
||||
with on the fixture
|
||||
"""
|
||||
fixture = setup_fixture(obj, fixture_id=fixture_id, manager=manager)
|
||||
_testcase.add_cleanup(tobiko.cleanup_fixture, fixture)
|
||||
_testcase.add_cleanup(cleanup_fixture, fixture)
|
||||
return fixture
|
||||
|
||||
|
||||
@ -294,7 +293,7 @@ def get_name_and_object(obj: F) -> typing.Tuple[str, F]:
|
||||
def get_name_and_object(obj: typing.Any) -> typing.Tuple[str, typing.Any]:
|
||||
'''Get (name, obj) tuple identified by given :param obj:'''
|
||||
if isinstance(obj, str):
|
||||
return obj, tobiko.load_object(obj)
|
||||
return obj, _loader.load_object(obj)
|
||||
else:
|
||||
return get_object_name(obj), obj
|
||||
|
||||
@ -459,7 +458,7 @@ def get_object_name(obj) -> str:
|
||||
raise TypeError(f"Unable to get fixture name from object {obj!r}")
|
||||
|
||||
|
||||
class FixtureManager(object):
|
||||
class FixtureManager:
|
||||
|
||||
def __init__(self):
|
||||
self.fixtures: typing.Dict[str, F] = {}
|
||||
@ -497,6 +496,18 @@ class FixtureManager(object):
|
||||
return self.fixtures.pop(name, None)
|
||||
|
||||
|
||||
def fixture_manager(obj: FixtureType,
|
||||
manager: FixtureManager = None) -> FixtureManager:
|
||||
if manager is None:
|
||||
if isinstance(obj, str):
|
||||
obj = _loader.load_object(obj)
|
||||
if hasattr(obj, 'get_fixture_manager'):
|
||||
manager = obj.get_fixture_manager()
|
||||
else:
|
||||
manager = FIXTURES
|
||||
return _exception.check_valid_type(manager, FixtureManager)
|
||||
|
||||
|
||||
FIXTURES = FixtureManager()
|
||||
|
||||
|
||||
@ -526,7 +537,12 @@ class SharedFixture(fixtures.Fixture):
|
||||
__tobiko_fixture_name__: typing.Optional[str] = None
|
||||
__tobiko_fixture_id__: typing.Any = None
|
||||
|
||||
@classmethod
|
||||
def get_fixture_manager(cls) -> FixtureManager:
|
||||
return FIXTURES
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# make sure class states can be used before setUp
|
||||
self._clear_cleanups()
|
||||
|
||||
@ -586,7 +602,7 @@ class SharedFixture(fixtures.Fixture):
|
||||
class FixtureProperty(property):
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
instance = instance or tobiko.get_fixture(owner)
|
||||
instance = instance or get_fixture(owner)
|
||||
return super(FixtureProperty, self).__get__(instance, owner)
|
||||
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
# under the License.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import abc
|
||||
import typing
|
||||
|
||||
import tobiko
|
||||
@ -23,11 +22,26 @@ from tobiko.tests import unit
|
||||
from tobiko import actors
|
||||
|
||||
|
||||
class Greeter(abc.ABC):
|
||||
class Greeter(actors.Actor):
|
||||
|
||||
@abc.abstractmethod
|
||||
setup_called = False
|
||||
cleanup_called = False
|
||||
|
||||
async def setup_actor(self):
|
||||
self.setup_called = True
|
||||
|
||||
async def cleanup_actor(self):
|
||||
self.cleanup_called = True
|
||||
|
||||
@actors.actor_method
|
||||
async def greet(self, whom: str, greeted: 'Greeted'):
|
||||
raise NotImplementedError
|
||||
assert self.setup_called
|
||||
assert not self.cleanup_called
|
||||
if not whom:
|
||||
raise ValueError("'whom' parameter can't be empty")
|
||||
|
||||
self.log.info(f"Hello {whom}!")
|
||||
await greeted.greeted(whom=whom, greeter=self)
|
||||
|
||||
|
||||
class Greeted:
|
||||
@ -40,33 +54,9 @@ class Greeted:
|
||||
self.whom = whom
|
||||
|
||||
|
||||
class GreeterActor(actors.Actor[Greeter]):
|
||||
|
||||
setup_called = False
|
||||
cleanup_called = False
|
||||
|
||||
async def setup_actor(self):
|
||||
self.setup_called = True
|
||||
|
||||
async def cleanup_actor(self):
|
||||
self.cleanup_called = True
|
||||
|
||||
@actors.actor_method
|
||||
async def greet(self, whom: str, greeted: Greeted):
|
||||
assert isinstance(self, Greeter)
|
||||
assert isinstance(self, GreeterActor)
|
||||
assert self.setup_called
|
||||
assert not self.cleanup_called
|
||||
if not whom:
|
||||
raise ValueError("'whom' parameter can't be empty")
|
||||
|
||||
self.log.info(f"Hello {whom}!")
|
||||
await greeted.greeted(whom=whom, greeter=self)
|
||||
|
||||
|
||||
class ActorTest(unit.TobikoUnitTest):
|
||||
|
||||
actor = tobiko.required_fixture(GreeterActor, setup=False)
|
||||
actor = tobiko.required_fixture(Greeter, setup=False)
|
||||
|
||||
async def test_setup_actor(self):
|
||||
self.assertFalse(self.actor.setup_called)
|
||||
@ -76,6 +66,13 @@ class ActorTest(unit.TobikoUnitTest):
|
||||
self.assertFalse(self.actor.cleanup_called)
|
||||
|
||||
async def test_cleanup_actor(self):
|
||||
self.assertFalse(self.actor.setup_called)
|
||||
self.assertFalse(self.actor.cleanup_called)
|
||||
await actors.cleanup_actor(self.actor)
|
||||
self.assertFalse(self.actor.setup_called)
|
||||
self.assertFalse(self.actor.cleanup_called)
|
||||
|
||||
async def test_cleanup_actor_after_setup(self):
|
||||
await actors.setup_actor(self.actor)
|
||||
self.assertTrue(self.actor.setup_called)
|
||||
self.assertFalse(self.actor.cleanup_called)
|
||||
@ -90,8 +87,7 @@ class ActorTest(unit.TobikoUnitTest):
|
||||
|
||||
async def test_async_request(self):
|
||||
greeter = actors.start_actor(self.actor)
|
||||
self.assertIsInstance(greeter, actors.ActorRef)
|
||||
self.assertIsInstance(greeter, Greeter)
|
||||
self.assertIsInstance(greeter, actors.ActorRef[Greeter])
|
||||
greeted = Greeted()
|
||||
await greeter.greet(whom=self.id(), greeted=greeted)
|
||||
self.assertEqual(self.id(), greeted.whom)
|
||||
@ -99,8 +95,7 @@ class ActorTest(unit.TobikoUnitTest):
|
||||
|
||||
async def test_async_request_failure(self):
|
||||
greeter = actors.start_actor(self.actor)
|
||||
self.assertIsInstance(greeter, actors.ActorRef)
|
||||
self.assertIsInstance(greeter, Greeter)
|
||||
self.assertIsInstance(greeter, actors.ActorRef[Greeter])
|
||||
greeted = Greeted()
|
||||
|
||||
try:
|
||||
|
@ -16,7 +16,7 @@ from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
import tobiko
|
||||
from tobiko.common.managers import loader
|
||||
from tobiko.common import _loader
|
||||
from tobiko.tests.unit import TobikoUnitTest
|
||||
|
||||
|
||||
@ -33,8 +33,8 @@ class TestLoader(TobikoUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestLoader, self).setUp()
|
||||
self.manager = loader.LoaderManager()
|
||||
self.patch(loader, 'LOADERS', self.manager)
|
||||
self.manager = _loader.LoaderManager()
|
||||
self.patch(_loader, 'LOADERS', self.manager)
|
||||
|
||||
def test_load_object_with_none(self):
|
||||
object_id = '.'.join([__name__, 'SOME_NONE'])
|
||||
|
Loading…
x
Reference in New Issue
Block a user