From 5177c1239e2e10238afd3a04970a6bb1109e3df8 Mon Sep 17 00:00:00 2001 From: Federico Ressi Date: Thu, 3 Mar 2022 10:53:48 +0100 Subject: [PATCH] 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 --- tobiko/__init__.py | 6 +- tobiko/actors/__init__.py | 18 +- tobiko/actors/_actor.py | 266 ++++++++---------- tobiko/actors/_manager.py | 76 +++++ tobiko/actors/_proxy.py | 68 +++-- tobiko/common/_fixture.py | 38 ++- .../common/{managers/loader.py => _loader.py} | 0 tobiko/common/managers/__init__.py | 0 tobiko/tests/unit/actors/test_actor.py | 61 ++-- tobiko/tests/unit/test_loader.py | 6 +- 10 files changed, 312 insertions(+), 227 deletions(-) create mode 100644 tobiko/actors/_manager.py rename tobiko/common/{managers/loader.py => _loader.py} (100%) delete mode 100644 tobiko/common/managers/__init__.py diff --git a/tobiko/__init__.py b/tobiko/__init__.py index 09d9e1b14..1d6f532e9 100644 --- a/tobiko/__init__.py +++ b/tobiko/__init__.py @@ -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 diff --git a/tobiko/actors/__init__.py b/tobiko/actors/__init__.py index ec4341938..b8b6f06f2 100644 --- a/tobiko/actors/__init__.py +++ b/tobiko/actors/__init__.py @@ -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 diff --git a/tobiko/actors/_actor.py b/tobiko/actors/_actor.py index f3cf65ec3..065ded9bd 100644 --- a/tobiko/actors/_actor.py +++ b/tobiko/actors/_actor.py @@ -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 diff --git a/tobiko/actors/_manager.py b/tobiko/actors/_manager.py new file mode 100644 index 000000000..4e3f070f7 --- /dev/null +++ b/tobiko/actors/_manager.py @@ -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 diff --git a/tobiko/actors/_proxy.py b/tobiko/actors/_proxy.py index 26b0d9407..611c31150 100644 --- a/tobiko/actors/_proxy.py +++ b/tobiko/actors/_proxy.py @@ -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 diff --git a/tobiko/common/_fixture.py b/tobiko/common/_fixture.py index f7f14bef7..7d67bd5c9 100644 --- a/tobiko/common/_fixture.py +++ b/tobiko/common/_fixture.py @@ -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) diff --git a/tobiko/common/managers/loader.py b/tobiko/common/_loader.py similarity index 100% rename from tobiko/common/managers/loader.py rename to tobiko/common/_loader.py diff --git a/tobiko/common/managers/__init__.py b/tobiko/common/managers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tobiko/tests/unit/actors/test_actor.py b/tobiko/tests/unit/actors/test_actor.py index 56d77c3f9..a3a75e65b 100644 --- a/tobiko/tests/unit/actors/test_actor.py +++ b/tobiko/tests/unit/actors/test_actor.py @@ -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: diff --git a/tobiko/tests/unit/test_loader.py b/tobiko/tests/unit/test_loader.py index 31e4accf7..3ea5fc576 100644 --- a/tobiko/tests/unit/test_loader.py +++ b/tobiko/tests/unit/test_loader.py @@ -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'])