123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- # Copyright (c) 2016 Anki, Inc.
- #
- # 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 in the file LICENSE.txt or 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.
-
- __all__ = []
-
- import threading
-
- import asyncio
- import concurrent.futures
- import functools
- import inspect
- import traceback
- import types
-
-
- class _MetaBase(type):
- '''Metaclass for all Cozmo package classes.
-
- Ensures that all *_factory class attributes are wrapped into a _Factory
- descriptor to automatically support synchronous operation.
- '''
-
- def __new__(mcs, name, bases, attrs, **kw):
- for k, v in attrs.items():
- if k.endswith('_factory'):
- # TODO: check type here too
- attrs[k] = _Factory(v)
- return super().__new__(mcs, name, bases, attrs, **kw)
-
- def __setattr__(cls, name, val):
- if name.endswith('_factory'):
- cls.__dict__[name].__set__(cls, val)
- else:
- super().__setattr__(name, val)
-
-
- class Base(metaclass=_MetaBase):
- '''Base class for Cozmo package objects.
-
- *_factory attributes are automatically wrapped into a _Factory descriptor to
- support synchronous operation.
- '''
-
- # used by SyncFatory
- _sync_thread_id = None
- _sync_abort_future = None
-
- def __init__(self, _sync_thread_id=None, _sync_abort_future=None, **kw):
- # machinery for SyncFactory
- if _sync_abort_future is not None:
- self._sync_thread_id = threading.get_ident()
- else:
- self._sync_thread_id = _sync_thread_id
- self._sync_abort_future = _sync_abort_future
- super().__init__(**kw)
-
- @property
- def loop(self):
- ''':class:`asyncio.BaseEventLoop`: loop instance that this object is registered with.'''
- return getattr(self, '_loop', None)
-
-
-
- class _Factory:
- '''Descriptor to wraps an object factory method.
-
- If the factory is called while the program is running in synchronous mode
- then the objects returned by the factory will be wrapped by a _SyncProxy
- object, which translates asynchronous responses to synchronous ones
- when made outside of the thread the top level object's event loop is running on.
- '''
-
- def __init__(self, factory):
- self._wrapped_factory = factory
-
- def __get__(self, ins, owner):
- sync_thread_id = getattr(ins, '_sync_thread_id', None)
- loop = getattr(ins, '_loop', None)
- if sync_thread_id:
- # Object instance is running in sync mode
- return _SyncFactory(self._wrapped_factory, loop, sync_thread_id, ins._sync_abort_future)
- # Pass through to the factory. Set loop here as a convenience as all
- # Cozmo objects require it by virtue of inheriting from event.Dispatcher
- return functools.partial(self._wrapped_factory, loop=loop)
-
- def __set__(self, ins, val):
- self._wrapped_factory = val
-
-
- def _SyncFactory(f, loop, thread_id, sync_abort_future):
- '''Instantiates a class by calling a factory function and then wrapping it with _SyncProxy'''
- def factory(*a, **kw):
- kw['_sync_thread_id'] = thread_id
- kw['_sync_abort_future'] = sync_abort_future
- if 'loop' not in kw:
- kw['loop'] = loop
- obj = f(*a, **kw)
- return _mkproxy(obj)
- return factory
-
-
- def _mkpt(cls, name):
- # create a passthru function
- f = getattr(cls, name)
- @functools.wraps(f)
- def pt(self, *a, **kw):
- wrap = self.__wrapped__
- f = object.__getattribute__(wrap, name)
- return f(*a, **kw)
- return pt
-
-
- class _SyncProxy:
- '''Wraps cozmo objects to provide synchronous access when required.
-
- Each method call and attribute access is passed through to the wrapped object.
-
- If the caller is operating in a different thread to the callee (for example, the
- caller is operating outside of the context of the event loop), then any
- calls to the wrapped object are dispatched to the event loop running on the
- loop's native thread.
-
- Returned co-routines functions and Futures are waited upon until completion.
- '''
-
- def __init__(self, wrapped):
- self.__wrapped__ = wrapped
-
- def __getattribute__(self, name):
- wrapped = object.__getattribute__(self, '__wrapped__')
- if name == '__wrapped__':
- return wrapped
-
- # if name points to a property, this will execute the property getter
- # and return the value, else returns the value according to usual
- # lookup rules.
- value = object.__getattribute__(wrapped, name)
-
- # determine whether the call is being invoked locally, from within the
- # event loop's native thread, or elsewhere (usually the main thread)
- thread_id = object.__getattribute__(wrapped, '_sync_thread_id')
- is_local_thread = thread_id is None or threading.get_ident() == thread_id
-
- if is_local_thread:
- # passthru/no-op if being called from the same thread as the object
- # was created from.
- return value
-
- if inspect.ismethod(value) and not asyncio.iscoroutinefunction(value):
- # Wrap the sync method into a coroutine that can be dispatched
- # from the same thread as the main event loop is running in
- f = value.__func__
- f = _to_coroutine(f)
- value = types.MethodType(f, wrapped)
- #value = types.MethodType(f, self)
-
- elif inspect.isfunction(value) and not asyncio.iscoroutinefunction(value):
- # Dispatch functions in the main event loop thread too
- value = _to_coroutine(value)
-
- if inspect.isawaitable(value):
- return _dispatch_coroutine(value, wrapped._loop, wrapped._sync_abort_future)
-
- elif asyncio.iscoroutinefunction(value):
- # Wrap coroutine into synchronous dispatch
- @functools.wraps(value)
- def wrap(*a, **kw):
- return _dispatch_coroutine(value(*a, **kw), wrapped._loop, wrapped._sync_abort_future)
- return wrap
-
- return value
-
- def __setattr__(self, name, value):
- if name == '__wrapped__':
- return super().__setattr__(name, value)
- wrapped = object.__getattribute__(self, '__wrapped__')
- return wrapped.__setattr__(name, value)
-
- def __repr__(self):
- wrapped = self.__wrapped__
- return "wrapped-" + object.__getattribute__(wrapped, '__repr__')()
-
-
- def _to_coroutine(f):
- @functools.wraps(f)
- async def wrap(*a, **kw):
- return f(*a, **kw)
- return wrap
-
-
- def _mkproxy(obj):
- '''Create a _SyncProxy for an object.'''
- # dynamically generate a class tailored for the wrapped object.
- d = {}
- cls = obj.__class__
- for name in dir(cls):
- if ((name.endswith('__') and name.startswith('__'))
- and name not in ('__class__', '__new__', '__init__', '__getattribute__', '__setattr__', '__repr__')):
- d[name] = _mkpt(cls, name)
-
- if hasattr(obj, '__aenter__'):
- d['__enter__'] = lambda self: self.__wrapper__.__aenter__()
- d['__exit__'] = lambda self, *a: self.__wrapper__.__aexit__(*a)
-
- cls = type("_proxy_"+obj.__class__.__name__, (_SyncProxy,), d)
- proxy = cls(obj)
- obj.__wrapper__ = proxy
- return proxy
-
-
- def _dispatch_coroutine(co, loop, abort_future):
- '''Execute a coroutine in a loop's thread and block till completion.
-
- Wraps a co-routine function; calling the function causes the co-routine
- to be dispatched in the event loop's thread and blocks until that call completes.
-
- Waits for either the coroutine or abort_future to complete.
- abort_future provides the main event loop with a means of triggering a
- clean shutdown in the case of an exception.
- '''
- fut = asyncio.run_coroutine_threadsafe(co, loop)
- result = concurrent.futures.wait((fut, abort_future), return_when=concurrent.futures.FIRST_COMPLETED)
- result = list(result.done)[0].result()
- if getattr(result, '__wrapped__', None) is None:
- # If the call retuned the wrapped contents of a _SyncProxy then return
- # the enclosing proxy instead to the sync caller
- wrapper = getattr(result, '__wrapper__', None)
- if wrapper is not None:
- result = wrapper
- return result
|