You are not logged in Log in Join
You are here: Home » Members » Phillip J. Eby's Zope Center » My Wikis » ZPatterns Wiki » SkinScriptSyntax

Log in
Name

Password

 
 
HomePage » SkinScript »

SkinScriptSyntax

The SkinScript Language Reference

Preface

This document describes the syntax for SkinScript as defined in ZPatterns 0.4.3, which is not yet publically available. If you are using an earlier version of ZPatterns, the following features are not available:

  • The QUERY keyword and the OTHERWISE LET clause on WITH/COMPUTE declarations
  • The INITIALIZE OBJECT WITH declaration
  • The DEPENDENT ON clause of WITH/COMPUTE declarations

It is also recommended that you read about HowTriggersWork before trying to define complex rules with SkinScript.

Basic Concepts and Syntax

If you're familiar with ZPatterns, you know that DataSkins get their attributes and behavior from AttributeProviders and RuleAgents, respectively. Providers and Agents (known as DataPlugIns) can be written directly in Python, but it is much easier to write them in SkinScript. SkinScript lets you define an individual attribute provider or rule agent in a single declarative statement, using standard Zope tools such as SQL Methods, DTML Methods, Python Methods, and so on to do the actual data retrieval or storage. In effect, SkinScript is a "glue language" which lets you define the linkages between a DataSkin and the methods you want to implement its "skeleton" of data storage and triggered behavior.

In most languages, the order that statements appear in makes a difference to the results you get, and SkinScript is no exception. Although each SkinScript statement defines a seperate and independent data plug-in, the ordering makes a difference in how they will be used by DataSkins. When a DataSkin needs to perform an operation (such as retrieving an attribute), it asks its DataManager for a list of suitable DataPlugIns. This list is ordered according to the original order of plug-ins as listed on the Data Plug-ins tab of the Racks or Customizers involved. And the declarations of a SkinScript Method are treated as though they were individual plug-ins appearing in place of the SkinScript Method in that list.

In other words, SkinScript is literally a language for defining data plug-ins. A SkinScript script consists of a series of declarations, seperated by whitespace. Each declaration is compiled into a single data plug-in. SkinScript is a keyword-driven language, is case-sensitive, and is not whitespace sensitive, even for Python expressions contained within declarations. All whitespace which occurs outside of quoted strings is treated as though it were a single space. (Incidentally, this means that the usual Python rules about where you can put linebreaks in expressions do not apply. You can write the expression 1+2 split across three lines, if you so desire.) Comments are marked as in Python, using a # symbol to mean that the rest of the current line is a comment. Comments are treated as simple whitespace.

Declarations

Declarations are the basic building block of SkinScript. Declarations are compiled into AttributeProviders or RuleAgents, one per declaration. Some declarations provide attribute values:

  • INITIALIZE OBJECT WITH assignmentlist
  • 'WITH [QUERY]' expression COMPUTE nameorassignlist [OTHERWISE LET assignmentlist] [DEPENDENT ON dependencies]
  • WITH SELF COMPUTE assignmentlist

Some handle attribute storage:

  • [WHEN eventspec] STORE attributelist USING expression [SAVING mementolist]
  • STORE attributelist IN SELF

And others can call an expression upon transaction commit:

  • WHEN eventspec CALL expression [SAVING mementolist]

Each declaration has its own parameters, but there are certain conventions which are followed across most kinds of declarations.

Declaration Parameters

expression
A DTML-style Python expression. As with DTML, the "_" object is available for access to Python built-ins and library functions. Please see the section below on "Variables and Functions Available in Expressions" for details on how names other than "_" are looked up in SkinScript expressions.
assignmentlist
A comma-seperated list of assignments in the form attributename = expression, similar to passing keyword arguments to a function or method.
nameorassignlist
Similar to assignmentlist, but with a special shorthand for the case where attributename and expression are the same (e.g. foo=foo). To make such an assignment, you can replace attributename = expression with just attributename. So foo=bar,baz=baz,bada=bing can be simplified to foo=bar,baz,bada=bing if a clause's syntax allows a nameorassignmentlist.
mementolist
A mementolist is syntactically identical to a nameorassignlist, but has a different context and purpose. A mementolist is used to save old values of expressions for later comparison purposes when a (sub)transaction commits, and appears only in SAVING clauses. Expression variables are looked up in the context of the DataSkin whose snapshot is being taken. So in the clause SAVING bar,foo=baz the DataSkin?'s bar attribute will be saved as OLD['bar'], and its baz attribute will be saved as OLD['foo'].

A mementolist is computed only once per (sub)transaction for a given DataSkin. The first time the DataSkin is changed in a (sub)transaction, the SAVING clause is executed for all declarations that have one, even if the declaration never ends up firing.

eventspec
A clause of the form OBJECT ADDED, CHANGED, DELETED, where the ADDED, CHANGED and DELETED keywords may be used in any combination. For example, OBJECT ADDED,DELETED and OBJECT CHANGED are both valid eventspec clauses. Please see HowTriggersWork for details on how ZPatterns events are interpreted by SkinScript.
attributelist
A comma-seperated list of attribute names, to which the declaration will be applied. An asterisk ("*") may be used as a wildcard attribute name, meaning that the declaration will be applicable for any attribute name. Remember, however, that declarations associated with specific names will take precedence over wildcard declarations, even if the wildcard declaration comes before the specific declaration and would match the name being looked up.
dependencies
A comma-seperated list of attribute names which, when changed, will cause the dependent attributes to be recalculated on their next access. Wildcards cannot be used, only actual attribute names.

Variables and Functions Available in Expressions

Names used in an expression are usually looked up from the attributes of individual SkinScript statement and its acquisition parents (e.g. the Rack or Customizer which it's contained in, and so on up the line). One exception to this rule is in the COMPUTE clause of a WITH/COMPUTE declaration, where names are first looked up in the RESULT object returned by the WITH clause. The other exception is in SAVING clauses, where names are looked up in the context of the DataSkin whose snapshot is being taken.

The following variable names and functions are provided by SkinScript for use in expressions.

  • Generally available:
    self
    The DataSkin instance which the declaration is being applied to at the time the expression is being called.
    NOT_FOUND
    A special value which causes an attribute to be non-existent, if this is the value provided. In a WITH/COMPUTE declaration, if the WITH expression returns NOT_FOUND, the COMPUTE clause is ignored, and the OTHERWISE LET clause is activated if one exists.
  • Available for WITH/COMPUTE declarations:
    RESULT
    The result of the WITH expression in a WITH/COMPUTE declaration. Available only from expressions in the COMPUTE clause. This variable is placed atop the DTML namespace stack during execution of the COMPUTE clause, so just referring to a name in a COMPUTE expression will look up that name in the RESULT object first. Only if it is not found there, will the search continue to the declaration and its acquisition context.
    ATTRIBUTE_NAME
    A string containing the name of the attribute which the DataSkin is currently trying to retrieve. Available in both the WITH and COMPUTE clauses.
  • Available for WHEN/STORE and WHEN/CALL declarations:
    TRIGGER_EVENT
    A string, either "ADD", "CHANGE", or "DELETE", denoting the type of event which caused the expression to be executed.
    'OLD["name"]'
    OLD is a mapping object containing the values saved by the "SAVING mementolist" clause of a WHEN/STORE or WHEN/CALL declaration.
    HAS_CHANGED("name")
    returns a true value if the attribute named "name" has been changed or deleted during the current (sub)transaction.
    CHANGED_ATTRS()
    returns a list of the names of attributes which have been changed or deleted during the current (sub)transaction.
    'ORIGINAL["name"]'
    returns the value of the attribute named "name" before it was changed or deleted, or the NOT_FOUND value if the attribute did not exist before this (sub)transaction. A KeyError will be raised if "name" was not changed/deleted, so use HAS_CHANGED() to check before using this.

INITIALIZE OBJECT WITH assignmentlist

What It's For

Providing default attribute values to newly created objects.

How It Works

This declaration assigns the specified attribute values to the object when it is created. Due to limitations of the built-in Zope event model, it is not possible to unequivocally determine when DataSkins outside of Racks are created, so this declaration applies only to objects created inside Racks. The assignmentlist is executed in context of the DataSkin, so later assignments can reference the values of earlier assignments.

Examples

  • Constant values:
         INITIALIZE OBJECT WITH foo=1, bar='baz', degrees=360,
           radians = degrees / (180 / _.math.pi)
    
  • Defaults from properties in acquisition context:
         INITIALIZE OBJECT WITH foo=myfoo, bar=mybar
    

WITH [QUERY] expression COMPUTE nameorassignlist [OTHERWISE LET assignmentlist] [DEPENDENT ON dependencies]

What It's For

Providing readable values for computed attributes and/or attributes which are loaded from an external data source (e.g. via an LDAP or SQL query). WITH/COMPUTE statements minimize redundant calculations by determining some attribute values before they are actually needed, as soon as one of a set of related values are needed. This is especially important where database retrievals are involved. It would be very bad to have to issue an SQL query for each attribute access, even if the result was then cached. So a WITH/COMPUTE statement executes the expensive expression once, and then caches the attributes computed by the nameorassignlist. Notice, by the way, that this means you can do performance tuning by changing the grouping of attributes in your WITH/COMPUTE statements, so that computations which are very expensive and infrequently used are placed in standalone statements, and so on. And if you're using data from outer-joined SQL tables, you may even decide to split them into seperate queries (one per table) and corresponding WITH/COMPUTE statements, if some of the tables' data is rarely used.

How It Works

When an attribute defined in the "nameorassignlist" is requested, call "expression". If the result of expression (available as RESULT in the "nameorassignlist" expressions) is not NOT_FOUND, compute all attributes from nameorassignlist, in the order they are listed, caching the results for the remainder of the (sub)transaction (unless later reset by a change to a DEPENDENT ON attribute).

If the RESULT is NOT_FOUND, the search for the attribute value falls through to the next declaration (or attribute provider if the SkinScript is finished). If there is an OTHERWISE LET clause, the assignments given there are computed and cached for the remainder of the (sub)transaction. (This is to prevent repeated execution of "expression" that will only result in further failures.)

The optional QUERY keyword states that "expression" is a query method such as an LDAP or SQL query that returns a sequence of objects. When the QUERY keyword is used, the declaration automatically takes the first item of the sequence to be used as RESULT, unless the sequence is empty, in which case it behaves as though the RESULT were NOT_FOUND.

The optional DEPENDENT ON clause declares that the computed attributes depend on the listed attributes for their values, and that if any of the DEPENDENT ON attributes are overwritten, the cached values should be reset. (And thus recomputed the next time they are accessed.) So, if for example you had a ZODB-stored field which was a key to be looked up in an RDBMS, listing the key field in the DEPENDENT ON clause would ensure that any access to the RDBMS-stored fields after a change in key would return data from the correct record. (Note, however, that the attributes are only reset if a DEPENDENT ON attribute is changed by being written to. A change in the value calculated or retrieved by another statement or provider has no effect on the attribute cache.)

Examples

  • Simple SQL query. SomeSQLMethod is an SQL Method (located somewhere in the acquisition context of this SkinScript method) that takes someparam as a parameter to return a row by primary key, and returns a result which contains the fields foo, bar, and baz, which we want to use as attributes of the same names:
        WITH QUERY SomeSQLMethod(someparam=self.id) COMPUTE foo,bar,baz
    
  • SQL query with renaming and computations. Again, notice how the WITH expression is computed in the acquisition context of the SkinScript method itself, but the COMPUTE expressions are in the context of the result of the WITH expression:
        WITH QUERY GetRoomData(roomname=self.id) COMPUTE
          labor_cost=labor_rate*labor_hours, material_cost=materials,
          height=roomdim1, width=roomdim2
    
  • A simple computed attribute. The formula will be computed the first time the DataSkin needs to know its total_cost attribute in a given (sub)transaction, and again if the labor_cost or material_cost attributes are written to:
        WITH self.labor_cost + self.material_cost COMPUTE total_cost = RESULT
        DEPENDENT ON labor_cost,material_cost
    
  • Interdependent computed attributes. The first time that area, square_yards, or price_per_sq_yd is asked for in a (sub)transaction, all three will be computed (and recomputed if the non-calculated attributes are written to):
        WITH self.width * self.height COMPUTE
          area = RESULT,
          square_yards = self.area / 9,
          price_per_sq_yd = self.total_cost / self.square_yards
    
        # We have to list 'real' attributes, not intermediate values!
        DEPENDENT ON  width,height,labor_cost,material_cost
    
  • Object re-mapping; convert data from some object in another part of the application. Notice that we can even save a reference to the original object so we can call methods on it later to save our changed attributes back into it... (And of course, we can also seperate more expensive and less-often used attribute computations into other WITH self.original_object COMPUTE statements):
        WITH SomeSpecialist.getItem(self.id) COMPUTE
          my_foo=its_foo, my_bar=its_bar, original_object=RESULT
    

WITH SELF COMPUTE nameorassignlist [DEPENDENT ON dependencies]

What It's For

A shorthand way of defining computed attributes. This variant form of WITH/COMPUTE simply treats self as RESULT, placing it on the namespace stack for the execution of the nameorassignlist.

How It Works

When an attribute defined in the "nameorassignlist" is requested, compute all attributes from nameorassignlist, in the order they are listed, caching the results for the remainder of the (sub)transaction (unless reset by a change to a DEPENDENT ON attribute). The DataSkin itself is placed on the namespace stack during execution of the nameorassignlist, so expressions can refer to other attributes without having to prefix them with self.. WITH SELF COMPUTE executes slightly faster than an otherwise equivalent WITH self COMPUTE statement, and a lot faster than repeating self.foo, self.bar, and so on in an expression.

The optional DEPENDENT ON clause declares that the computed attributes depend on the listed attributes for their values, and that if any of the DEPENDENT ON attributes are overwritten, the cached values should be reset. (And thus recomputed the next time they are accessed.) This is helpful for ensuring that calculation-based attributes always reflect the object's current state. Note, however, that the attributes are only reset if a DEPENDENT ON attribute is changed by being written to. A change in the value calculated or retrieved by another statement or provider has no effect on the attribute cache.

Examples

  • Simple computed attribute. The formula will be computed the first time the DataSkin needs to know its total_cost attribute in a given (sub)transaction. Notice how much shorter this is than the equivalent example shown under the general WITH/COMPUTE examples. It will also execute more quickly, although by an utterly insignificant amount:
        WITH SELF COMPUTE total_cost = labor_cost+material_cost
        DEPENDENT ON labor_cost,material_cost
    
  • Interdependent computed attributes. Again, it's much less verbose, and a hair faster than its generic WITH/COMPUTE cousin:
        WITH SELF COMPUTE
          area            = width * height,
          square_yards    = area / 9,
          price_per_sq_yd = total_cost / square_yards
    
        # We have to list 'real' attributes, not intermediate values!
        DEPENDENT ON  width,height,labor_cost,material_cost
    

[WHEN eventspec] STORE attributelist USING expression [SAVING mementolist]

What It's For

Storing/updating attributes in an external data source. The attributelist can contain an asterisk (*) to mean "all attributes".

How It Works

When an attribute listed in "attributelist" is changed/deleted, the new value is cached in the DataSkin until a (sub)transaction commit occurs. At that time, "expression" is called, with the 'OLD[]' variable containing the values saved by "mementolist", if specified. (See the section above on "Declaration Parameters" for more details on how memento lists work.)

If the optional "WHEN eventspec" clause is used, "expression" will only be called if the eventspec matches the object-level event. (Note that this means that if you use the WHEN clause, you will need declarations for each possible event for each attribute to ensure that the attributes will be saved under all possible circumstances.)

A STORE/USING declaration is compiled into a single Data Plug-in with two functions: to act as an attribute setter, and to act as a rule agent. The attribute setter part simply caches changes to attributes until (sub)transaction commit time, when the rule agent part becomes active. The rule agent part executes the USING expression if and only if one of the STORE attributes have been changed, and the WHEN clause (if any) is applicable to the situation as of commit time. Note that the seperation of these parts means that the USING expression can and will be called even if more than one declaration (or other Attribute Provider) handles storage for the same attributes! This is actually quite useful when you have data that you want updated in more than one back-end database, but it can be surprising if you expect first-come, only-served behavior as is the case for attribute getters.

There is one interesting side effect, however, if a wildcard (*) is used in the STORE list. Since the * matches any attribute, the USING expression will be called at subtransaction commit as long as any attribute has changed and the WHEN clause is applicable. Again, this is a bit different from attribute getters and other attribute setters, where * effectively means "any attribute that hasn't already been claimed by another provider."

Examples

  • Context-dependent method-driven storage reusing already-written methods. In this example, we use different methods for each situation, which we assume are already-written and which we do not want to change for our specific situation. Notice that for the ADD and DELETE events we use WHEN ... CALL declarations, because we want the widget data to be added or deleted regardless of whether the foo or bar attributes have specifically changed:
         WHEN OBJECT ADDED CALL 
           AddWidget(widget_id=self.id, foo_field=self.foo, wbar=self.bar)
    
         WHEN OBJECT CHANGED STORE foo,bar USING 
           UpdateMethod(widget_id=self.id, foo_field=self.foo, wbar=self.bar)
    
         WHEN OBJECT DELETED CALL DeleteWidget(widget_id=self.id)
    
  • Object remapping. This example shows how to create a "virtual" object whose attributes my_foo and my_bar are mapped to/from another object from a different specialist (SomeSpecialist) using the same id value, but whose fields are named its_foo and its_bar. The example works by maintaining an original_object attribute which contains the object from the other specialist. This is actually a pretty simple remapping example; a single SkinScript script might contain multiple remappings like this to combine parts of different objects into a whole. Also, this example assumes that we are creating an object in SomeSpecialist for every DataSkin created, while common real-life uses will often have the original_object created on demand for an existing DataSkin, or conversely, have the DataSkin created on demand for an existing 'original_object':
         # Handle the case where we just got created...
         INITIALIZE OBJECT WITH original_object=SomeSpecialist.newItem(self.id)
    
         # ...and the case where we already existed
         WITH SomeSpecialist.getItem(self.id) COMPUTE original_object=RESULT
    
         # Map the other object's names to mine...
         WITH self.original_object COMPUTE my_foo=its_foo, my_bar=its_bar
    
         # ...and mine to its
         WHEN OBJECT ADDED,CHANGED STORE my_foo, my_bar USING
           self.original_object.mange_changeProperties(
             its_foo=self.my_foo, its_bar=self.my_bar
           )
    
         # Last, but not least, get rid of the other guy when I'm deleted
         WHEN OBJECT DELETED CALL self.original_object.manage_delete()
    
  • Complex storage mapping using a memento. This example illustrates mapping a more complex kind of attribute into data storage. DelKeywords and AddKeywords are methods (in the SkinScript method's acquisition context) that delete or add keyword records in some (unspecified) kind of database, given an item id and a list of keywords. The example calls a method on the DataSkin called getKeywords() to extract keywords from the description attribute. If the description attribute is changed during the (sub)transaction, at commit time the external database will be updated. For illustrative purposes, appropriate WHEN/CALL declarations are included to ensure that adds and deletes are properly handled. Notice that the DELETED declaration uses the value for the keywords which existed at the start of the (sub)transaction, since it could theoretically have changed before the object ended up being deleted:
         WHEN OBJECT CHANGED STORE description USING
           DelKeywords(item=self.id, kwlist=OLD['kw']),
           AddKeywords(item=self.id, kwlist=self.getKeywords(self.description))
         SAVING
           kw=getKeywords(description)
    
         WHEN OBJECT ADDED CALL 
           AddKeywords(item=self.id,kwlist=getKeywords(self.description))
    
         WHEN OBJECT DELETED CALL
           DelKeywords(item=self.id,kwlist=OLD['kw'])
         SAVING
           kw=getKeywords(description)
    

STORE attributelist IN SELF

What It's For

Specifying attributes to be stored persistently within the object.

How It Works

When an attribute listed in "attributelist" is changed/deleted, it will be stored directly in the object as a persistent attribute. Of course, this will only work if the object itself is stored in the ZODB, and no previous declarations or AttributeProviders declared storage for the attribute. "attributelist" can be just an asterisk (*) to indicate that all attributes not stored in some other way should be stored persistently.

Examples

  • Simple storage. The attributes foo, bar, and baz will be stored persistently in the ZODB. (Note that if the DataSkin is stored in a Rack which is not using persistent storage, this will not work, and data will silently be lost.)

    STORE foo, bar, baz IN SELF

  • Wildcard. Any attribute which is not explicitly claimed for storage by another attribute provider/declaration will be stored persistently, assuming that there is not another wildcard attribute setter which appears earlier than this declaration in precedence order.

    STORE * IN SELF

WHEN eventspec CALL expression [SAVING mementolist]

What It's For

Calling an expression at (sub)transaction commit if any of the specified events have occurred.

How It Works

At (sub)transaction commit time, if one of the events specified in "eventspec" occurred, "expression" is called, with the 'OLD[]' variable containing the values saved by "mementolist", if specified.

Examples

  • E-mail notification upon object add. EMailToManager is a DTML document or method in the acquisition context of the SkinScript method, which expects a new_item parameter containing the object which the notification is supposed to be about. Note that if this snippet were used in a Customizer, the ADD event can occur when an object is moved into the area covered by that Customizer. This would mean notifications would be sent for objects which were not necessarily newly created:
         WHEN OBJECT ADDED CALL EMailToManager(_.None,_,new_item=self)
    
  • Catalog example #1. This is an interesting way of doing automatic cataloging/re-cataloging of objects without having them be CatalogAware. Note that the order of declarations here is very important: when an object is changed, uncatalog_object will be called before catalog_object. SomeCatalog, of course, is a catalog somewhere in the SkinScript method's acquisition context:
         WHEN OBJECT CHANGED,DELETED CALL SomeCatalog.uncatalog_object(self.absolute_url(1))
         WHEN OBJECT ADDED,CHANGED CALL SomeCatalog.catalog_object(self,self.absolute_url(1))
    
  • Complex event alert. This example triggers if there is a 5% or greater change in the ROI of an investment, based on any of the input factors having changed (e.g. cashflow amounts or dates). It works by saving the old result of the DataSkin?'s computeROI() method right before any change is made to the DataSkin?'s attributes. The CALL expression is called at (sub)transaction commit if any changes have been made to the object. That expression compares the new value of computeROI() with the old value as a percentage change, and if it's greater than 5%, calls the SendROIAlert() function which is somewhere in the SkinScript method's acquisition context:
         WHEN OBJECT CHANGED CALL
           (abs(self.computeROI()-OLD['ROI'])/OLD['ROI'] > .05 ) 
            and SendROIAlert(investment=self)
         SAVING 
           ROI=computeROI()
    
  • Catalog example #2. The previous catalog example recatalogs the object if any attribute is changed, even if it is simply set back to what it was in the first place. While future versions of ZCatalog may optimize this case, we can do it ourselves with a bit more complex SkinScript. This example will only recatalog the object if attributes foo or bar were set or the result of method baz has changed:
         WHEN OBJECT ADDED CALL SomeCatalog.catalog_object(self,self.absolute_url(1))
         WHEN OBJECT DELETED CALL SomeCatalog.uncatalog_object(self.absolute_url(1))
    
         WHEN OBJECT CHANGED CALL
          (HAS_CHANGED('foo') or HAS_CHANGED('bar') or self.baz()<>OLD['baz']) and
          (  SomeCatalog.uncatalog_object(self.absolute_url(1)),
             SomeCatalog.catalog_object(self,self.absolute_url(1))
           )
         SAVING baz=baz()
    
  • Generic method-driven storage with custom method. In this example, it is assumed that SomeMethod is a DTML, SQL, or Python method that expects to get a DataSkin and save its attributes in an external database. The method must be smart enough to handle an ADD, CHANGE, or DELETE event, and takes a parameter called what_happened to tell it which one happened. In the case of an SQL method, it's pretty straightforward to write DTML that generates an appropriate INSERT, UPDATE, or DELETE statement according to which event happened. By using the HAS_CHANGED function, SomeMethod can even generate an optimal UPDATE statement that only changes fields that have actually changed. As you can see, this example puts the bulk of the processing burden on SomeMethod, which has to be written specifically for the situation. This may be optimal, however, if you are going to have to write the methods anyway:
         WHEN OBJECT ADDED,CHANGED,DELETED CALL
           SomeMethod(object=self, what_happened=TRIGGER_EVENT, has_changed=HAS_CHANGED)
    

Language Keywords

The following keywords are SkinScript reserved words and cannot be used as attribute names or appear in expressions. If you must access an attribute with one of these names in an expression, you must use '_["name"]?' syntax, as is sometimes used in DTML expressions to access otherwise inaccessible names.

Keywords: ADDED CALL CHANGED COMPUTE DELETED DEPENDENT IN INCLUDE INITIALIZE LET OBJECT ON OTHERWISE QUERY SAVING SELF SLOT STORE USING WHEN WITH