Services

A service is an object that is registered to the service registry of the framework, associated to a set of specifications it implements and to properties.

The bundle that registers the service must keep track of the ServiceRegistration object returned by the framework. It allows to update the service properties and to unregister the service. This object shall not be accessible by other bundles/services, as it gives access and control over the life cycle of the service it represents. Finally, all services must be unregistered when their bundle is stopped.

A consumer can look for a service that matches a specification and a set of properties, using its BundleContext. The framework will return a ServiceReference object, which provides a read-only access to the description of its associated service: properties, registering bundle, bundles using it, etc.

Specifications

A specification is identified by a name, a string shared by the provider and the consumers of a service. That name is what the service registry stores, what an LDAP filter matches and what is sent on the wire when a service is exported with Remote Services.

The name can be given directly, as in iPOPO v1, but the recommended way in iPOPO v3 is to declare a Protocol describing the service and to name it with the Specification decorator. The provider and the consumers then share a type, which lets the development tools check the methods and their signatures, instead of sharing a bare string:

from typing import Protocol

from pelix.constants import Specification


# Set the name of the specification
@Specification("spell_dictionary_service")
class SpellDictionary(Protocol):
    """
    Definition of the spell dictionary service
    """

    def check_word(self, word: str) -> bool:
        """
        Determines if the given word is contained in the dictionary.

        @param word the word to be checked.
        @return True if the word is in the dictionary, False otherwise.
        """
        ...

The decorator stores the names it is given in the __SPECIFICATION__ attribute of the class, and the framework reads that attribute when a class is used as a specification. The specification module must be importable by the provider and the consumer bundles, but it doesn’t need to be installed as a bundle itself.

The decorator accepts several names at once, given as arguments or as lists, and removes the duplicates:

@Specification("sample.hello", ["sample.greeter", "sample.hello"])
class Hello(Protocol):
    ...
# Hello.__SPECIFICATION__ == ["sample.hello", "sample.greeter"]

A specification inheriting from another one adds its names to those of its parent, unless ignore_parent=True is given:

@Specification("sample.hello.fr")
class HelloFR(Hello, Protocol):
    ...
# HelloFR.__SPECIFICATION__ == ["sample.hello.fr", "sample.hello", "sample.greeter"]

@Specification("sample.hello.fr", ignore_parent=True)
class OnlyFR(Hello, Protocol):
    ...
# OnlyFR.__SPECIFICATION__ == ["sample.hello.fr"]

Called without any argument, the decorator uses the name of the class it decorates, which is also what the framework falls back to when a class is used as a specification without having been decorated at all.

Warning

Give an explicit name to your specifications.

The fallback on the name of the class means that two unrelated bundles, each defining a protocol named Store or Config, register the very same specification: a consumer of one gets bound to the provider of the other. An explicit, namespaced name such as acme.storage.Store avoids the collision, and keeps the name stable if the class is later renamed or moved.

Properties

When registered and while it is available, the properties of a service can be set and updated by its provider.

Although, some properties are reserved for the framework, each service has at least the following properties:

Name

Type

Description

objectClass

list of str

List of the specifications implemented by this service

service.id

int

Identifier of the service. Unique in a framework instance

service.scope

str

OSGi service scope: singleton, prototype or factory

The framework also uses the following property to sort the result of a service look up:

Name

Type

Description

service.ranking

int

The rank/priority of the service. The lower the rank, the more priority

Service Factory

A service factory is a pseudo-service with a specific flag, which can create individual instances of service objects for different bundles. Sometimes a service needs to be differently configured depending on which bundle uses the service. For example, the log service needs to be able to print the logging bundle’s id, otherwise the log would be hard to read.

A service factory is registered in exactly the same way as a normal service, using register_service(), with the factory argument set to True. The only difference is an indirection step before the actual service object is handed out.

The client using the service need not, and should not, care if a service is generated by a factory or by a plain object.

A simple service factory example

class ServiceInstance:
    def __init__(self, value):
        self.__value = value

    def cleanup(self):
        self.__value = None

    def get_value(self):
        return self.__value

class ServiceFactory:
    def __init__(self):
        # Bundle -> Instance
        self._instances = {}

    def get_service(self, bundle, registration):
        """
        Called each time a new bundle requires the service
        """
        instance = ServiceInstance(bundle.get_bundle_id())
        self._instances[bundle] = instance
        return instance

    def unget_service(self, bundle, registration):
        """
        Called when a bundle has released all its references
        to the service
        """
        # Release connections, ...
        self._instances.pop(bundle).cleanup()

bundle_context.register_service(
    "sample.factory", ServiceFactory(), {}, factory=True)

Note

The framework will cache generated service objects. Thus, at most one service can be generated per client bundle.

Prototype Service Factory

A prototype service factory is a pseudo-service with a specific flag, which can create multiple instances of service objects for different bundles.

Each time a bundle requires the service, the prototype service factory is called and can return a different instance. When called, the framework gives the factory the Bundle object requesting the service and the ServiceRegistration of the requested service. This allows a single factory to be registered for multiple services.

Note that there is no Prototype Service Factory implemented in the core Pelix/iPOPO Framework.

A Prototype Service Factory is registered in exactly the same way as a normal service, using register_service(), with the prototype argument set to True.

A simple prototype service factory example:

class ServiceInstance:
    def __init__(self, value):
        self.__value = value

    def cleanup(self):
        self.__value = None

    def get_value(self):
        return self.__value

class PrototypeServiceFactory:
    def __init__(self):
        # Bundle -> [instances]
        self._instances = {}

    def get_service(self, bundle, registration):
        """
        Called each time ``get_service()`` is called
        """
        bnd_instances = self._instances.setdefault(bundle, [])
        instance = ServiceInstance(
            [bundle.get_bundle_id(), len(bnd_instances)])
        bnd_instances.append(instance)
        return instance

    def unget_service_instance(self, bundle, registration, service):
        """
        Called when a bundle releases an instance of the service
        """
        bnd_instances[bundle].remove(service)
        service.cleanup()

    def unget_service(self, bundle, registration):
        """
        Called when a bundle has released all its references
        to the service
        """
        # Release global resources...

        # When this method is called, all instances will have been cleaned
        # up individually in ``unget_service_instance``
        if len(self._instances.pop(bundle)) != 0:
           raise ValueError("Should never happen")

bundle_context.register_service(
    "sample.proto", PrototypeServiceFactory(), {}, factory=True)

Note

A Prototype Service Factory is considered as a Service Factory, hence both is_factory() and is_prototype() will return True for this kind of service

API

The service provider has access to the ServiceRegistration object created by the framework when register_service() is called.

class pelix.framework.ServiceRegistration(framework: Framework, reference: ServiceReference[T], properties: dict[str, Any], update_callback: Callable[[ServiceReference[T]], None])

Represents a service registration object

Parameters:
  • framework – The host framework

  • reference – A service reference

  • properties – A reference to the ServiceReference properties dictionary object

  • update_callback – Method to call when the sort key is modified

get_reference() ServiceReference[T]

Returns the reference associated to this registration

Returns:

A ServiceReference object

set_properties(properties: dict[str, Any]) None

Updates the service properties

Parameters:

properties – The new properties

Raises:

TypeError – The argument is not a dictionary

unregister() None

Unregisters the service

Consumers can access the service using its ServiceReference object, unique and constant for each service. This object can be retrieved using the BundleContext and its get_service_reference* methods. A consumer can check the properties of a service through this object, before consuming it.

class pelix.framework.ServiceReference(bundle: Bundle, properties: dict[str, Any])

Represents a reference to a service

Parameters:
  • bundle – The bundle registering the service

  • properties – The service properties

Raises:

BundleException – The properties doesn’t contain mandatory entries

get_bundle() Bundle

Returns the bundle that registered this service

Returns:

the bundle that registered this service

get_properties() dict[str, Any]

Returns a copy of the service properties

Returns:

A copy of the service properties

get_property(name: str) Any

Retrieves the property value for the given name

Returns:

The property value, None if not found

get_property_keys() tuple[str, ...]

Returns an array of the keys in the properties of the service

Returns:

An array of property keys.

get_using_bundles() list[Bundle]

Returns the list of bundles that use this service

Returns:

A list of Bundle objects

is_factory() bool

Returns True if this reference points to a service factory

Returns:

True if the service provides from a factory

is_prototype() bool

Returns True if this reference points to a prototype service factory

Returns:

True if the service provides from a prototype factory

Finally, here are the methods of the BundleContext class that can be used to handle services:

class pelix.framework.BundleContext(framework: Framework, bundle: Bundle)

The bundle context is the link between a bundle and the framework. It is unique for a bundle and is created by the framework once the bundle is installed.

Parameters:
  • framework – Hosting framework

  • bundle – The associated bundle

get_all_service_references(clazz: None | str | type[T] = None, ldap_filter: None | str | LDAPFilter | LDAPCriteria = None) list[ServiceReference[T]] | None

Returns an array of ServiceReference objects. The returned array of ServiceReference objects contains services that were registered under the specified class and match the specified filter expression.

Parameters:
  • clazz – Class implemented by the service

  • ldap_filter – Service filter

Returns:

The sorted list of all matching service references, or None

get_service(reference: ServiceReference[T]) T

Returns the service described with the given reference

Parameters:

reference – A ServiceReference object

Returns:

The service object itself

get_service_reference(clazz: None | str | type[T], ldap_filter: None | str | LDAPFilter | LDAPCriteria = None) ServiceReference[T] | None

Returns a ServiceReference object for a service that implements and was registered under the specified class

Parameters:
  • clazz – The class name with which the service was registered.

  • ldap_filter – A filter on service properties

Returns:

A service reference, None if not found

get_service_references(clazz: None | str | type[T], ldap_filter: None | str | LDAPFilter | LDAPCriteria = None) list[ServiceReference[T]] | None

Returns the service references for services that were registered under the specified class by this bundle and matching the given filter

Parameters:
  • clazz – The class name with which the service was registered.

  • ldap_filter – A filter on service properties

Returns:

The list of references to the services registered by the calling bundle and matching the filters.

register_service(clazz: str | type[T] | Iterable[str | type[Any]], service: T, properties: dict[str, Any] | None, send_event: bool = True, factory: bool = False, prototype: bool = False) ServiceRegistration[T]

Registers a service

Parameters:
  • clazz – Class or Classes (list) implemented by this service

  • service – The service instance

  • properties – The services properties (dictionary)

  • send_event – If not, doesn’t trigger a service registered event

  • factory – If True, the given service is a service factory

  • prototype – If True, the given service is a prototype service factory (the factory argument is considered True)

Returns:

A ServiceRegistration object

Raises:

BundleException – An error occurred while registering the service

unget_service(reference: ServiceReference[Any]) bool

Disables a reference to the service

Returns:

True if the bundle was using this reference, else False

Listening to service events

The bundle context can be used to register to service events, using the following methods:

class pelix.framework.BundleContext(framework: Framework, bundle: Bundle)

The bundle context is the link between a bundle and the framework. It is unique for a bundle and is created by the framework once the bundle is installed.

Parameters:
  • framework – Hosting framework

  • bundle – The associated bundle

add_service_listener(listener: ServiceListener, ldap_filter: None | LDAPCriteria | LDAPFilter | str = None, specification: str | type[Any] | Iterable[str | type[Any]] | None = None) bool

Registers a service listener

The service listener must have a method with the following prototype:

def service_changed(self, event):
    '''
    Called by Pelix when some service properties changes

    event: A ServiceEvent object
    '''
    # ...
Parameters:
  • listener – The listener to register

  • ldap_filter – Filter that must match the service properties (optional, None to accept all services)

  • specification – The specification that must provide the service (optional, None to accept all services)

Returns:

True if the listener has been successfully registered

remove_service_listener(listener: ServiceListener) bool

Unregisters a service listener

Parameters:

listener – The service listener

Returns:

True if the listener has been unregistered

A bundle listener must implement the following interface:

class pelix.internals.registry.ServiceListener(*args, **kwargs)

Protocol that must be implemented by a service listener

service_changed(event: ServiceEvent[Any]) None

Notified when a service event occurred

Parameters:

event – Service event