Building a Pyre Application
From DANSE
Useful links to Pyre documentation:
- Pyre Conventions (http://www.cacr.caltech.edu/projects/pyre/docs/ch01s05.html)
- Pyre Tutorial (http://www.cacr.caltech.edu/projects/pyre/docs/ch04.html)
- Pyre Components (http://www.cacr.caltech.edu/projects/pyre/docs/ch06s04.html)
- UML diagrams for Application class (http://arcs.cacr.caltech.edu/arcs/pythia/dia.html)
Deprecated links:
- An older tutorial on Building a Pyre component.
Introduction
The Pyre framework connects various components together at runtime to form applications. Pyre assumes responsibility for managing the components, and provides a uniform set of steps for constructing and initializing components. This allows Pyre to provide guarantees about how and when components will be ready to use.
Pyre applications are the foundation of the Pyre framework, and are the smallest independently usable quantity. Applications are built to perform a specific type of task, or solve a specific type of problem. Pyre components are the base unit for the Pyre framework. Like applications, Pyre components are also designed to perform a single function; however, they are not as independent as are Pyre applications and typically operate at a higher level of granularity. The distinction between a Pyre application and a Pyre component is primarily a conceptual one:
- components are not designed to run independently (except for testing purposes)
- components are designed to perform a function and may a return result, while applications are designed to run as a script and return nothing.
- components can be used as Pyre facilities, while applications cannot
- components can be utilized by other components or an application, while applications must be at the top-level (they cannot act as modular components)
So, a Pyre component that reads a NeXus file may be combined with a data selector component, a background subtractor component, and a NeXusWriter component to build an application that rebins your neutron scattering data.
Figure: Pictoral representation of a Pyre application. In Pyre, applications are built from modular components that the user interconnects and launches at runtime.
Generating a Template Pyre Application
Let's start by building a very simple application. We will then learn the core pieces of the application, and how to modify each piece. Pyre provides an easy way to generate an application template.
>$ app.py --name=demo
This will generate a simple (single-component) application named 'demo.py'...
#!/usr/bin/env python
# =================================================================
#
# {LicenseText}
#
# =================================================================
#
from pyre.applications.Script import Script
class DemoApp(Script):
class Inventory(Script.Inventory):
import pyre.inventory
name = pyre.inventory.str('name', default='world')
name.meta['tip'] = 'the entity to greet'
def main(self, *args, **kwds):
print 'Hello %s!' % self.friend
return
def __init__(self):
Script.__init__(self, 'demo')
self.friend = ""
return
def _defaults(self):
Script._defaults(self)
return
def _configure(self):
Script._configure(self)
self.friend = self.inventory.name
return
def _init(self):
Script._init(self)
return
# main
if __name__ == '__main__':
app = DemoApp()
app.run()
# version
__id__ = "$Id$"
# End of file
This simple application demonstrates the Pyre version of the universial introductory program for software engineering... "Hello world!".
>$ python demo.py Hello world!
We will use this simple application for the rest of the tutorial. Even though we have not explained what comprises a Pyre application, we will soon see that all Pyre applications have:
- a main class ('DemoApp') derived from the pyre Application class
- a main method (and corresponding 'if __name__ = '__main__':)
- an __init__ method
Our simple application also includes two of the most common optional methods:
- the _configure method
- the Inventory class
and also has two additional less common optional methods:
- the _defaults method
- the _init method
Each of these classes and methods (as well as some additional common methods for Pyre applications) will be explained below.
Conceptual Differences Between a Pyre Application and a Pyre Component
As mentioned previously, the differences between Pyre applications and Pyre components are primarily conceptual. However, these conceptual differences are highlighted by slight structural differences.
Pyre components:
- inherit from "pyre.components.Component" instead of "pyre.applications.Script"
- substitute all instances of "Script" with "Component"
- do not have a main method {i.e called by run()}
- restrict the use of if __name__ == '__main__': to simple 'functional' testing
- can be used as Pyre facilities {provide a facility name in Component.__init__()}
These differences are very subtle; thus this tutorial can provide the developer a basic understanding of Pyre components and Pyre applications simultaneously. We will return to this topic again at the end of the tutorial, when we will build a pyre component.
Basic Structure of a Pyre Application
Through inheritance, our new application class instantly gets all of the properties and functionality of a standard Pyre Application.
from pyre.applications.Script import Script class DemoApp(Script):
This encapsulates all of the work it takes to integrate a new application into the modular framework, and allows the developer to concentrate on extending the functionallity of our application.
It is Pyre naming convention to capatalize the filename of the component or application, and also to use the exact same name for the 'base' class within the file. Oddly, the 'app.py' application generator follows neither of those conventions. The convention creates an easy way to remember the import sequence for your application. For example, if our code is within a directory "template", then the pyre convention for importing a application is:
>>> from template.Demo import Demo **OR** >>> from template import Demo
However, we currently would have:
>>> from template.demo import DemoApp
We can choose to correct this within the file itself, or fix it later using a Pyre factory.
If we choose to modify the code, we would first rename the file from 'demo.py' to 'Demo.py', then we would simply substitute "Demo" for "DemoApp" everywhere within the file 'Demo.py'. However, there may be reasons that one would like the names to differ within the actual code. Thus we will keep the file as it is right now, and later use a Factory to force the file conform to convention.
Inventory Class
The inventory is a warehouse of pyre.factories, pyre.properties and pyre.validators. Pyre properties encompass the pyre.inventory implementation of basic programming types such as: property (generic), str, bool, int, float, list, slice, and dimensional (dimensional quantities). Pyre facilities act as "markers" or "specification of requirements" for concrete Components to fulfill. They use special features of the inventory to automatically bind components, and rely on support architecture to allow the user to override the default component to be bound. An (older) example of how to use components as Pyre facilities can be found here.
Using the inventory makes a component very easily reconfigurable at runtime. It should specify reasonable defaults for property values and instantiations of required facilities, but architecture exists to very easily override these--without the class needing to be modified at all. Thus, if a parameter is expected to be frequently changed by the user, it should be an inventory property. Since the inventory often contains the portion of the code that is most frequently examined or modified, many developers choose to position it as the first member in the main class -- we will follow this convention within this tutorial.
The Properties and Facilities stored in the dictionary have special properties to make sure they are not modified incorrectly, and to allow special actions when they are loaded. Pyre Validators such as: less, greater, range, and choice can be used within the inventory to constrain the acceptable input to a component's inventory.
Returning to our code, we see that our inventory contains only one property -- a string for to whom we say hello. The string is saved as the inventory variable "name", and has a default of 'world'. Let's add another inventory parameter to change the greeting as well.
class Inventory(Script.Inventory):
import pyre.inventory
name = pyre.inventory.str('name', default='world')
greeting = pyre.inventory.str('greeting', default='Hello')
Adding a second inventory item doesn't do anything for us yet, because "Hello" is still hard-wired into our run() method.
run Method
The run method (named 'main', but called by 'run') is essentially a script of what you want your application to do. In our example, we will simply print a message composed of a salutation to a friend.
def main(self, *args, **kwds):
"""run() --> give greeting"""
print '%s %s!' % (self.salutation, self.friend)
return
Note that some documentation has been added to explain the basic use and result for our run method. In general, a run method utilizes the basic data members for your application class to perform the task that you script; therefore it is preferred that the run method should have no connection to the variables defined within the Inventory class.
_configure Method
The _configure method relates the application's data members to the inventory variables. This provides the connection between the parameters the user is meant to set (within the inventory) and the variables the application is meant to manipulate (i.e. the data members). For our code, we now can define a relationship between 'greeting' and 'salutation'.
def _configure(self):
Script._configure(self)
self.friend = self.inventory.name
self.salutation = self.inventory.greeting
return
It is the preferred approach to use an _configure method to protect the run method from using inventory variables, however it is not necessary to do so (as shown in this minimal Pyre application). The _configure method is automatically called by Pyre whenever app.run() is launched. In our example above, the _configure method sets the values of the data members to be equal to the values of the corresponding inventory variables, and then Pyre proceeds to the main() method.
__init__ Method
This method is called upon instantiation of the base class (in our example, "DemoApp"). The __init__ method is used to make the initial declaration of all necessary data members. Also, to ensure that our new application performs as correctly as a Pyre Application, __init__ should also inherit the functionallity of a standard Pyre Application (through the use of a call to Script.__init__).
def __init__(self):
Script.__init__(self, 'demo')
self.friend = ""
self.salutation = ""
return
Script.__init__ standardly takes two arguments, self and application_name. The application name is commonly exactly the same as the filename.
_defaults Method
this one we will delete for now... (FIXME)
_init Method
this one we will delete for now... (FIXME)
help Method
The help method is commonly very tiny, and completely reusable across almost any component or application. However, it is one of the most important methods to include in your code. It provides for a standard place to put your documentation, and a standard way to access the documenation from python.
def help(self):
print self.__doc__
return
The call to self.__doc__ will pick up the documentation string that is given immediately after the base class is declared.
This can be best demonstrated by looking at the whole of the code we have developed thus far with this tutorial...
#!/usr/bin/env python
# =================================================================
#
# {LicenseText}
#
# =================================================================
#
from pyre.applications.Script import Script
class DemoApp(Script):
"""Pyre 'Hello' application
Inventory:
name --> who to greet; (str)
greeting --> how to greet; (str)
Members:
friend --> name of friend to greet
salutation --> content of greeting
Methods:
run() --> give greeting
Notes:
None
"""
class Inventory(Script.Inventory):
import pyre.inventory
name = pyre.inventory.str('name', default='world')
greeting = pyre.inventory.str('greeting', default='Hello')
def main(self, *args, **kwds):
"""run() --> give greeting"""
print '%s %s!' % (self.salutation, self.friend)
return
def __init__(self):
Script.__init__(self, 'demo')
self.friend = ""
self.salutation = ""
return
def _configure(self):
Script._configure(self)
self.friend = self.inventory.name
self.salutation = self.inventory.greeting
return
def help(self):
print self.__doc__
return
# main
if __name__ == '__main__':
app = DemoApp()
app.run()
# version
__id__ = "$Id$"
# End of file
Notice that the documentation provides a good summary of all of the important methods within the application, the content of the inventory, and any data members. It is also a good idea to leave space for any specific notes that may help understand the requirements, dependencies, or special input/output types for this application and its methods. The help method for a pyre application can be either called directly from the commandline
>$ python demo.py --? Pyre 'Hello' application Inventory: name --> who to greet; (str) greeting --> how to greet; (str) Members: friend --> name of friend to greet salutation --> content of greeting Methods: run() --> give greeting Notes: None
or from within a python shell or script
>>> from demo import DemoApp >>> da = DemoApp() >>> da.help() Pyre 'Hello' application Inventory: name --> who to greet; (str) greeting --> how to greet; (str) Members: friend --> name of friend to greet salutation --> content of greeting Methods: run() --> give greeting Notes: None
It is also not a bad idea to put usage instructions inside of the main application __doc__ string. We could add a summary of how to instantiate the main class, how to get to the class's help function, and so on. A main __doc__ string, and an author string are commonly included as the first bit of python code in the file, just preceeding the import statements. Our code would thus begin...
#!/usr/bin/env python
# =================================================================
#
# {LicenseText}
#
# =================================================================
#
__author__="""mmckerns@caltech.edu"""
__doc__="""Instructions for Pyre 'Hello' Application:
Import the Demo class >>> from demo import DemoApp
Instantiate the Demo class >>> da = DemoApp()
Get help >>> da.help()
"""
from pyre.applications.Script import Script
and continue as above.
If you have never used the built-in help function for python, you should try it our on our example right now!
>>> import demo >>> help(demo)
You should get very detailed information on your classes, methods, and much more.
Pyre also provides internal documentation from the command line through the 'journal' package... (FIXME)
Instantiation with a Factory
TEXT NEEDED.....
Create a new directory "template", and move 'demo.py' into it. Create a new file __init__.py within the "template" directory...
# =================================================================
#
# {LicenseText}
#
# =================================================================
#
__doc__="""Instructions for Pyre 'Hello' Application:
Import the Demo class >>> from template import Demo
Instantiate the Demo class >>> da = Demo()
Get help >>> da.help()
"""
def Demo():
"""Pyre 'Hello' application"""
from demo import DemoApp as DemoFactory
return DemoFactory()
# version
__id__ = "$Id$"
# End of file
We can now get help on our code... (FIXME)
>>> import template
>>> help(template)
Help on package template:
NAME
template
FILE
/home/mmckerns/dev/tutorials/pyre_component/template/__init__.py
DESCRIPTION
Instructions for Pyre 'Hello' Application:
Import the Demo class >>> from template import Demo
Instantiate the Demo class >>> da = Demo()
Get help >>> da.help()
PACKAGE CONTENTS
demo
FUNCTIONS
Demo()
Pyre 'Hello' application
DATA
__id__ = '$Id$'
and our application functions as before, however the import path is cleaned up a bit...
>>> from template import Demo >>> da = Demo() >>> da.help()
(FIXME)...
From Pyre Application to Pyre Components
Creating a Template Pyre Component
- Show 'development' of 'greeter.py' using 'component.py'
Using a Component as a Facility within an Application
- Modify demo.py to use greeter.py as a facility
Further Overriding Behavior within Pyre
- Show .pml usage
- Show .odb usage
Converting Existing Python Code to a Pyre Component
- Building from the Template
- Parameters, Input, and State variables
- Accessing the user's existing python code
User Defined Methods
- Rewrite within component
- Inherit from component (preferred)
Using State Variables and Accessor Methods
- state variables
- instantiation of an underlying class
- put() and get()
Testing
- Reusing Unit Test Suites

