1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
|
\lstset{frame=single,
basicstyle=\footnotesize ,
backgroundcolor=\color{white},
language=Python}
\section{The PyUtilib Component Architecture}
\label{chap:pca}
The PyUtilib Component Architecture (\pca) is included in the
PyUtilib software package~\cite{PyUtilib}. It inspired by the Trac
plugin framework~\cite{Trac}, and it was initially motivated by our
need for an independent, self-contained plugin framework for
scientific computing applications. The core of the \pcasp is
provided through a small set of classes within PyUtilib's
\code{pyutilib.component.core} package. This section provides a
tutorial introduction of the \pcasp as well as a detailed description
of the \pcasp classes and their functionality.
\subsection{\pcasp Definitions}
There are different notions of software components~\cite{Mar05}, so we begin by
providing some definitions. The relationships among these terms is
illustrated in Figure \ref{fig:defs}. A \textit{plugin} is a
class that implements a set of related methods in the context of an
application. Thus, a plugin can be described as a component definition.
An \textit{interface} class defines a type that a plugin uses to register
its capabilities. A plugin class includes declarations that denote that
it implements one-or-more interfaces. An interface is defined by the
methods and data that are used. However, the \pcasp does not enforce
this interface or support interface conversions (see Zope~\cite{Zope}
and Envisage~\cite{Envisage} for examples of plugin frameworks that
support this functionality).
A \textit{service} is an instance of a plugin class that is registered
globally with its interfaces. We say that a plugin class instance
is \textit{active} if it is registered, and a plugin class instance
is only treated as a service if it is active. A service can be an
instance of either a singleton or non-singleton plugin. There is
exactly one service for a singleton plugin (and that service is
instantiated automatically), whereas there can be multiple services
of non-singleton plugins. Singleton plugins are active by default,
whereas non-singleton plugins are conditionally active depending
how the plugin interface is declared.
\begin{figure}[htb]
\center
\includegraphics[height=5in,angle=-90]{figures/definitions.pdf}
\caption{An illustration of how classes within the PCA relate to one another. Interface classes are independent declarations of APIs. Plugin classes can declare that they implement an Interface's API, and extension points declare that they require a specific
Interface API. Both singleton and non-singleton plugins can be used, but services for non-singleton plugins are explicitly constructed.}
\label{fig:defs}
\end{figure}
A software application or a component can declare \textit{extension
points} that other components can \textit{plug in} to. An extension
point is defined with respect to a specific \textit{interface}
class. Thus, a service that supports an interface plugs into an
extension point for that interface. In this way, extension points
provide a generic mechanism for applications to employ the functionality
provided by other services.
This mechanisms supports a flexible,
modular programming paradigm that enables software applications to
be extended in a dynamic manner. The \pcasp includes a global component
registry and a framework for automating the execution of plugin
services. All plugins and interfaces automatically register themselves
with the registry. This registry then acts as a broker, dynamically providing
extension points with the appropriate matching services at run time.
Thus, an application
developer can define extension points without knowing how they will be
implemented, and plugin developers can register extensions without
needing to know the details of how -- or where -- they are employed.
This capability facilitates the dynamic registration
and application of components within large software systems.
\subsubsection{Relationship to the Trac Component System}
The general design of \pcasp is inspired by the plugin system
included in Trac~\cite{Trac}. The \pcasp generalizes the Trac
component architecture by supporting namespace management of plugins,
non-singleton plugins, support for non-active plugin instances and
caching of interface interactions. For those familiar with Trac,
the following classes roughly correspond with each other:
\begin{center}
\begin{tabular}{|l|l|} \hline
Trac Class Name & PyUtilib Class Name \\ \hline
Interface & Interface \\
ExtensionPoint & ExtensionPoint \\
Component & SingletonPlugin \\
ComponentManager & PluginEnvironment \\ \hline
\end{tabular}
\end{center}
The \code{PluginEnvironment} class is used to manage plugins, but unlike Trac this class does not need to be explicitly constructed.
\subsection{A Simple Example}
Figure~\ref{fig:example1} provides a simple example that is adapted from
the description of the Trac component architecture~\cite{TCA}. This
example illustrates the three main steps to setting up a plugin:
\begin{enumerate}
\item Defining an interface
\item Declaring extension points
\item Defining classes that implement the interface.
\end{enumerate}
This example begins by defining \code{ITaskObserver}, a subclass of \code{Interface}.
Although it is not required to define methods in an interface, these
declarations provide documentation for plugin developers.
We then declare a \code{TaskList} that manages a
dictionary of tasks and descriptions. The \code{TaskList} creates an
\code{ITaskObserver} extension point, and when a task is added to the
task list, it calls all services that implement the \code{ITaskObserver}
interface. Finally, we define a \code{TaskPrinter} as a singleton
plugin. The \code{TaskPrinter} implements the \code{ITaskObserver}
interface, and when called prints the task name and description. As the
\code{TaskPrinter} is a singleton plugin, the \pcasp automatically
instantiates and registers a single \code{TaskPrinter} service.
%In this example, a singleton plugin is declared, which automatically
%registers the single component service. Non-singleton plugin services need to
%explicitly created, but they are also automatically registered.
Assuming the module in Figure~\ref{fig:example1}
is saved as \code{task.py}, then the following Python script
illustrates how this plugin is used:
\begin{quotation}
\begin{lstlisting}
from task import *
# Construct a TaskList object and then add several tasks.
task_list = TaskList()
task_list.add('Make coffee', 'Need to make some coffee')
task_list.add('Bug triage', 'Double-check all issues')
\end{lstlisting}
\end{quotation}
This script generates the following output:
\begin{quotation}
\begin{lstlisting}
Task: Make coffee
Need to make some coffee
Task: Bug triage
Double-check all issues
\end{lstlisting}
\end{quotation}
\begin{figure}
\center
\begin{quotation}
\begin{lstlisting}
# A simple example that manages a task list. An observer
# interface adds actions that occur when a task is added.
from pyutilib.component.core import *
# An interface class that defines the API for plugins that
# observe when a task is added.
class ITaskObserver(Interface):
def task_added(self, name, description):
"""Called when a task is added."""
# The task list application, which declares an extension point
# for observers. Observers are notified when a new task
# is added to the task list.
class TaskList(object):
observers = ExtensionPoint(ITaskObserver)
def __init__(self):
"""The TaskList constructor, which initializes the list"""
self.tasks = {}
def add(self, name, description):
"""Add a task, and notify the observers"""
self.tasks[name] = description
for observer in self.observers:
observer.task_added(name, description)
# A plugin that prints information about tasks when they
# are added.
class TaskPrinter(SingletonPlugin):
implements(ITaskObserver)
def task_added(self, name, description):
print 'Task:', name
print ' ', description
\end{lstlisting}
\end{quotation}
\caption{A simple example of \pcasp components.}
\label{fig:example1}
\end{figure}
\section{PCA Classes}
\label{chap:core}
The \pcasp consists of the following core classes that are defined in the \code{pyutilib.component.core} package:
\begin{description}
\item[Interface] Subclasses of this class declare plugin interfaces that are registered in the \pca.
\item[ExtensionPoint] A class used to declare extension points, which can access services with a particular interface.
\item[Plugin] Subclasses of this class declare plugins, which can be used to implement interfaces within the \pca.
\item[SingletonPlugin] Subclasses of this class declare singleton plugins, for which a single service is constructed.
\item[PluginEnvironment] A class that maintains the registries for interfaces, extension points, plugins and services.
\item[PluginGlobals] A class that maintains global data concerning the set of environments that are currently being used.
\item[PluginError] The exception class that is raised when errors arise in the \pca.
\end{description}
The following sections provide a detailed description of how these classes are used in the
\pca.
\subsection{Interfaces and Extension Points}
A subclass of the \code{Interface} class is used to declare extension
points in an application. The \code{ExtensionPoint} class is used to
declare an extension point and to retrieve information about the plugins
that implement the specified interface. For example, the following is
a minimal declaration of an interface and extension point:
\begin{quotation}
\begin{lstlisting}
class IMyInterface(Interface):
"""An interface subclass"""
ep = ExtensionPoint(IMyInterface)
\end{lstlisting}
\end{quotation}
Note that the \code{IMyInterface} class is not required to define the API
of the interface. The \pcasp does not enforce checking of API conformance
for plugins, and hence any declaration in the \code{IMyInterface}
class would be ignored. Additionally, note that an instance of
\code{IMyInterface} is not created; the \code{IMyInterface} class is simply
used to declare a type that is used to index related plugins.
An instance of \code{ExtensionPoint} can be used to iterate through
all extensions, or to search for an extension that matches a particular
keyword. For example, the following code iterates through all extensions
and applies the \code{pprint} method:
\begin{quotation}
\begin{lstlisting}
for service in ep:
service.pprint()
\end{lstlisting}
\end{quotation}
If you wish to know the number of services that are registered with an
extension point, you can call the standard \code{len} function:
\begin{quotation}
\begin{lstlisting}
print len(ep)
\end{lstlisting}
\end{quotation}
Several other methods can be used to more carefully select services from
an extension point. The \code{extensions} method returns a Python \code{set} object
that contains the services:
\begin{quotation}
\begin{lstlisting}
#
# This loop iterates over all services, just the same
# as when an the iterator method is used (see above).
#
for service in ep.extensions():
service.pprint()
\end{lstlisting}
\end{quotation}
The Python \code{\_\_call\_\_} method provides a convenient shorthand for this same function. Thus, the following is equivalent:
\begin{quotation}
\begin{lstlisting}
for service in ep():
service.pprint()
\end{lstlisting}
\end{quotation}
These methods have two optional arguments that control the selection of services. The \code{all}
keyword indicates whether
the set returned by \code{extensions} includes all disabled services.
\begin{quotation}
\begin{lstlisting}
#
# This loop iterates over all services, including
# the 'disabled' services.
#
for service in ep.extensions(all=True):
service.pprint()
\end{lstlisting}
\end{quotation}
It is
convenient to explicitly support enabling and disabling services in many
applications, though services are enabled by default. Disabled services
remain in the registry, but by default they are not included in the set
returned by an extension point.
The \pcasp can also support \textit{named services}, which requires that
the services have a \code{name} attribute. Service names are not required
to be unique. For example, when multiple instances of a non-singleton
plugin are created, then these services can be accessed as follows:
\newpage
\begin{quotation}
\begin{lstlisting}
#
# A simple plugin that implements the IMyInterface interface
#
class MyPlugin(Plugin):
implements(IMyInterface)
def __init__(self):
self.name="myname"
#
# Another simple plugin that implements the IMyInterface interface
#
class MyOtherPlugin(Plugin):
implements(IMyInterface)
def __init__(self):
self.name="myothername"
#
# Constructing services
#
service1 = MyPlugin()
service2 = MyPlugin()
service3 = MyOtherPlugin()
#
# A function that iterates over all IMyInterface services, and
# returns the MyPlugin instances (which are service1 and service2).
#
def get_services():
ep = ExtensionPoint(IMyInterface)
return ep("myname")
\end{lstlisting}
\end{quotation}
In some applications, there is a one-to-one correspondence between service
names and their instances. In this context, a simpler syntax is to use
the \code{service} method:
\begin{quotation}
\begin{lstlisting}
class MySingletonPlugin(SingletonPlugin):
implements(IMyInterface)
def __init__(self):
self.name="mysingletonname"
ep = ExtensionPoint(IMyInterface)
ep.service("mysingletonname").pprint()
\end{lstlisting}
\end{quotation}
The \code{service} method raises a \code{PluginError} if there is more than one service
with a given name. Note, however, that this method returns \code{None} if no
service has been registered with the specified name.
Note that an integer cannot be used to select a service from an extension
point. Services are not registered in an indexable array, so this option
is disallowed.
\subsection{Plugins}
\pcasp plugins are subclasses of either the \code{Plugin} or \code{SingletonPlugin}
classes. Subclasses of \code{Plugin} need to be explicitly constructed,
but otherwise they do not need to be registered; simply constructing a
subclass of \code{Plugin} invokes the registration of that instance. Similarly,
simply declaring a subclass of \code{SingletonPlugin} invokes both the construction
and registration of this component.
\pcasp plugins are registered with different interfaces using the \code{implements}
function, which is a static method of \code{Plugin}. Note that a plugin can be
registered with more than one interface. Further, a service can be applied to different
extension points independently, but it can maintain state information
that impacts its use across different extension points.
The default behavior of the \pcasp is to ignore the declarations in an interface class, but
the \code{implements} function includes an \code{inherit} keyword can be used to define a plugin that
inherits interface methods. For example:
\begin{quotation}
\begin{lstlisting}
class IMyInterface(Interface):
def print(self):
print "This is the default print method"
def add(self, x):
return x+2
class MyPlugin(Plugin):
implements(IMyInterface, inherit=True)
def add(self, x):
return x+3
\end{lstlisting}
\end{quotation}
In this example, the \code{MyPlugin} class implements the \code{IMyInterface} interface. Since the \code{inherit} keyword is \code{True}, the \code{MyPlugin} class inherits the \code{print} method. Thus, \code{MyPlugin} has a complete implementation of the \code{IMyInterface} interface.
Although this behavior is generally useful, the API for \pcasp
intentionally does not make interface inheritance the default behavior.
When inheritance is used, a developer can get into trouble if they mistype
the name of a plugin method. When this occurs, the interface method is
used, without any notification to the user. This could easily lead to
erroneous plugin behavior that is quite difficult to track down.
When \code{Plugin} classes are instantiated or \code{SingletonPlugin}
classes are declared, the resulting class service is registered in global \pcasp
data (see below). The \code{Plugin} class includes several methods
for controlling this registration. The \code{disable} and \code{enable}
methods provide a simple mechanism for controlling whether a service
is returned with associated extension points. These methods clear the
\pcasp caches that are associated with these extension points to ensure
that the extension points are correctly setup. The \code{activate}
and \code{deactivate} methods respectively add and remove the service
from the global environment. This has a similar effect as \code{enable}
and \code{disable}, except that after deactivation the service is no
longer associated with the \pcasp global data, while after \code{disable}
the service is still registered but flagged as not active.
\subsection{Environments}
The \code{PluginEnvironment} class defines namespaces that contain component services and interfaces.
These namespaces provide
a mechanism for organizing component services in an extensible
manner. Applications can define new namespaces that contain their services
without worrying about conflicts with services defined in other Python
libraries.
A global registry of environments
is maintained by the \code{PluginGlobals} class. This registry is a stack
of environments, and the top of this stack defines the current
environment. When an interface is declared, its namespace is the name
of the current environment. For example:
\begin{quotation}
\begin{lstlisting}
#
# Declare an interface in the current environment
#
class Interface1(Interface):
pass
#
# Set the current environment to 'new_environ'
#
PluginGlobals.push_env( "new_environ" )
#
# Declare an interface in the 'new_environ' environment
#
class Interface2(Interface):
pass
#
# Go back to the original environment
#
PluginGlobals.pop_env()
\end{lstlisting}
\end{quotation}
Component services are automatically registered in namespaces in two ways. First,
for each interface that the service implements, the service is registered in
the namespace in which the interface was declared. Second, a service is
registered in the namespace in which its corresponding plugin class is declared.
For example, consider the code in Figure~\ref{fig:env1}.
When \code{Plugin1} is instantiated, this service is registered in the
following environments:
\begin{quotation}
\begin{lstlisting}
env1 for Interface1
env2 for Interface2
env4 for Interface1
env3
\end{lstlisting}
\end{quotation}
The last registration is the default, since a service is always
registered in the environment where its plugin class is declared.
Note that \code{env4} namespace is declared explicitly in this example.
\begin{figure}
\begin{quotation}
\begin{lstlisting}
#
# Declare Interface1 in namespace env1
#
PluginGlobals.push_env("env1")
class Interface1(Interface):
pass
#
# Declare Interface2 in namespace env2
#
PluginGlobals.push_env("env2")
class Interface2(Interface):
pass
PluginGlobals.pop_env()
#
# Declare Plugin1 in namespace env3
#
PluginGlobals.push_env("env3")
class Plugin1(Plugin):
implements(Interface1)
implements(Interface2)
implements(Interface1,"env4")
PluginGlobals.pop_env()
\end{lstlisting}
\end{quotation}
\caption{\label{fig:env1} Illustration of how plugin declarations are related to component environments.}
\end{figure}
\subsection{Global Component Data}
Global component data in \pcasp is managed in the \code{PluginGlobals} class. This
class contains a variety of static methods that are used to access
this data:
\begin{description}
\item[default\_env] This method returns the default environment, which is constructed when the \pcasp is loaded.
\item[env] This method returns the current environment if no argument is specified. Otherwise, it returns the specified environment.
\item[push\_env,pop\_env] These methods respectively push a new environment onto the environment stack and pop the current environment from the stack.
\item[services] This method returns the component services in the current environment (or the named environment if one is specified).
\item[singleton\_services] This method returns the singleton component services in the current environment (or the named environment if one is specified).
\item[load\_services] Load services using \code{IPluginLoader} extension points (see Section~\ref{sec:loaders}).
\item[pprint] This method provides a text summary of the registered interfaces, plugins and services.
\item[clear] This method empties the environment stack and defines a new default environment. This setup then bootstraps the configuration of the \code{pyutilib.component.core} environment. Note that this does not clear the component registry; in practice that may not make sense since it is not easy to reload modules in Python.
\end{description}
|