iPOPO in 10 minutes

Authors

Shadi Abras, Thomas Calmant

This tutorial presents how to use the iPOPO framework and its associated service-oriented component model. The concepts of the service-oriented component model are introduced, followed by a simple example that demonstrates the features of iPOPO. This framework uses decorators to describe components.

Note

This tutorial has been updated to use types instead of named specifications.

Introduction

iPOPO aims to simplify service-oriented programming on OSGi frameworks in Python language; the name iPOPO is an abbreviation for injected POPO, where POPO would stand for Plain Old Python Object. The name is in fact a simple modification of the Apache iPOJO project, which stands for injected Plain Old Java Object

iPOPO provides a new way to develop OSGi/iPOJO-like service components in Python, simplifying service component implementation by transparently managing the dynamics of the environment as well as other non-functional requirements. The iPOPO framework allows developers to more clearly separate functional code (i.e. POPOs) from the non-functional code (i.e. dependency management, service provision, configuration, etc.). At run time, iPOPO combines the functional and non-functional aspects. To achieve this, iPOPO provides a simple and extensible service component model based on POPOs.

Since iPOPO v3, we recommend using types as much as possible to avoid issues when developping large softwares with the framework.

Basic concepts

iPOPO is separated into two parts:

  • Pelix, the underlying bundle and service registry

  • iPOPO, the service-oriented component framework

It also defines three major concepts:

  • A bundle is a single Python module, i.e. a .py file, that is loaded using the Pelix API.

  • A service is a Python object that is registered to the service registry using the Pelix API, associated to a set of specifications and to a dictionary of properties.

  • A component is an instance of component factory, i.e. a class manipulated by iPOPO decorators. Those decorators injects information into the class that are later used by iPOPO to manage the components. Components are defined inside bundles.

Simple example

In this tutorial we will present how to:

  • Publish a service

  • Require a service

  • Use lifecycle callbacks to activate and deactivate components

Presentation of the Spell application

To illustrate some of iPOPO features, we will implement a very simple application. Three bundles compose this application:

  • A bundle that defines a component implementing a dictionary service (an English and a French dictionaries).

  • One with a component requiring the dictionary service and providing a spell checker service.

  • One that defines a component requiring the spell checker and providing a user line interface.

Service hierarchy

The spell dictionary components provide the spell_dictionary_service specification. The spell checker provides a spell_checker_service specification.

Preparing the tutorial

The example contains several bundles:

  • spell_checker_api.py defines the Python protocols that describe the different services in use.

  • spell_dictionary_EN.py defines a component that implements the Dictionary service, containing some English words.

  • spell_dictionary_FR.py defines a component that implements the Dictionary service, containing some French words.

  • spell_checker.py contains an implementation of a Spell Checker. The spell checker requires a dictionary service and checks if an input passage is correct, according to the words contained in the wished dictionary.

  • spell_client.py provides commands for the Pelix shell service. This component uses a spell checker service. The user can interact with the spell checker with this command line interface.

Finally, a main_pelix_launcher.py script starts the Pelix framework. It is not considered as a bundle as it is not loaded by the framework, but it can control the latter.

Definining specifications

Note

This section is new in iPOPO v3

Instead of relying exclusively on specification names to link components together, it is now recommended to declare protocols or classes and to use those to declare injected fields.

For example, the spell_checker_api bundle contains only the definition of the specifications we will use in this project. It is recommended to use a specific file to define specifications and constants in order to share it between the provider and consumer bundles.

 1#!/usr/bin/python
 2# -- Content-Encoding: UTF-8 --
 3"""
 4Module defining the types used in the Spell Checker
 5"""
 6
 7from typing import List, Protocol
 8
 9from pelix.constants import Specification
10
11
12# Set the name of the specification
13@Specification("spell_dictionary_service")
14class SpellDictionary(Protocol):
15    """
16    Definition of the spell dictionary service
17    """
18
19    def check_word(self, word: str) -> bool:
20        """
21        Determines if the given word is contained in the dictionary.
22
23        @param word the word to be checked.
24        @return True if the word is in the dictionary, False otherwise.
25        """
26        ...
27
28
29@Specification("spell_checker_service")
30class SpellChecker(Protocol):
31    """
32    Definition of the spell checker service
33    """
34
35    def check(self, passage: str, language: str = "EN") -> List[str] | None:
36        """
37        Checks the given passage for misspelled words.
38
39        :param passage: the passage to spell check.
40        :param language: language of the spell dictionary to use
41        :return: An array of misspelled words or null if no words are misspelled
42        :raise KeyError: No dictionary for this language
43        """
44        ...
  • The @Specification decorator will store in the protocol/class the name of the specification. This is highly recommended as it will be the name used in the Pelix service registry and when communicating with remote framework if you want to use remote services.

  • We recommend using the Python Protocol as parent of each specification class as it is meant to declare a type.

Once the specifications are defined, we can continue by implementing them with different components.

Note

Depending on your own code style, you might to easer provide explicit types on specification methods methods or let them be inherited from the protocol.

Also note that the specification module must be importable by the bundles, but doesn’t need to be installed as a bundle itself.

The English dictionary bundle: Providing a service

The spell_dictionary_EN bundle is a simple implementation of the Dictionary service. It contains few English words.

 1#!/usr/bin/python
 2# -- Content-Encoding: UTF-8 --
 3"""
 4This bundle provides a component that is a simple implementation of the
 5Dictionary service. It contains some English words.
 6"""
 7
 8from typing import Set
 9
10from spell_checker_api import SpellDictionary
11
12from pelix.framework import BundleContext
13from pelix.ipopo.decorators import ComponentFactory, Instantiate, Invalidate, Property, Provides, Validate
14
15
16# Name the iPOPO component factory
17@ComponentFactory("spell_dictionary_en_factory")
18# This component provides a dictionary service
19@Provides(SpellDictionary)
20# It is the English dictionary
21@Property("_language", "language", "EN")
22# Automatically instantiate a component when this factory is loaded
23@Instantiate("spell_dictionary_en_instance")
24class EnglishSpellDictionary(SpellDictionary):
25    """
26    Implementation of a spell dictionary, for English language.
27    """
28
29    def __init__(self) -> None:
30        """
31        Declares members, to respect PEP-8.
32        """
33        self.dictionary: Set[str] = set()
34
35    @Validate
36    def validate(self, context: BundleContext) -> None:
37        """
38        The component is validated. This method is called right before the
39        provided service is registered to the framework.
40        """
41        # All setup should be done here
42        self.dictionary = {"hello", "world", "welcome", "to", "the", "ipopo", "tutorial"}
43        print("An English dictionary has been added")
44
45    @Invalidate
46    def invalidate(self, context: BundleContext) -> None:
47        """
48        The component has been invalidated. This method is called right after
49        the provided service has been removed from the framework.
50        """
51        self.dictionary = set()
52
53    # No need to have explicit types: annotations are inherited
54    def check_word(self, word):
55        word = word.lower().strip()
56        return not word or word in self.dictionary
  • The @Component decorator is used to declare an iPOPO component. It must always be on top of other decorators.

  • The @Provides decorator indicates that the component provides a service. We also indicate the type of service we provide, either using the type directly (recommended) or its specification name.

  • The @Instantiate decorator instructs iPOPO to automatically create an instance of our component. The relation between components and instances is the same than between classes and objects in the object-oriented programming.

  • The @Property decorator indicates the properties associated to this component and to its services (e.g. French or English language).

  • The method decorated with @Validate will be called when the instance becomes valid.

  • The method decorated with @Invalidate will be called when the instance becomes invalid (e.g. when one its dependencies goes away) or is stopped.

For more information about decorators, see iPOPO Decorators.

In order for IDEs and type checking tools like MyPy to help you developping components, you should indicate that the component class inherits from the specification protocols it provides.

The French dictionary bundle: Providing a service

The spell_dictionary_FR bundle is a similar to the spell_dictionary_EN one. It only differs in the language component property, as it checks some French words declared during component validation.

 1#!/usr/bin/python
 2# -- Content-Encoding: UTF-8 --
 3"""
 4This bundle provides a component that is a simple implementation of the
 5Dictionary service. It contains some French words.
 6"""
 7
 8from typing import Set
 9
10from spell_checker_api import SpellDictionary
11
12from pelix.framework import BundleContext
13from pelix.ipopo.decorators import ComponentFactory, Instantiate, Invalidate, Property, Provides, Validate
14
15
16# Name the iPOPO component factory
17@ComponentFactory("spell_dictionary_fr_factory")
18# This component provides a dictionary service
19@Provides(SpellDictionary)
20# It is the French dictionary
21@Property("_language", "language", "FR")
22# Automatically instantiate a component when this factory is loaded
23@Instantiate("spell_dictionary_fr_instance")
24class FrenchSpellDictionary(SpellDictionary):
25    """
26    Implementation of a spell dictionary, for French language.
27    """
28
29    def __init__(self) -> None:
30        """
31        Declares members, to respect PEP-8.
32        """
33        self.dictionary: Set[str] = set()
34
35    @Validate
36    def validate(self, context: BundleContext) -> None:
37        """
38        The component is validated. This method is called right before the
39        provided service is registered to the framework.
40        """
41        # All setup should be done here
42        self.dictionary = {"bonjour", "le", "monde", "au", "a", "ipopo", "tutoriel"}
43        print("A French dictionary has been added")
44
45    @Invalidate
46    def invalidate(self, context: BundleContext) -> None:
47        """
48        The component has been invalidated. This method is called right after
49        the provided service has been removed from the framework.
50        """
51        self.dictionary = set()
52
53    # No need to have explicit types: annotations are inherited
54    def check_word(self, word):
55        word = word.lower().strip()
56        return not word or word in self.dictionary

It is important to note that the iPOPO factory name must be unique in a framework: only the first one to be registered with a given name will be taken into account. The name of component instances follows the same rule.

The spell checker bundle: Requiring a service

The spell_checker bundle aims to provide a spell checker service. However, to serve this service, this implementation requires a dictionary service. During this step, we will create an iPOPO component requiring a Dictionary service and providing the Spell Checker service.

  1#!/usr/bin/python
  2# -- Content-Encoding: UTF-8 --
  3"""
  4The spell_checker component uses the dictionary services to check the spell of
  5a given text.
  6"""
  7
  8import re
  9from typing import Dict, List, Set
 10
 11from spell_checker_api import SpellChecker, SpellDictionary
 12
 13from pelix.framework import BundleContext
 14from pelix.internals.registry import ServiceReference
 15from pelix.ipopo.decorators import (
 16    BindField,
 17    ComponentFactory,
 18    Instantiate,
 19    Invalidate,
 20    Provides,
 21    Requires,
 22    UnbindField,
 23    Validate,
 24)
 25
 26
 27# Name the component factory
 28@ComponentFactory("spell_checker_factory")
 29# Provide a Spell Checker service
 30@Provides(SpellChecker)
 31# Consume all Spell Dictionary services available (aggregate them)
 32@Requires("_spell_dictionaries", SpellDictionary, aggregate=True)
 33# Automatic instantiation
 34@Instantiate("spell_checker_instance")
 35class SpellCheckerImpl:
 36    """
 37    A component that uses spell dictionary services to check the spelling of
 38    given texts.
 39    """
 40
 41    # We can declare the type of injected fields
 42    _spell_dictionaries: List[SpellDictionary]
 43
 44    def __init__(self) -> None:
 45        """
 46        Define class members
 47        """
 48        # the list of available dictionaries, constructed
 49        self.languages: Dict[str, SpellDictionary] = {}
 50
 51        # list of some punctuation marks could be found in the given passage,
 52        # internal
 53        self.punctuation_marks: Set[str] = set()
 54
 55    @BindField("_spell_dictionaries")
 56    def bind_dict(
 57        self, field: str, service: SpellDictionary, svc_ref: ServiceReference[SpellDictionary]
 58    ) -> None:
 59        """
 60        Called by iPOPO when a spell dictionary service is bound to this
 61        component
 62        """
 63        # Extract the dictionary language from its properties
 64        language = svc_ref.get_property("language")
 65
 66        # Store the service according to its language
 67        self.languages[language] = service
 68
 69    @UnbindField("_spell_dictionaries")
 70    def unbind_dict(
 71        self, field: str, service: SpellDictionary, svc_ref: ServiceReference[SpellDictionary]
 72    ) -> None:
 73        """
 74        Called by iPOPO when a dictionary service has gone away
 75        """
 76        # Extract the dictionary language from its properties
 77        language = svc_ref.get_property("language")
 78
 79        # Remove it from the computed storage
 80        # The injected list of services is updated by iPOPO
 81        del self.languages[language]
 82
 83    @Validate
 84    def validate(self, context: BundleContext) -> None:
 85        """
 86        This spell checker has been validated, i.e. at least one dictionary
 87        service has been bound.
 88        """
 89        # Set up internal members
 90        self.punctuation_marks = {",", ";", ".", "?", "!", ":", " "}
 91        print("A spell checker has been started")
 92
 93    @Invalidate
 94    def invalidate(self, context: BundleContext) -> None:
 95        """
 96        The component has been invalidated
 97        """
 98        self.punctuation_marks = set()
 99        print("A spell checker has been stopped")
100
101    def check(self, passage, language="EN"):
102        # list of words to be checked in the given passage
103        # without the punctuation marks
104        checked_list = re.split("([!,?.:; ])", passage)
105        try:
106            # Get the dictionary corresponding to the requested language
107            dictionary = self.languages[language]
108        except KeyError:
109            # Not found
110            raise KeyError(f"Unknown language: {language}")
111
112        # Do the job, calling the found service
113        return [
114            word
115            for word in checked_list
116            if word not in self.punctuation_marks and not dictionary.check_word(word)
117        ]
  • The @Requires decorator specifies a service dependency. This required service is injected in a local variable in this bundle. Its aggregate attribute tells iPOPO to collect the list of services. Again, the specification can be given by type or by name. providing the required specification, instead of the first one.

  • The @BindField decorator indicates that a new required service bounded to the platform.

  • The @UnbindField decorator indicates that one of required service has gone away.

As you can see, the injected field is declared twice:

  • in the @Requires decorator, so that iPOPO knows what fields must be injected and how, which is mandatory for iPOPO to work

  • at the class level, to give the injected field a type hint

This is due to a limitation of Python that doesn’t support annotating class members. That being said, it is highly recommended to manually declare the field class level at class with its type in order to benefit fully from type checking tools and code completion in your IDE.

Also note that type hint you indicate must match the parameters you give to @Requires:

  • If optional is set True, the injected value can be None, else it can only be of the specification type

  • If aggregate is set to True, the injected value is a list

  • Adapt the type for more complex decorators like @RequiresMap, …

The spell client bundle

The spell_client bundle contains a very simple user interface allowing a user to interact with a spell checker service.

 1#!/usr/bin/python
 2# -- Content-Encoding: UTF-8 --
 3"""
 4This bundle defines a component that consumes a spell checker.
 5It provides a shell command service, registering a "spell" command that can be
 6used in the shell of Pelix.
 7
 8It uses a dictionary service to check for the proper spelling of a word by check
 9for its existence in the dictionary.
10"""
11
12from spell_checker_api import SpellChecker
13
14from pelix.framework import BundleContext
15from pelix.ipopo.decorators import ComponentFactory, Instantiate, Invalidate, Provides, Requires, Validate
16from pelix.shell import ShellCommandsProvider
17from pelix.shell.beans import ShellSession
18
19
20# Name the component factory
21@ComponentFactory("spell_client_factory")
22# Consume a single Spell Checker service
23@Requires("_spell_checker", SpellChecker)
24# Provide a shell command service
25@Provides(ShellCommandsProvider)
26# Automatic instantiation
27@Instantiate("spell_client_instance")
28class SpellClient(ShellCommandsProvider):
29    """
30    A component that provides a shell command (spell.spell), using a
31    Spell Checker service.
32    """
33
34    # Declare the injected field with its type
35    _spell_checker: SpellChecker
36
37    @Validate
38    def validate(self, context: BundleContext) -> None:
39        """
40        Component validated, just print a trace to visualize the event.
41        Between this call and the call to invalidate, the _spell_checker member
42        will point to a valid spell checker service.
43        """
44        print("A client for spell checker has been started")
45
46    @Invalidate
47    def invalidate(self, context: BundleContext) -> None:
48        """
49        Component invalidated, just print a trace to visualize the event
50        """
51        print("A spell client has been stopped")
52
53    def get_namespace(self):
54        """
55        Retrieves the name space of this shell command provider.
56        Look at the shell tutorial for more information.
57        """
58        return "spell"
59
60    def get_methods(self):
61        """
62        Retrieves the list of (command, method) tuples for all shell commands
63        provided by this component.
64        Look at the shell tutorial for more information.
65        """
66        return [("spell", self.spell)]
67
68    def spell(self, session: ShellSession):
69        """
70        Reads words from the standard input and checks for their existence
71        from the selected dictionary.
72
73        :param session: The shell session, a bean to interact with the user
74        """
75        # Request the language of the text to the user
76        passage = None
77        language = session.prompt("Please enter your language, EN or FR: ")
78        language = language.upper()
79
80        while passage != "quit":
81            # Request the text to check
82            passage = session.prompt("Please enter your paragraph, or 'quit' to exit:\n")
83
84            if passage and passage != "quit":
85                # A text has been given: call the spell checker, which have been
86                # injected by iPOPO.
87                misspelled_words = self._spell_checker.check(passage, language)
88                if not misspelled_words:
89                    session.write_line("All words are well spelled!")
90                else:
91                    session.write_line(f"The misspelled words are: {misspelled_words}")

The component defined here implements and provides a shell command service, which will be consumed by the Pelix Shell Core Service. It registers a spell shell command.

Main script: Launching the framework

We have all the bundles required to start playing with the application. To run the example, we have to start Pelix, then all the required bundles.

 1#!/usr/bin/python
 2# -- Content-Encoding: UTF-8 --
 3"""
 4Starts a Pelix framework and installs the Spell Checker bundles
 5"""
 6
 7# Standard library
 8import logging
 9
10# Our spell checker API
11from spell_checker_api import SpellChecker
12
13# Pelix framework module and utility methods
14import pelix.framework
15from pelix.utilities import use_service
16
17
18def main():
19    """
20    Starts a Pelix framework and waits for it to stop
21    """
22    # Prepare the framework, with iPOPO and the shell console
23    # Warning: we only use the first argument of this method, a list of bundles
24    framework = pelix.framework.create_framework(
25        (
26            # iPOPO
27            "pelix.ipopo.core",
28            # Shell core (engine)
29            "pelix.shell.core",
30            # Text console
31            "pelix.shell.console",
32        )
33    )
34
35    # Start the framework, and the pre-installed bundles
36    framework.start()
37
38    # Get the bundle context of the framework, i.e. the link between the
39    # framework starter and its content.
40    context = framework.get_bundle_context()
41
42    # Start the spell dictionary bundles, which provide the dictionary services
43    context.install_bundle("spell_dictionary_EN").start()
44    context.install_bundle("spell_dictionary_FR").start()
45
46    # Start the spell checker bundle, which provides the spell checker service.
47    context.install_bundle("spell_checker").start()
48
49    # Sample usage of the spell checker service
50    # 1. get its service reference, that describes the service itself
51    ref_config = context.get_service_reference(SpellChecker)
52    if ref_config is None:
53        print("Error: spell service not found")
54        return
55
56    # 2. the use_service method allows to grab a service and to use it inside a
57    # with block. It automatically releases the service when exiting the block,
58    # even if an exception was raised
59    with use_service(context, ref_config) as svc_config:
60        # Here, svc_config points to the spell checker service
61        passage = "Welcome to our framwork iPOPO"
62        print("1. Testing Spell Checker:", passage)
63        misspelled_words = svc_config.check(passage)
64        print(">  Misspelled_words are:", misspelled_words)
65
66    # Start the spell client bundle, which provides a shell command
67    context.install_bundle("spell_client").start()
68
69    # Wait for the framework to stop
70    framework.wait_for_stop()
71
72
73# Classic entry point...
74if __name__ == "__main__":
75    logging.basicConfig(level=logging.DEBUG)
76    main()

Running the application

Launch the main_pelix_launcher.py script. When the framework is running, type in the console: spell to enter your language choice and then your passage.

Here is a sample run, calling python main_pelix_launcher.py:

INFO:pelix.shell.core:Shell services registered
An English dictionary has been added
** Pelix Shell prompt **
A French dictionary has been added
A dictionary checker has been started
1. Testing Spell Checker: Welcome to our framwork iPOPO
>  Misspelled_words are: ['our', 'framwork']
A client for spell checker has been started

$ spell
Please enter your language, EN or FR: FR
Please enter your paragraph, or 'quit' to exit:
Bonjour le monde !
All words are well spelled !
Please enter your paragraph, or 'quit' to exit:
quit
$ spell
Please enter your language, EN or FR: EN
Please enter your paragraph, or 'quit' to exit:
Hello, world !
All words are well spelled !
Please enter your paragraph, or 'quit' to exit:
Bonjour le monde !
The misspelled words are: ['Bonjour', 'le', 'monde']
Please enter your paragraph, or 'quit' to exit:
quit
$ quit
Bye !
A spell client has been stopped
INFO:pelix.shell.core:Shell services unregistered

You can now go back to see other tutorials or take a look at the Reference Cards.