class FakeNode(metaclass=WAMPInitMeta): """A fake Node class that emulates the interesting API of r.r.node.node.WAMPNode, just to test the wamp stuff""" on_node_register = Signal() on_node_registration_success = Signal() on_node_registration_failure = Signal() on_node_unregister = Signal() node_registered = False def __init__(self, context, path): self.node_context = context self.node_path = path self.loop = context.loop async def node_register(self): self.node_registered = True await self.on_node_register.notify(node=self, context=self.node_context) async def node_unregister(self): self.node_registered = False await self.on_node_unregister.notify(node=self, context=self.node_context)
async def test_04_signal_with_methods(events): asignal = Signal() class A(object): def __init__(self, name): self.ev = events[name] called = False async def handler(self, arg, kw): self.called = (arg, kw) self.ev.set() a1 = A('a1') a2 = A('a2') asignal.connect(a1.handler) asignal.connect(a2.handler) assert len(asignal.subscribers) == 2 await asignal.notify(1, kw='a') await events.wait() assert a1.called == (1, 'a') assert a2.called == (1, 'a')
def test_13_disconnect_wrapper(): c = dict(called=0, disconnect_handler=None) asignal = Signal() @asignal.on_disconnect def asignal(handler, subscribers, disconnect, notify): c['called'] += 1 c['disconnect_handler'] = handler assert len(subscribers) == 1 disconnect(handler) return 'foo' def handler(*args, **kwargs): pass asignal.connect(handler) res = asignal.disconnect(handler) assert res == 'foo' assert c['called'] == 1 assert c['disconnect_handler'] == handler assert len(asignal.subscribers) == 0 c = dict(called=0, disconnect_handler=None) class A(metaclass=SignalAndHandlerInitMeta): click = Signal() @click.on_disconnect def click(self, handler, subscribers, disconnect, notify): c['called'] += 1 c['disconnect_handler'] = handler assert len(subscribers) == 1 disconnect(handler) return 'foo' @handler('click') def handler(self, *args, **kwargs): pass a = A() def handler2(*args, **kwargs): pass a.click.connect(handler2) res = a.click.disconnect(handler2) assert res == 'foo' assert c['called'] == 1 assert c['disconnect_handler'] == handler2 # class-level handlers are excluded assert len(a.click.subscribers) == 0
class A(metaclass=SignalAndHandlerInitMeta): click = Signal(Signal.FLAGS.SORT_TOPDOWN) @handler('click') def z(self): called.append('z')
class A(metaclass=MySignalMeta): click = Signal() @handler('click') def handler1(self, *args, **kwargs): c['handler_called'] = True
async def test_10_external_signaller_async(events): from metapensiero.signal import ExternalSignaller c = dict(publish_called=False, register_called=False) class MyExternalSignaller(object): async def publish_signal(self, signal, instance, loop, args, kwargs): c['publish_called'] = (signal, instance, loop, args, kwargs) events.publish.set() def register_signal(self, signal, name): c['register_called'] = (signal, name) ExternalSignaller.register(MyExternalSignaller) assert c['register_called'] is False assert c['publish_called'] is False signaller = MyExternalSignaller() asignal = Signal(name='foo', external=signaller) assert c['register_called'] == (asignal, 'foo') assert c['publish_called'] is False await asignal.notify('foo', zoo='bar') await events.publish.wait() assert c['publish_called'] == (asignal, None, asyncio.get_event_loop(), ('foo', ), { 'zoo': 'bar' }) assert c['register_called'] == (asignal, 'foo')
class RPCTest(WAMPNode): foo = Signal() @handler('foo') def local_bar(self, **_): nonlocal counter counter += 1
class Service(BaseService): ping = Signal() pong = Signal() @handler('@service2.pong') def on_pong(self, details): events.pong.set() @handler('@service1.ping') def on_ping(self, details): events.ping.set() self.pong.notify() @handler('on_start') def _set_started_event(self): events[self.node_name].set()
async def test_01_signal_with_functions(events): asignal = Signal() c = dict(called1=False, called2=False) def handler1(arg, kw): # let python get the outer var here without using PY3 "nonlocal" c['called1'] = (arg, kw) def handler2(arg, kw): c['called2'] = (arg, kw) asignal.connect(handler1) asignal.connect(handler2) assert len(asignal.subscribers) == 2 await asignal.notify(1, kw='a') assert c['called1'] == (1, 'a') assert c['called2'] == (1, 'a')
class RPCTest(WAMPNode): foo = Signal() foo.name = '.' @call('.') def me(self, **_): nonlocal counter counter += 1 return counter
class Session(ClientSession, metaclass=SignalAndHandlerInitMeta): "A client session, enriched with some signals." on_join = Signal() "Signal emitted when the session is joined." on_leave = Signal() "Signal emitted when the session is detached." def onJoin(self, details): "Emit the :attr:`on_join` signal." loop = self.config.extra['joined']._loop self.on_join.notify(details, loop=loop) super().onJoin(details) def onLeave(self, details): "Emit the :attr:`on_leave` signal." loop = self.config.extra['joined']._loop self.on_leave.notify(details, loop=loop) super().onLeave(details)
class A(metaclass=SignalAndHandlerInitMeta): click = Signal() @click.on_notify def click(self, subscribers, notify, *args, **kwargs): notify('foo', k=2) @handler('dblclick') def handler(self, *args, **kwargs): pass
class A(metaclass=MySignalMeta): click = Signal() @handler('click') def handler1(self, *args, **kwargs): pass @handler('myext.dbclick') def handler2(self, *args, **kwargs): pass
class A(metaclass=SignalAndHandlerInitMeta): me = Signal() me.name = '.' def __init__(self): self.called = False @handler('.') def onme(self, arg, kw): self.called = (arg, kw)
async def test_03_signal_with_mixed_functions(events): asignal = Signal() c = dict(called1=False, called2=False) events.define('h1') async def handler1(arg, kw): c['called1'] = (arg, kw) events.h1.set() def handler2(arg, kw): c['called2'] = (arg, kw) asignal.connect(handler1) asignal.connect(handler2) assert len(asignal.subscribers) == 2 await asignal.notify(1, kw='a') assert c['called2'] == (1, 'a') await events.wait() assert c['called1'] == (1, 'a')
class RPCTest(WAMPNode): asignal = Signal() @call def callee(self): return self @handler('asignal') def store_incoming(self, node): events.asignal.set() self.node = node
class A(metaclass=SignalAndHandlerInitMeta): click = Signal() def __init__(self, name): self.called = False self.a_ev = events['a_' + name] @handler('click') async def onclick(self, arg, kw): self.called = (arg, kw) self.a_ev.set() return 1
class A(object): # the name here is needed for classes that don't explicitly support # signals click = Signal(name='click') def __init__(self, name): self.called = False self.click.connect(self.onclick) self.on_click_ev = events[name] async def onclick(self, arg, kw): self.called = (arg, kw) self.on_click_ev.set()
class A(metaclass=SignalAndHandlerInitMeta): click = Signal() @click.on_disconnect def click(self, handler, subscribers, disconnect, notify): c['called'] += 1 c['disconnect_handler'] = handler assert len(subscribers) == 1 disconnect(handler) return 'foo' @handler('click') def handler(self, *args, **kwargs): pass
class A(metaclass=SignalAndHandlerInitMeta): click = Signal() def __init__(self): self.called = False self.called2 = False @handler('click') def onclick(self, arg, kw): self.called = (arg, kw) @handler('click') async def click2(self, arg, kw): self.called2 = (arg, kw)
class A(metaclass=SignalAndHandlerInitMeta): click = Signal() @click.on_notify def click(self, subscribers, notify, *args, **kwargs): c['called'] += 1 c['wrap_args'] = (args, kwargs) assert len(subscribers) == 2 assert isinstance(self, A) notify('foo', k=2) return 'foo' @handler('click') def handler(self, *args, **kwargs): c['handler_called'] += 1 c['handler_args'] = (args, kwargs)
async def test_12_connect_wrapper(events): c = dict(called=0, connect_handler=None, handler_called=0, handler_args=None) asignal = Signal() @asignal.on_connect def asignal(handler, subscribers, connect, notify): c['called'] += 1 c['connect_handler'] = handler assert len(subscribers) == 0 connect(handler) return 'foo' def handler(*args, **kwargs): c['handler_called'] += 1 c['handler_args'] = (args, kwargs) res = asignal.connect(handler) res2 = await asignal.notify('bar', k=1) assert res == 'foo' assert c['called'] == 1 assert c['connect_handler'] == handler assert c['handler_called'] == 1 assert c['handler_args'] == (('bar', ), {'k': 1}) c = dict(called=0, connect_handler=None, handler_called=0, handler_args=None, handler2_called=0, handler2_args=None) class A(metaclass=SignalAndHandlerInitMeta): click = Signal() @click.on_connect def click(self, handler, subscribers, connect, notify): c['called'] += 1 c['connect_handler'] = handler assert len(subscribers) == 0 connect(handler) return 'foo' @handler('click') def handler(self, *args, **kwargs): c['handler_called'] += 1 c['handler_args'] = (args, kwargs) a = A() def handler2(*args, **kwargs): c['handler2_called'] += 1 c['handler2_args'] = (args, kwargs) res = a.click.connect(handler2) res2 = await a.click.notify('bar', k=1) assert res == 'foo' assert c['called'] == 1 assert c['handler_called'] == 1 assert c['connect_handler'] == handler2 assert c['handler_args'] == (('bar', ), {'k': 1}) assert c['handler2_called'] == 1 assert c['handler2_args'] == (('bar', ), {'k': 1})
class Connection(Client, metaclass=SignalAndHandlerInitMeta): "A client connection, enriched with some signals." on_connect = Signal() """Signal emitted when the connection is activated and the session has joined the realm. """ def __init__(self, url, realm, loop=None, **kwargs): """:param str url: a :term:`WAMP` connection url :param str realm: a :term:`WAMP` realm to enter :param loop: an optional asyncio loop Every other keyword argument will be passed to the underlying autobahn client. """ super().__init__(url, realm, loop=None, **kwargs) self.session = None self.session_details = None def _notify_disconnect(self): """NOTE: This is not a coroutine but returns one.""" try: return self.on_disconnect.notify(loop=self.loop) finally: self.session = None self.session_details = None async def _on_session_leave(self, details): return self._notify_disconnect() async def connect(self, username=None, password=None): "Emits the :attr:`on_connect` signal." session, sess_details = await super().connect(username, password, session_class=Session) self.session = session self.session_details = sess_details await self.on_connect.notify(session=session, session_details=sess_details, loop=self.loop) session.on_leave.connect(self._on_session_leave) return session, sess_details @property def connected(self): """Returns ``True`` if this connection is attached to a session.""" return self.session is not None and self.session.is_attached() async def disconnect(self): "Emits the :attr:`on_disconnect` signal." await self._notify_disconnect() await super().disconnect() def new_context(self): """ Return a new :class:`~metapensiero.raccoon.node.node.WAMPNodeContext` instance tied to this connection. """ return WAMPNodeContext(loop=self.loop, wamp_session=self.session) @on_connect.on_connect async def on_connect(self, handler, subscribers, connect): """Call handler immediately if the session is attached already.""" if self.connected: res = connect.notify(handler, session=self.session, session_details=self.session_details) else: res = self.loop.create_future() res.set_result(None) connect(handler) return res on_disconnect = Signal() "Signal emitted when the connection is deactivated." @on_disconnect.on_connect async def on_disconnect(self, handler, subscribers, disconnect): """Call handler immediately if the session is already attached.""" if not self.connected: res = disconnect.notify(handler, loop=self.loop) else: res = self.loop.create_future() res.set_result(None) disconnect(handler) return res def run(self): """Adds a ``SIGTERM`` handler and runs the loop until the connection ends or the process is killed. """ try: self.loop.add_signal_handler(signal.SIGTERM, self.loop.stop) except NotImplementedError: # signals are not available on Windows pass try: self.loop.run_forever() except KeyboardInterrupt: # wait until we send Goodbye if user hit ctrl-c # (done outside this except so SIGTERM gets the same handling) pass # give Goodbye message a chance to go through, if we still # have an active session if self.protocol._session: self.loop.run_until_complete(self.protocol._session.leave()) self.loop.close()
class RPCTest(FakeNode): on_test_event = Signal()
class WAMPNode(Node, serialize.Serializable, metaclass=WAMPInitMeta): """A Node subclass to deal with WAMP stuff. An instance gets local and WAMP pub/sub, WAMP RPC and automatic tree addressing. This is done by performing other two operations: *register* and *unregister* to be performed possibly at a different (later) time than the bind operation. This class is coded to run in tandem with the :class:`~.context.WAMPNodeContext` class that supplies the necessary informations about WAMP connection state. :meth:`node_register` should be called after the WAMP session has *joined* the Crossbar router. """ node_registered = False """It's ``True`` if this mode's :meth:`node_register` has been called and that this node has successfully completed the registration process. """ @signal def on_node_register(self, node, path, context, parent, points): """Signal emitted when the node's resources are registered. :param node: the bound node :type node: :class:`Node` :param path: the path where the node is bound, available also as `node.node_path` :type path: :class:`~.path.Path` :param context: the node_context of the node :type context: :class:`~.context.NodeContext` :param parent: this node's parent, if any :type parent: :class:`Node` :param points: a set containing the rpc points created by the registration """ on_node_register = Signal() """Signal emitted when :meth:`node_register` is called. Its events have two keywords, ``node`` and ``context``. """ on_node_registration_failure = Signal() """Signal emitted when registration fails. Its events have two keywords, ``node`` and ``context``. """ on_node_registration_success = Signal() """Signal emitted when the registration is complete. Its events have two keywords, ``node`` and ``context``. """ on_node_unregister = Signal() """Signal emitted when :meth:`node_unregister` is called. Its events have two keywords, ``node`` and ``context``. """ async def _node_after_bind(self, path, context=None, parent=None): """Specialized to attach this node to the parent's :attr:`on_node_register` and :attr:`on_node_unbind` signals. It automatically calls :meth:`node_register`. """ await super()._node_after_bind(path, context, parent) if parent is not None and isinstance(parent, WAMPNode): parent.on_node_register.connect(self._node_on_parent_register) await self.node_register() async def _node_on_parent_register(self, node, context): if self.node_context is None: self.node_context = context.new() await self.node_register() async def _node_unbind(self): """Specialized to call :meth:`node_unregister`.""" await self.node_unregister() await super()._node_unbind() if self.node_registered: del self.node_registered def call(self, path, *args, **kwargs): """Call another rpc endpoint published via :term:`WAMP`. .. important:: The ``disclose_me=True`` option (was the default in old ``raccoon.api`` framework) is now managed directly by crossbar in it's realm/role configuration. .. note:: this isn't a coroutine but it returns one. """ return self.__class__.manager.call(self, path, *args, **kwargs) @classmethod def node_deserialize(cls, value, endpoint_node): return endpoint_node.remote(value) async def node_register(self): """Register this node to the :term:`WAMP` session. It emits the `on_node_register` event.""" if not self.node_registered and self.node_context is not None and \ self.node_context.wamp_session is not None and \ self.node_context.wamp_session.is_attached(): await self.on_node_register.notify(node=self, context=self.node_context) @classmethod def node_serialize(cls, instance, srcpoint_node): if instance.node_path is None: raise serialize.SerializationError( "This instance cannot be serialized") return serialize.Serialized(str(instance.node_path)) async def node_unregister(self): """Unregisters the node from the :term:`WAMP` session. It emits the `on_node_unregister` event.""" if self.node_registered: await self.on_node_unregister.notify(node=self, context=self.node_context) def remote(self, path): "Return a :class:`~.proxy.Proxy` instance on the given `path`." return Proxy(self, path)
def test_11_notify_wrapper(events): c = dict(called=0, wrap_args=None, handler_called=0, handler_args=None) asignal = Signal() @asignal.on_notify def asignal(subscribers, notify, *args, **kwargs): c['called'] += 1 c['wrap_args'] = (args, kwargs) assert len(subscribers) == 1 notify('foo', k=2) return 'foo' def handler(*args, **kwargs): c['handler_called'] += 1 c['handler_args'] = (args, kwargs) asignal.connect(handler) res = asignal.notify('bar', k=1) assert res == 'foo' assert c['called'] == 1 assert c['wrap_args'] == (('bar', ), {'k': 1}) assert c['handler_called'] == 1 assert c['handler_args'] == (('foo', ), {'k': 2}) c = dict(called=0, wrap_args=None, handler_called=0, handler_args=None, handler2_called=0, handler2_args=None) class A(metaclass=SignalAndHandlerInitMeta): click = Signal() @click.on_notify def click(self, subscribers, notify, *args, **kwargs): c['called'] += 1 c['wrap_args'] = (args, kwargs) assert len(subscribers) == 2 assert isinstance(self, A) notify('foo', k=2) return 'foo' @handler('click') def handler(self, *args, **kwargs): c['handler_called'] += 1 c['handler_args'] = (args, kwargs) a = A() def handler2(*args, **kwargs): c['handler2_called'] += 1 c['handler2_args'] = (args, kwargs) a.click.connect(handler2) res = a.click.notify('bar', k=1) assert res == 'foo' assert c['called'] == 1 assert c['wrap_args'] == (('bar', ), {'k': 1}) assert c['handler_called'] == 1 assert c['handler_args'] == (('foo', ), {'k': 2}) assert c['handler2_called'] == 1 assert c['handler2_args'] == (('foo', ), {'k': 2})
class ApplicationService(BaseService): """ A service that publishes an API for the creation of long-running contexes. Each time a long-running session starts, it executes the supplied factory with a tailored context. :param factory: a class or method to execute per-session :param node_path: Path of the service :term:`WAMP` path :type node_path: an instance of :class:`~metapensiero.raccoon.node.path.Path` :param context: An optional parent context :type context: An instance of :class:`~metapensiero.raccoon.node.context.WAMPContext` :results: a dictionary containing initial session info. """ SESSION_CLASS = SessionRoot """The type of the session to instantiate when `~.create_session` is called.""" location_name = system.name on_session_stopped = Signal() """The `~metapensiero.signal.atom.Signal` that is fired each time a session is reached a ``stopped`` state.""" def __init__(self, factory, node_path, node_context=None): super().__init__(node_path, node_context=node_context) self._next_session_num = 1 self._sessions = {} self._factory = factory def _next_session_id(self): res = self._next_session_num self._next_session_num += 1 return str(res) async def _create_session(self, session_id, from_location, client_details=None): session_ctx = self.node_context.new(service=self, session_id=session_id) session_ctx.session_id = session_id session_path = Path(self.node_path + session_id) session_path.base = session_path sess = self.SESSION_CLASS(locations=[from_location, self.location_name], local_location_name=self.location_name, local_member_factory=self._factory, client_details=client_details) await sess.node_bind(session_path, session_ctx, self) return sess @call async def start_session(self, from_location, session_id=None, details=None): """The entrypoint for this service. This should be called by the client to start a session that will establish and orchestrate further communication. """ if (session_id and session_id not in self._sessions) or \ not session_id: session_id = self._next_session_id() session_root = sr = await self._create_session(session_id, from_location, details) self._sessions[session_id] = session_root else: session_root = sr = self._sessions[session_id] return { 'location': from_location, 'base': str(sr.node_path), 'id': sr.node_context.session_id } @handler('on_session_stopped') def _remove_session(self, session): del self._sessions[session.node_name]
class BaseService(ContextNode): """A simple class tailored to the needs of setting up some tree of endpoints immediately available as soon as the wamp connection is established. """ on_start = Signal() """Signal emitted when the service starts. This happens when the connection has successfully joined the realm and the ``session.is_attached()`` is ``True`` rather than a simple TCP connection. """ @on_start.on_connect async def on_start(self, handler, subscribers, connect): """Call handler immediately if the service is started already""" if self.started: await connect.notify(handler, self.node_path, self.node_context) connect(handler) def __init__(self, node_path, node_context=None): """ :param node_path: Path of the service :term:`WAMP` path :type node_path: an instance of :class:`~metapensiero.raccoon.node.path.Path` :param node_context: An optional parent context :type node_context: An instance of :class:`~metapensiero.raccoon.node.context.WAMPContext` """ super().__init__() self._connection = None self._tmp_path = node_path if node_context: context = node_context.new() else: context = WAMPNodeContext() context.chain(system.node_context) self._tmp_context = context self.started = False @property def connection(self): """ :param connection: The associated connection :type connection: A :class:`~.wamp.connection.Connection` """ return self._connection async def set_connection(self, connection): """Set the wamp connection.""" self._connection = connection self._tmp_context.loop = connection.loop await connection.on_connect.connect(self._on_connection_connected) async def _on_connection_connected(self, session, session_details, **kwargs): path, context = self._tmp_path, self._tmp_context del self._tmp_path, self._tmp_context context.wamp_session = session context.wamp_details = session_details await self.node_bind(path, context) await self.start_service(path, context) self.started = True await self.on_start.notify(local_path=self.node_path, local_context=self.node_context) async def start_service(self, path, context): """Start this service. Execute the :py:meth:`~.node.Node.bind` on the passed in arguments and register the instance on the :term:`WAMP` network Called by the default ``on_start`` signal handler. :param path: Dotted string or sequence of the :term:`WAMP` path. :param context: The execution context. :type context: A :py:class:`.context.GlobalContext` """ logger.debug("Service at %r started", self.node_path)
class ServiceNode(metaclass=SignalAndHandlerInitMeta): """Base node for all the service stuff.""" node_location = None """The location record.""" on_node_primary_signal = Signal() """Signal used to receive *infrastructure* messages. The messages that implement the pairing protocol are of type 'pairing_request', 'peer_ready' and 'peer_start'. """ on_node_primary_signal.name = '.' def _node_children(self): return {k: v for k, v in self.__dict__.items() if k != 'node_parent' and isinstance(v, node.Node)} def _node_remove_child(self, child): name = super()._node_remove_child(child) self.__delitem__(name) return name async def _node_unbind(self): from . import system await super()._node_unbind() system.unregister_node(self) async def _node_bind(self, path, context=None, parent=None): from . import system await super()._node_bind(path, context, parent) self.node_location = system.register_node(self) async def node_add(self, name, value): await super().node_add(name, value) self.__setitem__(name, value) def node_changed(self): self.node_location.changed() def node_depend(self): self.node_location.depend() def node_info(self): from . import system return { 'uri': str(self.node_path), 'type': self.__class__.__name__, 'system': system.node_info() } async def node_remove(self, name): self.__delitem__(name) await super().node_remove(name) def node_resolve(self, uri): """Resolve a path to a Node. :param str uri: the uri to resolve. :returns: A `metapensiero.raccoon.node.node.Node` or ``None`` if the operation fails. """ if isinstance(uri, node.Node): return uri from . import system if isinstance(uri, node.Path): uri = str(uri) else: uri = str(self.node_path.resolve(uri, self.node_context)) return system.resolve(uri)
class A(metaclass=SignalAndHandlerInitMeta): click = Signal()