Esempio n. 1
0
    async def request_resource(self, type: Union[str, type], alias: str = 'default', *,
                               timeout: Union[int, None] = None):
        """
        Request a resource matching the given type and alias.

        If no such resource was found, this method will wait ``timeout`` seconds for it to become
        available. The timeout does not apply to resolving awaitables created by lazy resource
        creators.

        :param type: type of the requested resource
        :param alias: alias of the requested resource
        :param timeout: the timeout (in seconds; omit to use the context's default timeout)
        :return: the value contained by the requested resource (**NOT** a :class:`Resource`
            instance)
        :raises asphalt.core.context.ResourceNotFound: if the requested resource does not become
            available in the allotted time

        """
        assert check_argument_types()
        if not type:
            raise ValueError('type must be a type or a nonempty string')
        if not alias:
            raise ValueError('alias must be a nonempty string')

        resource_type = qualified_name(type) if not isinstance(type, str) else type
        timeout = timeout if timeout is not None else self.default_timeout
        assert timeout >= 0, 'timeout must be a positive integer'

        # Build a context chain from this context and its parents
        context_chain = [self]
        while context_chain[-1]._parent:
            context_chain.append(context_chain[-1]._parent)

        # First try to look up the resource in the context chain
        for ctx in context_chain:
            resource = ctx._resources.get(resource_type, {}).get(alias)
            if resource is not None:
                value = resource.get_value(self)
                return await value if isawaitable(value) else value

        # Listen to resource publish events in the whole chain and wait for the right kind of
        # resource to be published
        def resource_listener(event: ResourceEvent):
            if event.resource.alias == alias and resource_type in event.resource.types:
                future.set_result(event.resource)

        future = Future()
        for ctx in context_chain:
            ctx.resource_published.connect(resource_listener)

        try:
            resource = await wait_for(future, timeout)
        except TimeoutError:
            raise ResourceNotFound(resource_type, alias) from None
        else:
            value = resource.get_value(self)
            return await value if isawaitable(value) else value
        finally:
            for ctx in context_chain:
                ctx.resource_published.disconnect(resource_listener)
Esempio n. 2
0
    def get_resources(self,
                      type: Union[str, type] = None,
                      *,
                      include_parents: bool = True) -> Sequence[Resource]:
        """
        Return the currently published resources specific to one type or all types.

        :param type: type of the resources to return, or ``None`` to return all resources
        :param include_parents: include the resources from parent contexts

        """
        resources = set(
            chain(*(value.values() for value in self._resources.values())))
        if include_parents and self._parent:
            resources = resources.union(
                self._parent.get_resources(type, include_parents=True))

        if type is not None:
            resource_type = qualified_name(type) if not isinstance(
                type, str) else type
            resources = (resource for resource in resources
                         if resource_type in resource.types)

        return sorted(resources,
                      key=lambda resource: (resource.types, resource.alias))
Esempio n. 3
0
    def _publish_resource(self, value, alias: str, context_attr: str,
                          types: Iterable[Union[str, type]],
                          creator: Optional[Callable[['Context'], Any]]):
        assert isinstance(alias,
                          str) and alias, 'alias must be a nonempty string'
        assert re.match(r'^\w+$', alias),\
            'alias can only contain alphanumeric characters and underscores'
        assert context_attr is None or isinstance(context_attr, str),\
            'context_attr must be a nonempty string or None'

        if isinstance(types, (str, type)):
            types = (types, )

        # Check for alias conflicts with existing resources
        types = tuple(t if isinstance(t, str) else qualified_name(t)
                      for t in types)
        for typename in types:
            if alias in self._resources[typename]:
                raise ResourceConflict(
                    'this context has an existing resource of type {} using the alias "{}"'
                    .format(typename, alias))

        # Check for context attribute conflicts
        if context_attr:
            # Check that there is no existing attribute by that name
            if context_attr in dir(self):
                raise ResourceConflict(
                    'this context already has an attribute "{}"'.format(
                        context_attr))

            # Check that there is no existing lazy resource using the
            # same context attribute
            if context_attr in self._lazy_resources:
                raise ResourceConflict(
                    'this context has an existing lazy resource using the attribute "{}"'
                    .format(context_attr))

        # Register the resource
        resource = Resource(value, types, alias, context_attr, creator)
        for typename in types:
            self._resources[typename][resource.alias] = resource

        if creator is not None and context_attr is not None:
            self._lazy_resources[context_attr] = resource

        # Add the resource as an attribute of this context if context_attr is defined
        if creator is None and resource.context_attr:
            setattr(self, context_attr, value)

        self.resource_published.dispatch(resource)
        return resource
Esempio n. 4
0
    def _publish_resource(self, value, alias: str, context_attr: str,
                          types: Iterable[Union[str, type]],
                          creator: Optional[Callable[['Context'], Any]]):
        assert isinstance(alias, str) and alias, 'alias must be a nonempty string'
        assert re.match(r'^\w+$', alias),\
            'alias can only contain alphanumeric characters and underscores'
        assert context_attr is None or isinstance(context_attr, str),\
            'context_attr must be a nonempty string or None'

        if isinstance(types, (str, type)):
            types = (types,)

        # Check for alias conflicts with existing resources
        types = tuple(t if isinstance(t, str) else qualified_name(t) for t in types)
        for typename in types:
            if alias in self._resources[typename]:
                raise ResourceConflict(
                    'this context has an existing resource of type {} using the alias "{}"'
                    .format(typename, alias))

        # Check for context attribute conflicts
        if context_attr:
            # Check that there is no existing attribute by that name
            if context_attr in dir(self):
                raise ResourceConflict(
                    'this context already has an attribute "{}"'.format(context_attr))

            # Check that there is no existing lazy resource using the
            # same context attribute
            if context_attr in self._lazy_resources:
                raise ResourceConflict(
                    'this context has an existing lazy resource using the attribute "{}"'
                    .format(context_attr))

        # Register the resource
        resource = Resource(value, types, alias, context_attr, creator)
        for typename in types:
            self._resources[typename][resource.alias] = resource

        if creator is not None and context_attr is not None:
            self._lazy_resources[context_attr] = resource

        # Add the resource as an attribute of this context if context_attr is defined
        if creator is None and resource.context_attr:
            setattr(self, context_attr, value)

        self.resource_published.dispatch(resource)
        return resource
Esempio n. 5
0
    def get_resources(self, type: Union[str, type] = None, *,
                      include_parents: bool = True) -> Sequence[Resource]:
        """
        Return the currently published resources specific to one type or all types.

        :param type: type of the resources to return, or ``None`` to return all resources
        :param include_parents: include the resources from parent contexts

        """
        resources = set(chain(*(value.values() for value in self._resources.values())))
        if include_parents and self._parent:
            resources = resources.union(self._parent.get_resources(type, include_parents=True))

        if type is not None:
            resource_type = qualified_name(type) if not isinstance(type, str) else type
            resources = (resource for resource in resources if resource_type in resource.types)

        return sorted(resources, key=lambda resource: (resource.types, resource.alias))
Esempio n. 6
0
def test_qualified_name(inputval, expected):
    assert qualified_name(inputval) == expected
Esempio n. 7
0
def test_qualified_name(inputval, expected):
    assert qualified_name(inputval) == expected
Esempio n. 8
0
 def __str__(self):
     return ('types={0.types!r}, alias={0.alias!r}, value={0.value!r}, '
             'context_attr={0.context_attr!r}, creator={1}'
             .format(self, qualified_name(self.creator) if self.creator else None))
Esempio n. 9
0
 def __str__(self):
     return ('types={0.types!r}, alias={0.alias!r}, value={0.value!r}, '
             'context_attr={0.context_attr!r}, creator={1}'.format(
                 self,
                 qualified_name(self.creator) if self.creator else None))
Esempio n. 10
0
    async def request_resource(self,
                               type: Union[str, type],
                               alias: str = 'default',
                               *,
                               timeout: Union[int, None] = None):
        """
        Request a resource matching the given type and alias.

        If no such resource was found, this method will wait ``timeout`` seconds for it to become
        available. The timeout does not apply to resolving awaitables created by lazy resource
        creators.

        :param type: type of the requested resource
        :param alias: alias of the requested resource
        :param timeout: the timeout (in seconds; omit to use the context's default timeout)
        :return: the value contained by the requested resource (**NOT** a :class:`Resource`
            instance)
        :raises asphalt.core.context.ResourceNotFound: if the requested resource does not become
            available in the allotted time

        """
        assert check_argument_types()
        if not type:
            raise ValueError('type must be a type or a nonempty string')
        if not alias:
            raise ValueError('alias must be a nonempty string')

        resource_type = qualified_name(type) if not isinstance(type,
                                                               str) else type
        timeout = timeout if timeout is not None else self.default_timeout
        assert timeout >= 0, 'timeout must be a positive integer'

        # Build a context chain from this context and its parents
        context_chain = [self]
        while context_chain[-1]._parent:
            context_chain.append(context_chain[-1]._parent)

        # First try to look up the resource in the context chain
        for ctx in context_chain:
            resource = ctx._resources.get(resource_type, {}).get(alias)
            if resource is not None:
                value = resource.get_value(self)
                return await value if isawaitable(value) else value

        # Listen to resource publish events in the whole chain and wait for the right kind of
        # resource to be published
        def resource_listener(event: ResourceEvent):
            if event.resource.alias == alias and resource_type in event.resource.types:
                future.set_result(event.resource)

        future = Future()
        for ctx in context_chain:
            ctx.resource_published.connect(resource_listener)

        try:
            resource = await wait_for(future, timeout)
        except TimeoutError:
            raise ResourceNotFound(resource_type, alias) from None
        else:
            value = resource.get_value(self)
            return await value if isawaitable(value) else value
        finally:
            for ctx in context_chain:
                ctx.resource_published.disconnect(resource_listener)
Esempio n. 11
0
def run_application(component: Component, *, event_loop_policy: str = None,
                    max_threads: int = None, logging: Union[Dict[str, Any], int, None] = INFO):
    """
    Configure logging and start the given root component in the default asyncio event loop.

    Assuming the root component was started successfully, the event loop will continue running
    until the process is terminated.

    Initializes the logging system first based on the value of ``logging``:
      * If the value is a dictionary, it is passed to :func:`logging.config.dictConfig` as
        argument.
      * If the value is an integer, it is passed to :func:`logging.basicConfig` as the logging
        level.
      * If the value is ``None``, logging setup is skipped entirely.

    By default, the logging system is initialized using :func:`~logging.basicConfig` using the
    ``INFO`` logging level.

    The default executor in the event loop is replaced with a new
    :class:`~concurrent.futures.ThreadPoolExecutor` where the maximum number of threads is set to
    the value of ``max_threads`` or, if omitted, the return value of :func:`os.cpu_count()`.

    :param component: the root component
    :param event_loop_policy: entry point name (from the ``asphalt.core.event_loop_policies``
        namespace) of an alternate event loop policy (or a module:varname reference to one)
    :param max_threads: the maximum number of worker threads in the default thread pool executor
        (the default value depends on the event loop implementation)
    :param logging: a logging configuration dictionary, :ref:`logging level <python:levels>` or
        ``None``

    """
    assert check_argument_types()

    # Configure the logging system
    if isinstance(logging, dict):
        dictConfig(logging)
    elif isinstance(logging, int):
        basicConfig(level=logging)

    # Switch to an alternate event loop policy if one was provided
    logger = getLogger(__name__)
    if event_loop_policy:
        create_policy = policies.resolve(event_loop_policy)
        policy = create_policy()
        asyncio.set_event_loop_policy(policy)
        logger.info('Switched event loop policy to %s', qualified_name(policy))

    # Assign a new default executor with the given max worker thread limit if one was provided
    event_loop = asyncio.get_event_loop()
    if max_threads is not None:
        event_loop.set_default_executor(ThreadPoolExecutor(max_threads))
        logger.info('Installed a new thread pool executor with max_workers=%d', max_threads)

    logger.info('Starting application')
    context = Context()
    exception = None
    try:
        try:
            event_loop.run_until_complete(component.start(context))
        except Exception as e:
            exception = e
            logger.exception('Error during application startup')
        else:
            # Enable garbage collection of the component tree
            del component

            # Finally, run the event loop until the process is terminated or Ctrl+C is pressed
            event_loop.run_forever()
    except (KeyboardInterrupt, SystemExit):
        pass
    finally:
        # Cancel all running tasks
        for task in asyncio.Task.all_tasks(event_loop):
            task.cancel()

        # Run all the finish callbacks
        future = context.finished.dispatch(exception, return_future=True)
        event_loop.run_until_complete(future)

    event_loop.close()
    logger.info('Application stopped')

    if exception is not None:
        sys.exit(1)
Esempio n. 12
0
    def dispatch_event(self, event: Event, *, return_future: bool = False) -> Optional[Future]:
        """
        Dispatch the given event to all listeners.

        Creates a new task in which all listener callbacks are called with the given event as
        the only argument. Coroutine callbacks are converted to their own respective tasks and
        waited for concurrently.

        :param event: the event object to dispatch
        :param return_future:
            If ``True``, return a :class:`~asyncio.Future` that completes when all the listener
            callbacks have been processed. If any one of them raised an exception, the future will
            have an :exc:`~asphalt.core.event.EventDispatchError` exception set in it which
            contains all of the exceptions raised in the callbacks.

            If set to ``False``, then ``None`` will be returned, and any exceptions raised in
            listener callbacks will be logged instead.
        :returns: a future or ``None``, depending on the ``return_future`` argument

        """
        async def do_dispatch():
            futures, exceptions = [], []
            for callback in listeners:
                try:
                    retval = callback(event)
                except Exception as e:
                    exceptions.append((callback, e))
                    if not return_future:
                        logger.exception('uncaught exception in event listener')
                else:
                    if isawaitable(retval):
                        if iscoroutine(retval):
                            retval = loop.create_task(retval)

                        futures.append((callback, retval))

            # For any callbacks that returned awaitables, wait for their completion and collect any
            # exceptions they raise
            for callback, awaitable in futures:
                try:
                    await awaitable
                except Exception as e:
                    exceptions.append((callback, e))
                    if not return_future:
                        logger.exception('uncaught exception in event listener')

            if exceptions and return_future:
                raise EventDispatchError(event, exceptions)

        assert isinstance(event, self.event_class), \
            'event must be of type {}'.format(qualified_name(self.event_class))
        if self.listeners:
            loop = get_event_loop()
            listeners = self.listeners.copy()
            future = loop.create_task(do_dispatch())
            return future if return_future else None
        elif return_future:
            # The event has no listeners, so skip the task creation and return an empty Future
            future = Future()
            future.set_result(None)
            return future
        else:
            return None