APPTOOLS(3) | apptools | APPTOOLS(3) |
apptools - apptools 4.5.0
The Application Scripting Framework is a component of the Enthought Tool Suite that provides developers with an API that allows traits based objects to be made scriptable. Operations on a scriptable object can be recorded in a script and subsequently replayed.
The framework is completely configurable. Alternate implementations of all major components can be provided if necessary.
The following are the concepts supported by the framework.
A scriptable type is a sub-type of HasTraits that has scriptable methods and scriptable traits. If a scriptable method is called, or a scriptable trait is set, then that action can be recorded in a script and subsequently replayed.
If the __init__() method is scriptable then the creation of an object from the type can be recorded.
Scriptable types can be explicitly defined or created dynamically from any sub-type of HasTraits.
The set of a scriptable type's scriptable methods and traits constitutes the type's scriptable API.
The API can be defined explicitly using the scriptable decorator (for methods) or the Scriptable wrapper (for traits).
For scriptable types that are created dynamically then the API can be defined in terms of one or more types or interfaces or an explicit list of method and trait names. By default, all public methods and traits (ie. those whose name does not begin with an underscore) are part of the API. It is also possible to then explicitly exclude a list of method and trait names.
A scriptable object is an instance of a scriptable type.
Scriptable objects can be explicitly created by calling the scriptable type. Alternatively a non-scriptable object can be made scriptable dynamically.
A script is a Python script and may be a recording or written from scratch.
If the creation of scriptable objects can be recorded, then it may be possible for a recording to be run directly by the Python interpreter and independently of the application that made the recording. Otherwise the application must run the script and first create any scriptable objects referred to in the script.
A script runs in a namespace which is, by default, empty. If the scriptable objects referred to in a script are not created by the script (because their type's __init__() method isn't scriptable) then they must be created by the application and added to the namespace. Adding an object to the namespace is called binding.
Scriptable objects whose creation can be recorded will automatically bind themselves when they are created.
It also possible to bind an object factory rather than the object itself. The factory will be called, and the object created, only if the object is needed by the script when it is run. This is typically used by plugins.
The name that an object is bound to need bear no relation to the object's name within the application. Names may be dotted names (eg. aaa.bbb.ccc) and appropriate objects representing the intermediate parts of such a name will be created automatically.
An event is fired whenever an object is bound (or when a bound factory is invoked). This allows other objects (eg. an embedded Python shell) to expose scriptable objects in other ways.
A script manager is responsible for the recording and subsequent playback of scripts. An application has a single script manager instance which can be explicitly set or created automatically.
In the current implementation scriptable Trait container types (eg. List, Dict) may only contain objects corresponding to fundamental Python types (eg. int, bool, str).
This section gives an overview of the API implemented by the framework. The complete API documentation is available as endo generated HTML.
The example application demonstrates some the features of the framework.
script_type is the existing, non-scriptable, type. The new type will be a sub-type of it. The api, includes and excludes arguments determine which methods and traits are made scriptable. By default, all public methods and traits (ie. those whose name does not begin with an underscore) are made scriptable.
The name and bind_policy arguments determine how scriptable objects are bound when they are created. name is the name that an object will be bound to. It defaults to the name of script_type with the first character forced to lower case. name may be a dotted name, eg. aaa.bb.c.
bind_policy determines what happens if an object is already bound to the name. If it is auto then a numerical suffix will be added to the name of the new object. If it is unique then an exception will be raised. If it is rebind then the object currently bound to the name will be unbound.
api is a class or interface (or a list of classes or interfaces) that is used to provide the names of the methods and traits to be made scriptable. The class or interface effectively defines the scripting API.
If api is not specified then includes is a list of method and trait names that are made scriptable.
If api and includes are not specified then excludes is a list of method and trait names that are not made scriptable.
If script_init is set then the __init__() method is made scriptable irrespective of the api, includes and excludes arguments.
If script_init is not set then objects must be explicitly bound and name and bind_policy are ignored.
See the description of create_scriptable_type() for an explanation of the api, includes and excludes arguments.
The ScriptManager class is the default implementation of the IScriptManager interface.
The IBindEvent interface defines the interface that is implemented by the object passed when the script manager's bind_event is fired.
The StartRecordingAction class is a canned PyFace action that starts the recording of changes to scriptable objects to a script.
The StopRecordingAction class is a canned PyFace action that ends the recording of changes to scriptable objects to a script.
The key part of supporting application scripting is to design an appropriate scripting API and to ensure than the application itself uses the API so that changes to the data can be recorded. The framework provides many ways to specify the scripting API. Which approach is appropriate in a particular case will depend on when it is a new application, or whether scripting is being added to an existing application, and how complex the application's data model is.
A scripting API is specified statically by the explicit use of the scriptable decorator and the Scriptable trait wrapper. For example:
from apptools.appscripting.api import scriptable, Scriptable from traits.api import HasTraits, Int, Str class DataModel(HasTraits):
foo = Scriptable(Str)
bar = Scriptable(Int, has_side_effects=True)
@scriptable
def baz(self):
pass
def weeble(self)
pass # Create the scriptable object. It's creation won't be recorded because # __init__() isn't decorated. obj = DataModel() # These will be recorded. obj.foo = '' obj.bar = 10 obj.baz() # This will not be recorded. obj.weeble() # This won't be recorded unless 'f' is passed to something that is # recorded. f = obj.foo # This will be recorded because we set 'has_side_effects'. b = obj.bar
A scripting API can also be specified dynamically. The following example produces a scriptable object with the same scriptable API as above (with the exception that has_side_effects cannot be specified dynamically):
from apptools.appscripting.api import create_scriptable_type from traits.api import HasTraits, Int, Str class DataModel(HasTraits):
foo = Str
bar = Int
def baz(self):
pass
def weeble(self)
pass # Create a scriptable type based on the above. ScriptableDataModel = create_scriptable_type(DataModel, excludes=['weeble']) # Now create scriptable objects from the scriptable type. Note that each # object has the same type. obj1 = ScriptableDataModel() obj2 = ScriptableDataModel()
Instead we could bypass the type and make the objects themselves scriptable as follows:
from apptools.appscripting.api import make_object_scriptable from traits.api import HasTraits, Int, Str class DataModel(HasTraits):
foo = Str
bar = Int
def baz(self):
pass
def weeble(self)
pass # Create unscriptable objects. obj1 = DataModel() obj2 = DataModel() # Now make the objects scriptable. Note that each object has a different # type, each a sub-type of 'DataModel'. make_object_scriptable(obj1, excludes=['weeble']) make_object_scriptable(obj2, excludes=['weeble'])
With a more sophisticated design we may choose to specify the scriptable API as an interface as follows:
from apptools.appscripting.api import make_object_scriptable from traits.api import HasTraits, Int, Interface, Str class DataModel(HasTraits):
foo = Str
bar = Int
def baz(self):
pass
def weeble(self)
pass class IScriptableDataModel(Interface):
foo = Str
bar = Int
def baz(self):
pass # Create an unscriptable object. obj = DataModel() # Now make the object scriptable. make_object_scriptable(obj, api=IScriptableDataModel)
Making a type's __init__() method has advantages and disadvantages. It means that the creation of scriptable objects will be recorded in a script (along with the necessary import statements). This means that the script can be run independently of your application by the standard Python interpreter.
The disadvantage is that, if you have a complex data model, with many interdependencies, then defining a complete and consistent scripting API that allows a script to run independently may prove difficult. In such cases it is better to have the application create and bind the scriptable objects itself.
The Permissions Framework is a component of the Enthought Tool Suite that provides developers with the facility to limit access to parts of an application unless the user is appropriately authorised. In other words it enables and disables different parts of the GUI according to the identity of the user.
The framework includes an API to allow it to be integrated with an organisation's existing security infrastructure, for example to look users up in a corporate LDAP directory.
The framework is completely configurable. Alternate implementations of all major components can be provided if necessary. The default implementations provide a simple local filesystem user database and allows roles to be defined and assigned to users.
The framework does not provide any facility for protecting access to data. It is not possible to implement such protection in Python and using the file security provided by a typical operating system.
The following are the concepts supported by the framework.
A permission is the basic tool that a developer uses to specify that access to a part of the application should be restricted. If the current user has the permission then access is granted. A permission may be attached to a PyFace action, to an item of a TraitsUI view, or to a GUI toolkit specific widget. When the user is denied access, the corresponding GUI control is disabled or completely hidden.
Each application has a current user who is either authorised or unauthorised. In order to become authorised a user must identify themselves and authenticate that identity.
An arbitrary piece of data (called a blob) can be associated with an authorised user which (with user manager support) can be stored securely. This might be used, for example, to store sensitive user preferences, or to implement a roaming profile.
The user manager is responsible for authorising the current user and, therefore, defines how that is done. It also provides information about the user population to the policy manager. It may also, optionally, provide the ability to manage the user population (eg. add or delete users). The user manager must either maintain a persistent record of the user population, or interface with an external user database or directory service.
The default user manager uses password based authorisation.
The user manager persists its data in a user database. The default user manager provides an API so that different implementations of the user database can be used (for example to store the data in an RDBMS, or to integrate with an existing directory service). A default user database is provided that pickles the data in a local file.
The policy manager is responsible for assigning permissions to users and for determining the permissions assigned to the current user. To do this it must maintain a persistent record of those assignments.
The default policy manager supplied with the framework uses roles to make it easier for an administrator to manage the relationships between permissions and users. A role is defined as a named set of permissions, and a user may have one or more roles assigned to them.
The policy manager persists its data in a policy database. The default policy manager provides an API so that different implementations of the policy database can be used (for example to store the data in an RDBMS). A default policy database is provided that pickles the data in a local file.
The permissions manager is a singleton object used to get and set the current policy and user managers.
The APIs provided by the permissions framework can be split into the following groups.
This part of the API is used by application developers.
This is the interface that an alternative policy manager must implement. The need to implement an alternative is expected to be very rare and so the API isn't covered further. See the definition of the IPolicyManager interface for the details.
This part of the API is used by developers to store the policy's persistent data in a more secure location (eg. on a remote server) than that provided by the default implementation.
This is the interface that an alternative user manager must implement. The need to implement an alternative is expected to be very rare and so the API isn't covered further. See the definition of the IUserManager interface for the details.
This part of the API is used by developers to store the user database in a more secure location (eg. on a remote server) than that provided by the default implementation.
The complete API documentation is available as endo generated HTML.
The architecture of the permissions framework comprises several layers, each of which can reimplemented to meet the requirements of a particular environment. Hopefully the following questions and answers will clarify what needs to be reimplemented depending on your environment.
Q: Do you want to use roles to group permissions and assign them to users?
Q: Do you want users to be authenticated using a password?
Q: Do you want to store your user accounts as pickled data in a local file?
The permissions framework will first try to import the different managers from the apptools.permissions.external namespace. The default managers are only used if no alternative was found. Therefore, alternative managers should be deployed as an egg containing that namespace.
Specifically the framework looks for the following classes:
PolicyStorage from apptools.permissions.external.policy_storage
UserDatabase from apptools.permissions.external.user_database
UserManager from apptools.permissions.external.user_manager
UserStorage from apptools.permissions.external.user_storage
The example server is such a package that provides PolicyStorage and UserStorage implementations that use an XML-RPC based server to provide remote (and consequently more secure) policy and user databases.
The default policy and user managers both (again by default) persist their data as pickles in local files called ets_perms_policydb and ets_perms_userdb respectively. By default these are stored in the application's home directory (ie. that returned by ETSConfig.application_home).
Note that this directory is normally in the user's own directory structure whereas it needs to be available to all users of the application.
If the ETS_PERMS_DATA_DIR environment variable is set then its value is used instead.
The directory must be writeable by all users of the application.
It should be restated that the default implementations do not provide secure access to the permissions and user data. They are useful in a cooperative environment and as working examples.
This section provides an overview of the part of the ETS Permissions Framework API used by application developers. The Permissions Framework example demonstrates the API in use. An application typically uses the API to do the following:
A permission is the object that determines the user's access to a part of an application. While it is possible to apply the same permission to more than one part of an application, it is generally a bad idea to do so as it makes it difficult to separate them at a later date.
A permission has an id and a human readable description. Permission ids must be unique. By convention a dotted notation is used for ids to give them a structure. Ids should at least be given an application or plugin specific prefix to ensure their uniqueness.
Conventionally all an applications permissions are defined in a single permissions.py module. The following is an extract of the example's permissions.py module:
from apptools.permissions.api import Permission # Add a new person. NewPersonPerm = Permission(id='ets.permissions.example.person.new',
description=u"Add a new person") # Update a person's age. UpdatePersonAgePerm = Permission(id='ets.permissions.example.person.age.update',
description=u"Update a person's age") # View or update a person's salary. PersonSalaryPerm = Permission(id='ets.permissions.example.person.salary',
description=u"View or update a person's salary")
Permissions are applied to different parts of an applications GUI. When the user has been granted a permission then the corresponding part of the GUI is displayed normally. When the user is denied a permission then the corresponding part of the GUI is disabled or completely hidden.
Permissions can be applied to TraitsUI view items and to any object which can be wrapped in a SecureProxy.
Items in TraitsUI views have enabled_when and visible_when traits that are evaluated to determine if the item should be enabled or visible respectively. These are used to apply permissions by storing the relevant permissions in the model so that they are available to the view. The enabled_when and visible_when traits then simply reference the permission's granted trait. The granted trait automatically reflects whether or not the user currently has the corresponding permission.
In order for the view to be correctly updated when the user's permissions change (ie. when they become authenticated) the view must use the SecureHandler handler. This handler is a simple sub-class of the standard Traits Handler class.
The following extract from the example shows a default view of the Person object that enables the age item when the user has the UpdatePersonAgePerm permission and shows the salary item when the user has the PersonSalaryPerm permission:
from apptools.permissions.api import SecureHandler from traits.api import HasTraits, Int, Unicode from traitsui.api import Item, View from permissions import UpdatePersonAgePerm, PersonSalaryPerm class Person(HasTraits):
"""A simple example of an object model"""
# Name.
name = Unicode
# Age in years.
age = Int
# Salary.
salary = Int
# Define the default view with permissions attached.
age_perm = UpdatePersonAgePerm
salary_perm = PersonSalaryPerm
traits_view = View(
Item(name='name'),
Item(name='age', enabled_when='object.age_perm.granted'),
Item(name='salary', visible_when='object.salary_perm.granted'),
handler=SecureHandler)
Any object can have permissions applied by wrapping it in a SecureProxy object. An adapter is used that manages the enabled and visible states of the proxied object according to the current user's permissions. Otherwise the proxy behaves just like the object being proxied.
Adapters are included for the following types of object:
See Writing SecureProxy Adapters for a description of how to write adapters for other types of objects.
The following extract from the example shows the wrapping of a standard PyFace action and the application of the NewPersonPerm permission:
from apptools.permissions.api import SecureProxy from permissions import NewPersonPerm ...
def _new_person_action_default(self):
"""Trait initializer."""
# Create the action and secure it with the appropriate permission.
act = Action(name='New Person', on_perform=self._new_person)
act = SecureProxy(act, permissions=[NewPersonPerm])
return act
A SecureProxy also accepts a show argument that, when set to False, hides the object when it becomes disabled.
The user manager supports the concept of the current user and is responsible for authenticating the user (and subsequently unauthorising the user if required).
The code fragment to authenticate the current user is:
from apptools.permissions.api import get_permissions_manager get_permissions_Manager().user_manager.authenticate_user()
Unauthorising the current user is done using the unauthenticate_user() method.
As a convenience two PyFace actions, called LoginAction and LogoutAction, are provided that wrap these two methods.
As a further convenience a PyFace menu manager, called UserMenuManager, is provided that contains all the user and management actions (see below) in the permissions framework. This is used by the example.
The user menu, login and logout actions can be imported from apptools.permissions.action.api.
The user manager has a user trait that is an object that implements the IUser interface. It is only valid once the user has been authenticated.
The IUser interface has a blob trait that holds any binary data (as a Python string). The data will be read when the user is authenticated. The data will be written whenever it is changed.
Both policy and user managers can provide actions that provide access to various management functions. Both have a management_actions trait that is a list of PyFace actions that invoke appropriate dialogs that allow the user to manage the policy and the user population appropriately.
User managers also have a user_actions trait that is a list of PyFace actions that invoke appropriate dialogs that allow the user to manage themselves. For example, the default user manager provides an action that allows a user to change their password.
The default policy manager provides actions that allows roles to be defined in terms of sets of permissions, and allows users to be assigned one or more roles.
The default user manager provides actions that allows users to be added, modified and deleted. A user manager that integrates with an enterprise's secure directory service may not provide any management actions.
All management actions have appropriate permissions attached to them.
SecureProxy will automatically handle most of the object types you will want to apply permissions to. However it is possible to implement additional adapters to support other object types. To do this you need to implement a sub-class of AdapterBase and register it.
Adapters tend to be one of two styles according to how the object's enabled and visible states are changed. If the states are changed via attributes (typically Traits based objects) then the adapter will cause a proxy to be created for the object. If the states are changed via methods (typically toolkit widgets) then the adapter will probably modify the object itself. We will refer to these two styles as wrapping adapters and patching adapters respectively.
The following gives a brief overview of the AdapterBase class:
The AdapterBase class is defined in adapter_base.py.
The PyFace action adapter is an example of a wrapping adapter.
The PyQt widget adapter is an example of a patching adapter.
This section provides an overview of the part of the ETS Permissions Framework API used by developers who want to store a policy manager's persistent data in a more secure location (eg. a remote server) than that provided by the default implementation.
The API is defined by the default policy manager which uses roles to make it easier to assign permissions to users. If this API isn't sufficiently flexible, or if roles are inappropriate, then an alternative policy manager should be implemented.
The API is fully defined by the IPolicyStorage interface. The default implementation of this interface stores the policy database as a pickle in a local file.
The IPolicyStorage interface defines a number of methods that must be implemented to read and write to the policy database. The methods are designed to be implemented using simple SQL statements.
In the event of an error a method must raise the PolicyStorageError exception. The string representation of the exception is used as an error message that is displayed to the user.
This section provides an overview of the part of the ETS Permissions Framework API used by developers who want to store a user database in a more secure location (eg. a remote server) than that provided by the default implementation.
The API is defined by the default user manager which uses password based authorisation. If this API isn't sufficiently flexible, or if another method of authorisation is used (biometrics for example) then an alternative user manager should be implemented.
The API is fully defined by the IUserDatabase interface. This allows user databases to be implemented that extend the IUser interface and store additional user related data. If the user database is being persisted in secure storage (eg. a remote RDBMS) then this could be used to store sensitive data (eg. passwords for external systems) that shouldn't be stored as ordinary preferences.
In most cases there will be no requirement to store additional user related data than that defined by IUser so the supplied UserDatabase implementation (which provides all the GUI code required to implement the IUserDatabase interface) can be used. The UserDatabase implementation delegates the access to the user database to an object implementing the IUserStorage interface. The default implementation of this interface stores the user database as a pickle in a local file.
The IUserStorage interface defines a number of methods that must be implemented to read and write to the user database. The methods are designed to be implemented using simple SQL statements.
In the event of an error a method must raise the UserStorageError exception. The string representation of the exception is used as an error message that is displayed to the user.
The IUserDatabase interface defines a set of Bool traits, all beginning with can_, that describe the capabilities of a particular implementation. For example, the can_add_user trait is set by an implementation if it supports the ability to add a new user to the database.
Each of these capability traits has a corresponding method which has the same name except for the can_ prefix. The method only needs to be implemented if the corresponding traits is True. The method, for example add_user() is called by the user manager to implement the capability.
The interface has two other methods.
The bootstrapping() method is called by the user manager to determine if the database is bootstrapping. Typically this is when the database is empty and no users have yet been defined. The permissions framework treats this situation as a special case and is able to relax the enforcement of permissions to allow users and permissions to be initially defined.
The user_factory() method is called by the user manager to create a new user object, ie. an object that implements the IUser interface. This allows an implementation to extend the IUser interface and store additional user related data in the object if the blob trait proves insufficient.
The preferences package provides a simple API for managing application preferences. The classes in the package are implemented using a layered approach where the lowest layer provides access to the raw preferences mechanism and each layer on top providing more convenient ways to get and set preference values.
Lets start by taking a look at the lowest layer which consists of the IPreferences interface and its default implementation in the Preferences class. This layer implements the basic preferences system which is a hierarchical arrangement of preferences 'nodes' (where each node is simply an object that implements the IPreferences interface). Nodes in the hierarchy can contain preference settings and/or child nodes. This layer also provides a default way to read and write preferences from the filesystem using the excellent ConfigObj package.
This all sounds a bit complicated but, believe me, it isn't! To prove it (hopefully) lets look at an example. Say I have the following preferences in a file 'example.ini':
[acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True [acme.ui.splash_screen] image = splash fgcolor = red
I can create a preferences hierarchy from this file by:
>>> from apptools.preferences.api import Preferences >>> preferences = Preferences(filename='example.ini') >>> preferences.dump()
Node() {}
Node(acme) {}
Node(ui) {'bgcolor': 'blue', 'ratio': '1.0', 'width': '50', 'visible': 'True'}
Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'}
The 'dump' method (useful for debugging etc) simply 'pretty prints' a preferences hierarchy. The dictionary next to each node contains the node's actual preferences. In this case, the root node (the node with no name) is the preferences object that we created. This node now has one child node 'acme', which contains no preferences. The 'acme' node has one child, 'ui', which contains some preferences (e.g. 'bgcolor') and also a child node 'splash_screen' which also contains preferences (e.g. 'image').
To look up a preference we use:
>>> preferences.get('acme.ui.bgcolor') 'blue'
If no such preferences exists then, by default, None is returned:
>>> preferences.get('acme.ui.bogus') is None True
You can also specify an explicit default value:
>>> preferences.get('acme.ui.bogus', 'fred') 'fred'
To set a preference we use:
>>> preferences.set('acme.ui.bgcolor', 'red') >>> preferences.get('acme.ui.bgcolor') 'red'
And to make sure the preferences are saved back to disk:
>>> preferences.flush()
To add a new preference value we simply set it:
>>> preferences.set('acme.ui.fgcolor', 'black') >>> preferences.get('acme.ui.fgcolor') 'black'
Any missing nodes in a call to 'set' are created automatically, hence:
>>> preferences.set('acme.ui.button.fgcolor', 'white') >>> preferences.get('acme.ui.button.fgcolor') 'white'
Preferences can also be 'inherited'. e.g. Notice that the 'splash_screen' node does not contain a 'bgcolor' preference, and hence:
>>> preferences.get('acme.ui.splash_screen.bgcolor') is None True
But if we allow the 'inheritance' of preference values then:
>>> preferences.get('acme.ui.splash_screen.bgcolor', inherit=True) 'red'
By using 'inheritance' here the preferences system will try the following preferences:
'acme.ui.splash_screen.bgcolor' 'acme.ui.bgcolor' 'acme.bgcolor' 'bgcolor'
At this point it is worth mentioning that preferences are always stored and returned as strings. This is because of the limitations of the traditional '.ini' file format i.e. they don't contain any type information! Now before you start panicking, this doesn't mean that all of your preferences have to be strings! Currently the preferences system allows, strings(!), booleans, ints, longs, floats and complex numbers. When you store a non-string value it gets converted to a string for you, but you always get a string back:
>>> preferences.get('acme.ui.width') '50' >>> preferences.set('acme.ui.width', 100) >>> preferences.get('acme.ui.width') '100' >>> preferences.get('acme.ui.visible') 'True' >>> preferences.set('acme.ui.visible', False) >>> preferences.get('acme.ui.visible') 'False'
This is obviously not terribly convenient, and so the following section discusses how we associate type information with our preferences to make getting and setting them more natural.
As mentioned previously, we would like to be able to get and set non-string preferences in a more convenient way. This is where the PreferencesHelper class comes in.
Let's take another look at 'example.ini':
[acme.ui] bgcolor = blue width = 50 ratio = 1.0 visible = True [acme.ui.splash_screen] image = splash fgcolor = red
Say, I am interested in the preferences in the 'acme.ui' section. I can use a preferences helper as follows:
from apptools.preferences.api import PreferencesHelper class SplashScreenPreferences(PreferencesHelper):
""" A preferences helper for the splash screen. """
PREFERENCES_PATH = 'acme.ui'
bgcolor = Str
width = Int
ratio = Float
visible = Bool >>> preferences = Preferences(filename='example.ini') >>> helper = SplashScreenPreferences(preferences=preferences) >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True
And, obviously, I can set the value of the preferences via the helper too:
>>> helper.ratio = 0.5
And if you want to prove to yourself it really did set the preference:
>>> preferences.get('acme.ui.ratio') '0.5'
Using a preferences helper you also get notified via the usual trait mechanism when the preferences are changed (either via the helper or via the preferences node directly:
def listener(obj, trait_name, old, new):
print trait_name, old, new >>> helper.on_trait_change(listener) >>> helper.ratio = 0.75 ratio 0.5 0.75 >>> preferences.set('acme.ui.ratio', 0.33) ratio 0.75 0.33
If you always use the same preference node as the root of your preferences you can also set the class attribute 'PreferencesHelper.preferences' to be that node and from then on in, you don't have to pass a preferences collection in each time you create a helper:
>>> PreferencesHelper.preferences = Preferences(filename='example.ini') >>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True
In many applications the idea of preferences scopes is useful. In a scoped system, an actual preference value can be stored in any scope and when a call is made to the 'get' method the scopes are searched in order of precedence.
The default implementation (in the ScopedPreferences class) provides two scopes by default:
This scope stores itself in the 'ETSConfig.application_home' directory. This scope is generally used when setting any user preferences.
This scope is transient (i.e. it does not store itself anywhere). This scope is generally used to load any predefined default values into the preferences system.
If you are happy with the default arrangement, then using the scoped preferences is just like using the plain old non-scoped version:
>>> from apptools.preferences.api import ScopedPreferences >>> preferences = ScopedPreferences(filename='example.ini') >>> preferences.load('example.ini') >>> p.dump()
Node() {}
Node(application) {}
Node(acme) {}
Node(ui) {'bgcolor': 'blue', 'ratio': '1.0', 'width': '50', 'visible': 'True'}
Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'}
Node(default) {}
Here you can see that the root node now has a child node representing each scope.
When we are getting and setting preferences using scopes we generally want the following behaviour:
a) When we get a preference we want to look it up in each scope in order. The first scope that contains a value 'wins'.
b) When we set a preference, we want to set it in the first scope. By default this means that when we set a preference it will be set in the application scope. This is exactly what we want as the application scope is the scope that is persistent.
So usually, we just use the scoped preferences as before:
>>> preferences.get('acme.ui.bgcolor') 'blue' >>> preferences.set('acme.ui.bgcolor', 'red') >>> preferences.dump()
Node() {}
Node(application) {}
Node(acme) {}
Node(ui) {'bgcolor': 'red', 'ratio': '1.0', 'width': '50', 'visible': 'True'}
Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'}
Node(default) {}
And, conveniently, preference helpers work just the same with scoped preferences too:
>>> PreferencesHelper.preferences = ScopedPreferences(filename='example.ini') >>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True
Should you care about getting or setting a preference in a particular scope then you use the following syntax:
>>> preferences.set('default/acme.ui.bgcolor', 'red') >>> preferences.get('default/acme.ui.bgcolor') 'red' >>> preferences.dump()
Node() {}
Node(application) {}
Node(acme) {}
Node(ui) {'bgcolor': 'red', 'ratio': '1.0', 'width': '50', 'visible': 'True'}
Node(splash_screen) {'image': 'splash', 'fgcolor': 'red'}
Node(default) {}
Node(acme) {}
Node(ui) {'bgcolor': 'red'}
You can also get hold of a scope via:
>>> default = preferences.get_scope('default')
And then perform any of the usual operations on it.
So that's a quick tour around the basic useage of the preferences API. For more imformation about what is provided take a look at the API documentation.
If you are using Envisage to build your applications then you might also be interested in the Preferences in Envisage section.
This section discusses how an Envisage application uses the preferences mechanism. Envisage tries not to dictate too much, and so this describes the default behaviour, but you are free to override it as desired.
Envisage uses the default implementation of the ScopedPreferences class which is made available via the application's 'preferences' trait:
>>> application = Application(id='myapplication') >>> application.preferences.set('acme.ui.bgcolor', 'yellow') >>> application.preferences.get('acme.ui.bgcolor') 'yellow'
Hence, you use the Envisage preferences just like you would any other scoped preferences.
It also registers itself as the default preferences node used by the PreferencesHelper class. Hence you don't need to provide a preferences node explicitly to your helper:
>>> helper = SplashScreenPreferences() >>> helper.bgcolor 'blue' >>> helper.width 100 >>> helper.ratio 1.0 >>> helper.visible True
The only extra thing that Envisage does for you is to provide an extension point that allows you to contribute any number of '.ini' files that are loaded into the default scope when the application is started.
e.g. To contribute a preference file for my plugin I might use:
class MyPlugin(Plugin):
...
@contributes_to('envisage.preferences')
def get_preferences(self, application):
return ['pkgfile://mypackage:preferences.ini']
This package provides a very handy and powerful Python script recording facility. This can be used to:
This package is not just a toy framework and is powerful enough to provide full script recording to the Mayavi application. Mayavi is a powerful 3D visualization tool that is part of ETS.
The scripting API primarily allows you to record UI actions for objects that have Traits. Technically the framework listens to all trait changes so will work outside a UI. We do not document the full API here, the best place to look for that is the apptools.scripting.recorder module which is reasonably well documented. We provide a high level overview of the library.
The quickest way to get started is to look at a small example.
The following example is taken from the test suite. Consider a set of simple objects organized in a hierarchy:
from traits.api import (HasTraits, Float, Instance,
Str, List, Bool, HasStrictTraits, Tuple, Range, TraitPrefixMap,
Trait) from apptools.scripting.api import (Recorder, recordable,
set_recorder) class Property(HasStrictTraits):
color = Tuple(Range(0.0, 1.0), Range(0.0, 1.0), Range(0.0, 1.0))
opacity = Range(0.0, 1.0, 1.0)
representation = Trait('surface',
TraitPrefixMap({'surface':2,
'wireframe': 1,
'points': 0})) class Toy(HasTraits):
color = Str
type = Str
# Note the use of the trait metadata to ignore this trait.
ignore = Bool(False, record=False) class Child(HasTraits):
name = Str('child')
age = Float(10.0)
# The recorder walks through sub-instances if they are marked
# with record=True
property = Instance(Property, (), record=True)
toy = Instance(Toy, record=True)
friends = List(Str)
# The decorator records the method.
@recordable
def grow(self, x):
"""Increase age by x years."""
self.age += x class Parent(HasTraits):
children = List(Child, record=True)
recorder = Instance(Recorder, record=False)
Using these simple classes we first create a simple object hierarchy as follows:
p = Parent() c = Child() t = Toy() c.toy = t p.children.append(c)
Given this hierarchy, we'd like to be able to record a script. To do this we setup the recording infrastructure:
from mayavi.core.recorder import Recorder, set_recorder # Create a recorder. r = Recorder() # Set the global recorder so the decorator works. set_recorder(r) r.register(p) r.recording = True
The key method here is the r.register(p) call above. It looks at the traits of p and finds all traits and nested objects that specify a record=True in their trait metadata (all methods starting and ending with _ are ignored). All sub-objects are in turn registered with the recorder and so on. Callbacks are attached to traits changes and these are wired up to produce readable and executable code. The set_recorder(r) call is also very important and sets the global recorder so the framework listens to any functions that are decorated with the recordable decorator.
Now lets test this out like so:
# The following will be recorded. c.name = 'Shiva' c.property.representation = 'w' c.property.opacity = 0.4 c.grow(1)
To see what's been recorded do this:
print r.script
This prints:
child = parent.children[0] child.name = 'Shiva' child.property.representation = 'wireframe' child.property.opacity = 0.40000000000000002 child.grow(1)
The recorder internally maintains a mapping between objects and unique names for each object. It also stores the information about the location of a particular object in the object hierarchy. For example, the path to the Toy instance in the hierarchy above is parent.children[0].toy. Since scripting with lists this way can be tedious, the recorder first instantiates the child:
child = parent.children[0]
Subsequent lines use the child attribute. The recorder always tries to instantiate the object referred to using its path information in this manner.
To record a function or method call one must simply decorate the function/method with the recordable decorator. Nested recordable functions are not recorded and trait changes are also not recorded if done inside a recordable function.
NOTE:
To stop recording do this:
r.unregister(p) r.recording = False
The r.unregister(p) reverses the r.register(p) call and unregisters all nested objects as well.
Here are a few advanced use cases.
For more details on the recorder itself we suggest reading the module source code. It is fairly well documented and with the above background should be enough to get you going.
The Undo Framework is a component of the Enthought Tool Suite that provides developers with an API that implements the standard pattern for do/undo/redo commands.
The framework is completely configurable. Alternate implementations of all major components can be provided if necessary.
The following are the concepts supported by the framework.
A command is an application defined operation that can be done (i.e. executed), undone (i.e. reverted) and redone (i.e. repeated).
A command operates on some data and maintains sufficient state to allow it to revert or repeat a change to the data.
Commands may be merged so that potentially long sequences of similar commands (e.g. to add a character to some text) can be collapsed into a single command (e.g. to add a word to some text).
A macro is a sequence of commands that is treated as a single command when being undone or redone.
A command is done by pushing it onto a command stack. The last command can be undone and redone by calling appropriate command stack methods. It is also possible to move the stack's position to any point and the command stack will ensure that commands are undone or redone as required.
A command stack maintains a clean state which is updated as commands are done and undone. It may be explicitly set, for example when the data being manipulated by the commands is saved to disk.
Canned PyFace actions are provided as wrappers around command stack methods to implement common menu items.
An undo manager is responsible for one or more command stacks and maintains a reference to the currently active stack. It provides convenience undo and redo methods that operate on the currently active stack.
An undo manager ensures that each command execution is allocated a unique sequence number, irrespective of which command stack it is pushed to. Using this it is possible to synchronise multiple command stacks and restore them to a particular point in time.
An undo manager will generate an event whenever the clean state of the active stack changes. This can be used to maintain some sort of GUI status indicator to tell the user that their data has been modified since it was last saved.
Typically an application will have one undo manager and one undo stack for each data type that can be edited. However this is not a requirement: how the command stack's in particular are organised and linked (with the user manager's sequence number) can need careful thought so as not to confuse the user - particularly in a plugin based application that may have many editors.
To support this typical usage the PyFace Workbench class has an undo_manager trait and the PyFace Editor class has a command_stack trait. Both are lazy loaded so can be completely ignored if they are not used.
This section gives a brief overview of the various classes implemented in the framework. The complete API documentation is available as endo generated HTML.
The example application demonstrates all the major features of the framework.
The UndoManager class is the default implementation of the IUndoManager interface.
The CommandStack class is the default implementation of the ICommandStack interface.
The ICommand interface defines the interface that must be implemented by any undoable/redoable command.
AbstractCommand is an abstract base class that implements the ICommand interface. It provides a default implementation of the merge() method.
The CommandAction class is a sub-class of the PyFace Action class that is used to wrap commands.
The UndoAction class is a canned PyFace action that undoes the last command of the active command stack.
The RedoAction class is a canned PyFace action that redoes the last command undone of the active command stack.
It is quite common in GUI applications to have a UI element displaying a collection of items that a user can select ("selection providers"), while other parts of the application must react to changes in the selection ("selection listeners").
Ideally, the listeners would not have a direct dependency on the UI object. This is especially important in extensible envisage applications, where a plugin might need to react to a selection change, but we do not want to expose the internal organization of the application to external developers.
This package defines a selection service that manages the communication between providers and listener.
The SelectionService object is the central manager that handles the communication between selection providers and listener.
Selection providers are components that wish to publish information about their current selection for public consumption. They register to a selection service instance when they first have a selection available (e.g., when the UI showing a list of selectable items is initialized), and un-register as soon as the selection is not available anymore (e.g., the UI is destroyed when the windows is closed).
Selection listeners can query the selection service to get the current selection published by a provider, using the provider unique ID.
The service acts as a broker between providers and listeners, making sure that they are notified when the selection event is fired.
Any object can become a selection provider by implementing the ISelectionProvider interface, and registering to the selection service.
Selection providers must provide a unique ID provider_id, which is used by listeners to request its current selection.
Whenever its selection changes, providers fire a selection event. The content of the event is an instance implementing ISelection that contains information about the selected items. For example, a ListSelection object contains a list of selected items, and their indices.
Selection providers can also be queried directly about their current selection using the get_selection method, and can be requested to change their selection to a new one with the set_selection method.
Selection providers publish their selection by registering to the selection service using the add_selection_provider method. When the selection is no longer available, selection providers should un-register through remove_selection_provider.
Typically, selection providers are UI objects showing a list or tree of items, they register as soon as the UI component is initialized, and un-register when the UI component disappears (e.g., because their window has been closed). In more complex applications, the registration could be done by a controller object instead.
Selection listeners request information regarding the current selection of a selection provider given their provider ID. The SelectionService supports two distinct use cases:
Listeners connect to the selection events for a given provider using the connect_selection_listener method. They need to provide the unique ID of the provider, and a function (or callable) that is called to send the event. This callback function takes one argument, an implementation of the ISelection that represents the selection.
It is possible for a listener to connect to a provider ID before it is registered. As soon as the provider is registered, the listener will receive a notification containing the provider's initial selection.
To disconnect a listener use the methods disconnect_selection_listener.
In other instances, an element of the application only needs the current selection at a specific time. For example, a toolbar button could open dialog representing a user action based on what is currently selected in the active editor.
The get_selection method calls the corresponding method on the provider with the given ID and returns an ISelection instance.
Finally, it is possible to request a provider to set its selection to a given set of objects with set_selection. The main use case for this method is multiple views of the same list of objects, which need to keep their selection synchronized.
If the items specified in the arguments are not available in the provider, a ProviderNotRegisteredError is raised, unless the optional keyword argument ignore_missing is set to True.
Users of the apptools.selection package can access the objects that are part of the public API through the convenience apptools.selection.api.
The selection service connects selection providers and listeners.
The selection service is a register of selection providers, i.e., objects that publish their current selection.
Selections can be requested actively, by explicitly requesting the current selection in a provider (get_selection(id)()), or passively by connecting selection listeners.
The provider is identified by its ID. If a provider with the same ID has been already registered, an IDConflictError is raised.
The signature if the listener callback is func(i_selection). The listener is called:
It is perfectly valid to connect a listener before a provider with the given ID is registered. The listener will remain connected even if the provider is repeatedly connected and disconnected.
If a provider with that ID has not been registered, a ProviderNotRegisteredError is raised.
If the provider has not been registered, a ProviderNotRegisteredError is raised.
If a provider with the given ID has not been registered, a ProviderNotRegisteredError is raised.
If ignore_missing is True, items that are not available in the selection provider are silently ignored. If it is False (default), a ValueError should be raised.
Source of selections.
If ignore_missing is True, items that are not available in the selection provider are silently ignored. If it is False (default), an ValueError should be raised.
Selection for ordered sequences of items.
Collection of selected items.
Selection for ordered sequences of items.
This is the default implementation of the IListSelection interface.
Fills in the required information (in particular, the indices) based on a list of selected items and a list of all available items.
NOTE:
Raised when a provider is added and its ID is already registered.
Raised when a listener that was never connected is disconnected.
Raised when a provider is requested by ID and not found.
Enthought
2008-2020, Enthought
November 30, 2020 | 4.5.0 |