Zope3部件体系架构教程四章-接口
Zope3部件体系架构教程四章-接口
http://www.315ok.org/blogfolder/108
http://www.315ok.org/logo.png
Zope3部件体系架构教程四章-接口
Zope3部件体系架构教程四章-接口
本教程中,我们将演示一个样例应用。一个世界烹饪网,用来收集和展示世界各地著名菜系菜单。首先,我们需要定义一个存储菜单信息的部件。正如我们在第二章讨论的一样,存储数据的部件叫内容部件。在我们写这么一个部件前,我们需要定义该部件存储和提供什么样的数据。也就是要定义一个接口。接口是定义部件之间怎样协助的一个契约,接口描述一个对象提供的方法和属性,接口描述一个部件是什么,而不描述具体怎样实现。
接口不能也不会更改python的语义。接口标识部件部件承诺提供的功能。在这个意义上讲,它和标签和标记没有任何区别。甚至,某些接口并不定义任何API,我们称这类接口为标记接口,其他使用接口的情况是数据的schemas,此时接口用来描述数据格式。
行话
在Zope世界的接口模型和其他语言最大的不同是zope中接口以对象为中心,其他的以类为中心。通常情况下,不关心一个对象的实现,只关心对象提供的功能。
在接口的行话中,我们说一个对象提供一个接口,就好像一个机器提供一种功能,一个人提供一种服务一样。在这种情况下,对象通常意味着: class instances, Python modules 或 even interfaces 本身 (因此接口也提供功能)。我们说类实现一个接口,也就是说该类所有的实例提供了这种接口。
定义接口接口定义采用类声明语句,从interface继承(从zope.interface包导入),按照惯例,接口名称首字母是I,以表明声明的是一个接口,而不是类。.例如,当定义一个带method的类时,该类的接口和他的方法可以是标准的的文档字符串。
一个简单的文档字符串就可以作为clss或method的body部分,而不会有任何语法问题。当为接口声明方法时,self参数也不需要,因为接口描述方法怎么使用,而不涉及具体实现。另外一个惯例,放置所有接口在一个module 或一个package里面。
属性定义
使用getName() 和 setName() 方式访问数据不十分python化。python中可以在对象的外面设置其属性。为什么不声明数据的gettable和settable属性?
考虑例子 4.2.3.
用Attribute的实例来声明属性,Attribute由zope.interface包提供,当然属性声明和方法声明能共存在接口中。
Example 4.2.1 An example interface (interfaces.py)
3. 采用类声明并不是定义一个真的类,而是接口.
这是因为interface并不是一个基类,而是一个对象,一个接口对象。
7. 由于使用了class声明语句,其中def 语句假定一个方法。我们知道此处接口定义不是一个类,而是一个对象,以方法定义结束该接口对象定义。这些方法仅仅描述可以调用的方法,因此我们可以省掉self参数。
8. 一个 Python docstring 填充方法的函数体,不需要任何其他语句。在此处,任何代码都不可执行。
Example 4.2.2 Deriving from an interface (interfaces.py)
Example 4.2.3 An interface with attribute declarations (interfaces.py)
注意,类定义中,并不强制要implements()语句,而且在类对接口的实现中,可以不必实现接口的全部内容,这在语法上没有任何错误,但在应用当中,如果由其他部件要引用整个接口信息时,会导致出错。
Example 4.3.1 A simple implementation of the IRecipeInfo interface(recipe.py)
17–30. ( init )构造方法,实现了由接口描述的5个方法
In conclusion, interface compliance is assumed, not enforced, though the interface machinery provides utilities for verification (see below).
There also are other ways of declaring that a component implements an interface. The classImplements() function declares an interface on an already existing class. This is useful when third party components are being used in Zope 3 and source code modification is not possible or unwanted.
The directlyProvides() function declares an interface on an object that has already been instantiated. A second call to directlyProvides() will
override anything that has been set during the first call, though. That means it would remove interfaces from the object unless they were stated explicitly again. It is therefore safer to use the alsoProvides function which only adds interfaces to the object and does not remove existing ones. Note that interfaces that are declared at a class level are not affected by this.
Making a particular object provide an additional interface is mostly used for marker interfaces. These are interfaces that do not promise any methods or attributes to be implemented but mark the object that implements them in a certain way. Zope 3 makes use of a few marker interfaces as we will see later on. Consider the following example code for classImplements() and alsoProvides(), typed in at the Python interpreter prompt:
In the example, the chilaquiles object is an instance of the RecipeInfo class and does not provide the IRecipeInfo interface. However, we can change that by using alsoProvides:
We have so far only talked about classes implementing interfaces. In general, implementers do not have to be classes, though. Any callable object that creates an object providing an interface can implement this interface. Classes just happen to work this way, but we could also think of functions that do this.
For example, imagine we have a function that creates RecipeInfo objects,basically a simple factory. Since the created objects provide IRecipeInfo (we made RecipeInfo implement IRecipeInfo, remember?), this factory function can be declared an implementer of IRecipeInfo by using the implementer decorator:
from zope.interface import moduleProvides
moduleProvides(IComponentArchitecture) That is equivalent to assigning the interface from the outside:
Summary
• Factories implement interfaces so that the objects they create provide them; normally these factories are classes, but can be any callable object (such as functions).
• Interfaces are typically declared on classes using the implements() function in the class body.
• It is also possible to declare that a single object or even a Python module provides an interface.
• An interface’s providedBy() method informs whether an object provides that interface.
4.4 Verifying implementationsAs mentioned before, interfaces are a Zope-specific addition and not part of Python, thus declarations in interfaces are not enforced. The interface package provides a way to verify if an object conforms to an interface it provides. For example, let us create a simple class that obviously does not comply with IRecipeInfo but claims to implement it:
However, be aware that not all classes that implement an interface comply with it directly. For example, certain required attributes might only be set by the constructor method ( init ) when an object is created. verifyClass would miss those and falsely report an error.
Summary
• Interface compliance is implied and not enforced at runtime.
• The zope.interface.verify module provides tools for the developer to verify that implementations concur with what the interface defines.
4.5 SchemasAs we have discussed before, simple content objects usually do not need methods because all they are responsible for is storing data. That can easily be done in instance attributes; defining setters and getters, while not complicated, is still quite verbose and frankly very unpythonic.
We have also seen that we can describe required attributes in interfaces using Attribute objects. However, even in the simple IRecipeInfo example above, it is quite obvious that we could supply much more information about each field than just a docstring. For example, we see that name should be a one line text string while time to cook should store an integer. Even though Python is dynamically-typed, we are absolutely certain that these attributes should only contain values of a certain type.
If we continue that thought and define the type of each attribute in the interface, we end up with what is called a schema. Much like in a table schema known from relational databases, each property is called a field. Different fields imply different type constraints.
Defining schemas Schemas are defined exactly like interfaces. There is no special schema object to derive from. However, instead of using Attribute, we now use fields provided by the zope.schema package to describe attributes. Table 4.1 lists the standard field types provided by Zope 3 and the type they describe.
Since schemas and interfaces are semantically and syntactically the same thing, an interface with method definitions can just as well contain schema fields and vice versa. It is all interfaces to Zope. Example 4.5.1 demonstrates what a schema definition looks like.
In Example 4.5.1, we see that all fields are passed nearly the same arguments, such as title and description. Table 4.2 gives an overview
over possible field arguments. All arguments to the field constructor listed below are also attributes of the field instance. We see that field parameters attributes are defined through a schema as well, since they have to comply with certain field constraints (e.g. title has to be a TextLine, that is why we have to pass a Unicode string)3.
All arguments to the field constructor are optional. However, providing at least the title and a longer descriptive text helps documentation. More usefully,they also support application purposes, such as automatically generated forms (see Chapter 8).
Custom constraints
We said earlier that different fields imply different type constraints. In addition to this implied type constraint, it is also possible to add a custom constraint by using the constraint parameter in the field constructor.
The field expects any callable that takes one argument for this parameter.
Imagine, for example, an interface that had an email field. There is no special field type for emails, but we can use TextLine with a custom
constraint. Verifying email addresses is best done with a regular expression:
Example 4.5.1 Defining a schema (interfaces.py)
4. As mentioned before, Zope does not distinguish between interfaces and schemas,the difference is pure nomenclature.
8–39. Fields on the schema are specified like Attribute definitions on a regular interface, however with a rich set of parameters that document the field and describe the constraints.
Table 4.1. Schema fields from zope.schema4
Field type Type constraint
Abstract fields (mostly used for subclassing)
Field Simple field without any type constraint (unless a custom one is provided), base class for all other fields.
Container Object supporting the in operator, meaning it has to provide either contains or getitem .
Iterable Object supporting iteration, meaning it has to provide either iter or getitem .
Fields for standard python types
Bool Boolean value (bool).
Int Integer (int).
Float Float (float).
Text Unicode text (unicode).
TextLine Like Text, but without newline characters.
Bytes Byte string (str), useful for binary data.
BytesLine Like Bytes, but without newline characters.
Tuple Tuple (tuple).
List List (list).
Dict Dictionary (dict).
Set Set (sets.Set).
Date Date value (datetime.date).
Datetime Date and time value (datetime.datetime).
Choice An object from a source or vocabulary.
Object Arbitrary object providing a schema.
Fields with special constraints
Password A TextLine used for storing passwords.
SourceText A Text field that holds the source of some computed output.
ASCII A string containing only ASCII characters.
InterfaceField Interface (zope.interface.Interface).
URI A BytesLine that holds a Uniform Resource Identifier (URI).
DottedName A BytesLine that contains a dotted name.
Id A BytesLine that contains either a URI or a dotted name.
Table 4.2. Schema field parameters/attributes
Name Type Description
title TextLine Label for the field.
description Text Longer description.
required Bool Require the existence of the value (mutually exclusive with default). Defaults to True.
readonly Bool Determine whether the field’s value can be changed.
default Field The default value if none was provided (mutually exclusive with required).
missing value Field In the case of missing input value, the value provided here is used.
order Int Gives information about the order in which fields in a schema are defined. This is a readonly attribute and may not be passed as an
argument to the field constructor.
constraint one-parameter callable
A callable (e.g. function) that validates its
parameter (the potential field value) in addition to the constraint implied by the field.
It may either return a Boolean or raise an exception derived from ValidationError.
min, max Int or Float Limit the numeric range of Int and Float fields.
min length,
max length
Int Require a minimum and/or maximum length. Applicable to all bytes, text and sequence fields.
value type Field The type of values in a collection. Applicable to all collection fields (including sequence fields).
unique Bool Specifies whether the values in a collection must be unique or not. Applicable to all collection fields (including sequence fields).
For automated forms as they are discussed in Chapter 8, the exception’s docstring will be presented to the user as the detailed error message:
We have seen that the constraint parameter lets us add a custom constraint to any field. Such a constraint is bound to that particular field, though.
It is not possible to express a constraint between several fields with it.
For example, consider a schema that describes a round-trip flight ticket.
Most tickets are not valid for longer than a year. Thus, the date of the departing flight and the date of the returning flight should not be longer than 12 months apart. Of course, the departing flight should also occur before the returning flight. With these constraints it is not possible to validate one or the other separately. Only when knowing both values it is possible to say whether they are valid.
A constraint that involves more than one field is called an invariant. The flight ticket example can easily be rendered with an invariant. For reasons of brevity, we continue on the interpreter prompt:
allowing checks on one, two or more of them.
Invariants are added to an interface by using the invariant function from the zope.interface package. When the callable is a mere function,
we can write it directly into the interface and use Python’s decorator syntax (@invariant). Again, looks are deceiving when writing interfaces: the functions created and decorated this way are not callable methods on the interface. In fact, they are not accessible at all from the outside, they are absorbed into the internal interface definition.
Let now create a dummy ticket object and stick some (invalid) dates on it. Note that the interface specifies a Datetime field for both dates, so we should use the datetime class from the Python standard library:
Traceback (most recent call last):
Introspecting schemas
Sometimes it is useful to introspect schemas, for example when you want to retrieve a list of the fields a schema carries. Schemas are just plain interfaces and can contain standard method or attribute declarations apart from schema fields. Therefore, an interface’s names() or namesAndDescriptions() method would not only return fields, but these other declarations as well.
That would not be too useful. If you only need to know about a schema’s fields, the zope.schema package provides a few functions for introspecting schemas as listed in Table 4.3.
Table 4.3. Schema introspecting functions provided by zope.schema
Function Parameters Description
getFieldNames schema Returns a list of the fields’ names defined in schema.
getFields schema Returns a dictionary containing a mapping of field names to fields defined in schema.
getFieldsInOrder schema Returns a list of (name, field) tuples in the order the fields are defined in schema.
getFieldNamesInOrder schema Returns a list of the fields’ names in the order they are defined in schema.
Summary
• Interfaces can be used to describe data schemas.
• Instead of attribute declarations, fields expressing type and other constraints are used to describe the schema.
• The zope.schema package provides many basic field types.
• Constraints involving more than one field may be implemented using an invariant.
Writing content objects that provide a schema and the automatic validation of schema compliance will be covered in Chapter 5. Sources and vocabularies,an advanced schema-related topic, are the subject of Chapter 17.
- 接口的语义
接口不能也不会更改python的语义。接口标识部件部件承诺提供的功能。在这个意义上讲,它和标签和标记没有任何区别。甚至,某些接口并不定义任何API,我们称这类接口为标记接口,其他使用接口的情况是数据的schemas,此时接口用来描述数据格式。
行话
在Zope世界的接口模型和其他语言最大的不同是zope中接口以对象为中心,其他的以类为中心。通常情况下,不关心一个对象的实现,只关心对象提供的功能。
在接口的行话中,我们说一个对象提供一个接口,就好像一个机器提供一种功能,一个人提供一种服务一样。在这种情况下,对象通常意味着: class instances, Python modules 或 even interfaces 本身 (因此接口也提供功能)。我们说类实现一个接口,也就是说该类所有的实例提供了这种接口。
定义接口接口定义采用类声明语句,从interface继承(从zope.interface包导入),按照惯例,接口名称首字母是I,以表明声明的是一个接口,而不是类。.例如,当定义一个带method的类时,该类的接口和他的方法可以是标准的的文档字符串。
一个简单的文档字符串就可以作为clss或method的body部分,而不会有任何语法问题。当为接口声明方法时,self参数也不需要,因为接口描述方法怎么使用,而不涉及具体实现。另外一个惯例,放置所有接口在一个module 或一个package里面。
属性定义
使用getName() 和 setName() 方式访问数据不十分python化。python中可以在对象的外面设置其属性。为什么不声明数据的gettable和settable属性?
考虑例子 4.2.3.
用Attribute的实例来声明属性,Attribute由zope.interface包提供,当然属性声明和方法声明能共存在接口中。
Example 4.2.1 An example interface (interfaces.py)
1 from zope.interface import Interface 2 3 class IRecipeInfo(Interface): 4 """Give information about a recipe. 5 """ 6 7 def getName(): 8 """Return the name of the dish described. 9 """ 10 11 def getIngredients(): 12 """Return a list of ingredients. 13 """ 14 15 def getTools(): 16 """Return a list of necessary kitchen tools. 17 """ 18 19 def getTimeToCook(): 20 """Return the time necessary for preparing the meal in 21 minutes. 22 """ 23 24 def getDescription(): 25 """Return the description of the recipe. 26 """1. 从 zope.interface 包导入基本接口interface
3. 采用类声明并不是定义一个真的类,而是接口.
这是因为interface并不是一个基类,而是一个对象,一个接口对象。
7. 由于使用了class声明语句,其中def 语句假定一个方法。我们知道此处接口定义不是一个类,而是一个对象,以方法定义结束该接口对象定义。这些方法仅仅描述可以调用的方法,因此我们可以省掉self参数。
8. 一个 Python docstring 填充方法的函数体,不需要任何其他语句。在此处,任何代码都不可执行。
Example 4.2.2 Deriving from an interface (interfaces.py)
1 ... 2 3 class IRecipe(IRecipeInfo): 4 """Give and store information about a recipe. 5 """ 6 7 def setName(name): 8 """Set the name of the dish provided in the ’name’ parameter. 9 """ 10 11 def setIngredients(ingredients): 12 """Set the ingredients necessary for this recipe provided in 13 the ’ingredients’ parameter.""" 14 15 def setTools(tools): 16 """Set the list of necessary kitchen tools. 17 """ 18 19 def setTimeToCook(time_to_cook): 20 """Set the time necessary for preparing the meal in minutes. 21 """ 22 23 def setDescription(description): 24 """Set the description of the recipe. 25 """3. 接口从 interfaces生成,继承interface的方法和属性 .
Example 4.2.3 An interface with attribute declarations (interfaces.py)
1 from zope.interface import Interface, Attribute
2
3 class IRecipe(Interface):
4 """Store information about a recipe.
5 """
6
7 name = Attribute("Name of the dish.")
8
9 ingredients = Attribute(
10 "List of ingredients necessary for this recipe.")
11
12 tools = Attribute("List of necessary kitchen tools.")
13
14 time_to_cook = Attribute(
15 "Necessary time for preparing the meal described.")
16
17 description = Attribute("Description of the recipe.")声明一个对象提供一个接口当一个部件需要提供某个接口时,需要显示声明。最通用的方法是声明一个类来实现某一接口,则其所有实例自动提供该接口,在类定义中用implements()来声明实现的接口,如例4.3.1 。注意,类定义中,并不强制要implements()语句,而且在类对接口的实现中,可以不必实现接口的全部内容,这在语法上没有任何错误,但在应用当中,如果由其他部件要引用整个接口信息时,会导致出错。
Example 4.3.1 A simple implementation of the IRecipeInfo interface(recipe.py)
1 from zope.interface import implements 2 from worldcookery.interfaces import IRecipeInfo 3 4 class RecipeInfo(object): 5 """Give information about a recipe. 6 """ 7 implements(IRecipeInfo) 8 9 def __init__(self, name, ingredients, tools, time_to_cook, 10 description): 11 self.name = name 12 self.ingredients = ingredients 13 self.tools = tools 14 self.time_to_took = time_to_cook 15 self.description = description 16 17 def getName(self): 18 return self.name 19 20 def getIngredients(self): 21 return self.ingredients 22 23 def getTools(self): 24 return self.tools 25 26 def getTimeToCook(self): 27 return self.time_to_cook 28 29 def getDescription(self): 30 return self.description7. 这个 RecipeInfo class 通过implements()语句来实现 IRecipeInfo 接口,implements()语句可以有多个参数,也就是说可以声明实现多个接口。
17–30. ( init )构造方法,实现了由接口描述的5个方法
In conclusion, interface compliance is assumed, not enforced, though the interface machinery provides utilities for verification (see below).
There also are other ways of declaring that a component implements an interface. The classImplements() function declares an interface on an already existing class. This is useful when third party components are being used in Zope 3 and source code modification is not possible or unwanted.
The directlyProvides() function declares an interface on an object that has already been instantiated. A second call to directlyProvides() will
override anything that has been set during the first call, though. That means it would remove interfaces from the object unless they were stated explicitly again. It is therefore safer to use the alsoProvides function which only adds interfaces to the object and does not remove existing ones. Note that interfaces that are declared at a class level are not affected by this.
Making a particular object provide an additional interface is mostly used for marker interfaces. These are interfaces that do not promise any methods or attributes to be implemented but mark the object that implements them in a certain way. Zope 3 makes use of a few marker interfaces as we will see later on. Consider the following example code for classImplements() and alsoProvides(), typed in at the Python interpreter prompt:
$ python >>> class RecipeInfo: ... pass ... >>> from worldcookery.interfaces import IRecipeInfo >>> IRecipeInfo.implementedBy(RecipeInfo) False >>> chilaquiles = RecipeInfo() >>> IRecipeInfo.providedBy(chilaquiles) False ▼Here we actually have a hint that interfaces are not classes but special objects,because they have implementedBy() and providedBy() methods which return True when the argument implements or provides the interface,respectively, and False otherwise. Interfaces have many more methods which are all documented in (surprise!) an interface, IInterface.
In the example, the chilaquiles object is an instance of the RecipeInfo class and does not provide the IRecipeInfo interface. However, we can change that by using alsoProvides:
>>> from zope.interface import alsoProvides >>> alsoProvides(chilaquiles, IRecipeInfo) >>> IRecipeInfo.providedBy(chilaquiles) True ▼Now the chilaquiles object, and only this object, provides the interface.We can verify this by checking another RecipeInfo instance:
>>> posole = RecipeInfo() >>> IRecipeInfo.providedBy(posole) False ▼If we now let the RecipeInfo class implement the interface, all its instances will automatically provide it as well, including the posole object:
>>> from zope.interface import classImplements >>> classImplements(RecipeInfo, IRecipeInfo) >>> IRecipeInfo.implementedBy(RecipeInfo) True >>> IRecipeInfo.providedBy(posole) True ▼Functions and modules
We have so far only talked about classes implementing interfaces. In general, implementers do not have to be classes, though. Any callable object that creates an object providing an interface can implement this interface. Classes just happen to work this way, but we could also think of functions that do this.
For example, imagine we have a function that creates RecipeInfo objects,basically a simple factory. Since the created objects provide IRecipeInfo (we made RecipeInfo implement IRecipeInfo, remember?), this factory function can be declared an implementer of IRecipeInfo by using the implementer decorator:
>>> from zope.interface import implementer >>> @implementer(IRecipeInfo) ... def makeRecipeInfo(): ... return RecipeInfo() ... >>> IRecipeInfo.implementedBy(makeRecipeInfo) True >>> IRecipeInfo.providedBy(makeRecipeInfo()) True ▼The similar semantics between a class factory and a function factory become apparent if we repeat the two last statements with the class instead of the function:
>>> IRecipeInfo.implementedBy(RecipeInfo) True >>> IRecipeInfo.providedBy(RecipeInfo()) True ■Another unusual use of interface is to indicate the API of a Python module.Since Python modules are regular objects, they can also provide interfaces.This can be expressed using the moduleProvides() function. For example, the zope.component module provides many standard Component Architecture API functions from one single module (see Appendix A). This API is documented in the IComponentArchitecture interface. Therefore the zope.component module somewhere contains these lines:
from zope.interface import moduleProvides
moduleProvides(IComponentArchitecture) That is equivalent to assigning the interface from the outside:
$ python >>> import zope.component >>> directlyProvides( ... zope.component, ... zope.component.interfaces.IComponentArchitecture ... ) ■As we have seen so far, the zope.interface module has a rich API with a lot of functions for declaring interfaces. implements, directlyProvides, etc. are all part of it. This API is also formally defined in an interface called IInterfaceDeclaration. It will of course not surprise you now that the zope.interface module provides this interface.
Summary
• Factories implement interfaces so that the objects they create provide them; normally these factories are classes, but can be any callable object (such as functions).
• Interfaces are typically declared on classes using the implements() function in the class body.
• It is also possible to declare that a single object or even a Python module provides an interface.
• An interface’s providedBy() method informs whether an object provides that interface.
4.4 Verifying implementationsAs mentioned before, interfaces are a Zope-specific addition and not part of Python, thus declarations in interfaces are not enforced. The interface package provides a way to verify if an object conforms to an interface it provides. For example, let us create a simple class that obviously does not comply with IRecipeInfo but claims to implement it:
$ python >>> from zope.interface import implements >>> from worldcookery.interfaces import IRecipeInfo >>> class NotARecipeInfo: ... implements(IRecipeInfo) ... >>> not_a_recipe_info = NotARecipeInfo() ▼We can then use verifyObject to perform the interface compliance check:
>>> from zope.interface.verify import verifyObject >>> verifyObject(IRecipeInfo, not_a_recipe_info) Traceback (most recent call last): ... zope.interface.exceptions.BrokenImplementation: An object has failed to implement interface <InterfaceClass interfaces.IRecipeInfo> The getName attribute was not provided. ▼As expected, verifyObject finds that the NotARecipe instance does not comply with the IRecipeInfo interface. The contrary is the case with instances of the RecipeInfo class we defined in Example 4.3.1; they fulfil the interface:
>>> from worldcookery.recipe import RecipeInfo >>> recipe_info = RecipeInfo() >>> verifyObject(IRecipeInfo, recipe_info) True ■If you want to directly check whether a class complies with an interface without creating an object first, you may use the verifyClass function from the zope.interface.verify module. Its usage is like verifyObject.
However, be aware that not all classes that implement an interface comply with it directly. For example, certain required attributes might only be set by the constructor method ( init ) when an object is created. verifyClass would miss those and falsely report an error.
Summary
• Interface compliance is implied and not enforced at runtime.
• The zope.interface.verify module provides tools for the developer to verify that implementations concur with what the interface defines.
4.5 SchemasAs we have discussed before, simple content objects usually do not need methods because all they are responsible for is storing data. That can easily be done in instance attributes; defining setters and getters, while not complicated, is still quite verbose and frankly very unpythonic.
We have also seen that we can describe required attributes in interfaces using Attribute objects. However, even in the simple IRecipeInfo example above, it is quite obvious that we could supply much more information about each field than just a docstring. For example, we see that name should be a one line text string while time to cook should store an integer. Even though Python is dynamically-typed, we are absolutely certain that these attributes should only contain values of a certain type.
If we continue that thought and define the type of each attribute in the interface, we end up with what is called a schema. Much like in a table schema known from relational databases, each property is called a field. Different fields imply different type constraints.
Defining schemas Schemas are defined exactly like interfaces. There is no special schema object to derive from. However, instead of using Attribute, we now use fields provided by the zope.schema package to describe attributes. Table 4.1 lists the standard field types provided by Zope 3 and the type they describe.
Since schemas and interfaces are semantically and syntactically the same thing, an interface with method definitions can just as well contain schema fields and vice versa. It is all interfaces to Zope. Example 4.5.1 demonstrates what a schema definition looks like.
In Example 4.5.1, we see that all fields are passed nearly the same arguments, such as title and description. Table 4.2 gives an overview
over possible field arguments. All arguments to the field constructor listed below are also attributes of the field instance. We see that field parameters attributes are defined through a schema as well, since they have to comply with certain field constraints (e.g. title has to be a TextLine, that is why we have to pass a Unicode string)3.
All arguments to the field constructor are optional. However, providing at least the title and a longer descriptive text helps documentation. More usefully,they also support application purposes, such as automatically generated forms (see Chapter 8).
Custom constraints
We said earlier that different fields imply different type constraints. In addition to this implied type constraint, it is also possible to add a custom constraint by using the constraint parameter in the field constructor.
The field expects any callable that takes one argument for this parameter.
Imagine, for example, an interface that had an email field. There is no special field type for emails, but we can use TextLine with a custom
constraint. Verifying email addresses is best done with a regular expression:
Example 4.5.1 Defining a schema (interfaces.py)
1 from zope.interface import Interface 2 from zope.schema import List, Text, TextLine, Int 3 4 class IRecipe(Interface): 5 """Store information about a recipe. 6 """ 7 8 name = TextLine( 9 title=u"Name", 10 description=u"Name of the dish", 11 required = True 12 ) 13 14 ingredients = List( 15 title=u"Ingredients", 16 description=u"List of ingredients necessary for this recipe.", 17 required=True, 18 value_type=TextLine(title=u"Ingredient") 19 ) 20 21 tools = List( 22 title=u"Tools", 23 description=u"List of necessary kitchen tools", 24 required=False, 25 value_type=TextLine(title=u"Tool") 26 ) 27 28 time_to_cook = Int( 29 title=u"Time to cook", 30 description=u"Necessary time for preparing the meal described, " 31 "in minutes.", 32 required=True 33 ) 34 35 description = Text( 36 title=u"Description", 37 description=u"Description of the recipe", 38 required=True 39 )2. The zope.schema package provides field types for the most common types of objects, such as Python lists, integers, and Unicode strings.
4. As mentioned before, Zope does not distinguish between interfaces and schemas,the difference is pure nomenclature.
8–39. Fields on the schema are specified like Attribute definitions on a regular interface, however with a rich set of parameters that document the field and describe the constraints.
Table 4.1. Schema fields from zope.schema4
Field type Type constraint
Abstract fields (mostly used for subclassing)
Field Simple field without any type constraint (unless a custom one is provided), base class for all other fields.
Container Object supporting the in operator, meaning it has to provide either contains or getitem .
Iterable Object supporting iteration, meaning it has to provide either iter or getitem .
Fields for standard python types
Bool Boolean value (bool).
Int Integer (int).
Float Float (float).
Text Unicode text (unicode).
TextLine Like Text, but without newline characters.
Bytes Byte string (str), useful for binary data.
BytesLine Like Bytes, but without newline characters.
Tuple Tuple (tuple).
List List (list).
Dict Dictionary (dict).
Set Set (sets.Set).
Date Date value (datetime.date).
Datetime Date and time value (datetime.datetime).
Choice An object from a source or vocabulary.
Object Arbitrary object providing a schema.
Fields with special constraints
Password A TextLine used for storing passwords.
SourceText A Text field that holds the source of some computed output.
ASCII A string containing only ASCII characters.
InterfaceField Interface (zope.interface.Interface).
URI A BytesLine that holds a Uniform Resource Identifier (URI).
DottedName A BytesLine that contains a dotted name.
Id A BytesLine that contains either a URI or a dotted name.
Table 4.2. Schema field parameters/attributes
Name Type Description
title TextLine Label for the field.
description Text Longer description.
required Bool Require the existence of the value (mutually exclusive with default). Defaults to True.
readonly Bool Determine whether the field’s value can be changed.
default Field The default value if none was provided (mutually exclusive with required).
missing value Field In the case of missing input value, the value provided here is used.
order Int Gives information about the order in which fields in a schema are defined. This is a readonly attribute and may not be passed as an
argument to the field constructor.
constraint one-parameter callable
A callable (e.g. function) that validates its
parameter (the potential field value) in addition to the constraint implied by the field.
It may either return a Boolean or raise an exception derived from ValidationError.
min, max Int or Float Limit the numeric range of Int and Float fields.
min length,
max length
Int Require a minimum and/or maximum length. Applicable to all bytes, text and sequence fields.
value type Field The type of values in a collection. Applicable to all collection fields (including sequence fields).
unique Bool Specifies whether the values in a collection must be unique or not. Applicable to all collection fields (including sequence fields).
$ python
>>> import re
>>> regex = r"[a-zA-Z0-9._%-]+@([a-zA-Z0-9-]+\.)*[a-zA-Z]{2,4}"
>>> check_email = re.compile(regex).match
>>> bool(check_email("philipp@weitershausen.de"))
True
>>> bool(check_email("this-is-not-an-email-address"))
False ▼ Above we aliased the match method of a regular expression object to the check email variable. This is now a one-argument callable which we can use when constructing the email field: >>> from zope.schema import TextLine >>> email = TextLine( ... title=u"Email", ... description=u"An email address", ... constraint=check_email ... ) ▼When we validate a value for the email field now, the field will use the extra constraint expressed through the regular expression:
>>> email.validate(u"philipp@weitershausen.de") >>> email.validate(u"this-is-not-an-email-address") Traceback (most recent call last): ... zope.schema._bootstrapinterfaces.ConstraintNotSatisfied: this-is-not-an-email-address ▼Note that for custom constraints that report an invalid value, the very general ConstraintNotSatisfied exception is raised. This may not always be satisfactory, for example when the schema field is used to render forms and a detailed error message should be displayed to the user as to why the input value is not valid. In this case it is possible to create a custom exception deriving from ValidationError and raise this in the custom validator.
For automated forms as they are discussed in Chapter 8, the exception’s docstring will be presented to the user as the detailed error message:
>>> from zope.schema import ValidationError >>> class NotAnEmailAddress(ValidationError): ... """This is not a valid email address""" ... >>> def validate_email(value): ... if not check_email(value): ... raise NotAnEmailAddress(value) ... return True ... >>> email.constraint = validate_email >>> email.validate(u’this-is-not-an-email-address’) Traceback (most recent call last): ... __main__.NotAnEmailAddress: this-is-not-an-email-address ■Invariants
We have seen that the constraint parameter lets us add a custom constraint to any field. Such a constraint is bound to that particular field, though.
It is not possible to express a constraint between several fields with it.
For example, consider a schema that describes a round-trip flight ticket.
Most tickets are not valid for longer than a year. Thus, the date of the departing flight and the date of the returning flight should not be longer than 12 months apart. Of course, the departing flight should also occur before the returning flight. With these constraints it is not possible to validate one or the other separately. Only when knowing both values it is possible to say whether they are valid.
A constraint that involves more than one field is called an invariant. The flight ticket example can easily be rendered with an invariant. For reasons of brevity, we continue on the interpreter prompt:
$ python
>>> from zope.interface import Interface, Invalid, invariant
>>> from zope.schema import Datetime
>>> class IRoundTripTicket(Interface):
... departing = Datetime(title=u"Departure date")
... returning = Datetime(title=u"Return date")
... @invariant
... def departingBeforeReturning(ticket):
... if ticket.departing > ticket.returning:
... raise Invalid("Departing date must be before "
... "returning date")
... @invariant
... def oneYearValid(ticket):
... delta = ticket.returning - ticket.departing
... if delta.days > 365:
... raise Invalid("Ticket is only valid for one year!")
... ▼ As you can see, invariants are one-parameter callables like custom constraints. However, they take a whole object as their argument, not just the value of a particular field. This gives invariants access to all the attributes of an object,allowing checks on one, two or more of them.
Invariants are added to an interface by using the invariant function from the zope.interface package. When the callable is a mere function,
we can write it directly into the interface and use Python’s decorator syntax (@invariant). Again, looks are deceiving when writing interfaces: the functions created and decorated this way are not callable methods on the interface. In fact, they are not accessible at all from the outside, they are absorbed into the internal interface definition.
Let now create a dummy ticket object and stick some (invalid) dates on it. Note that the interface specifies a Datetime field for both dates, so we should use the datetime class from the Python standard library:
>>> class Ticket: ... pass >>> ticket = Ticket() >>> from datetime import datetime >>> ticket.departing = datetime(2006, 1, 1, 8, 0, 0) >>> ticket.returning = datetime(2006, 1, 1, 7, 0, 0) ▼IRoundTripTicket.validateInvariants(ticket)
Traceback (most recent call last):
... zope.interface.exceptions.Invalid: Departing date must be before returning date ■As expected we are presented with the right exception. I leave it up to you now to test the other invariant. You will also see that the validateInvariants method simply returns if the invariant tests pass.
Introspecting schemas
Sometimes it is useful to introspect schemas, for example when you want to retrieve a list of the fields a schema carries. Schemas are just plain interfaces and can contain standard method or attribute declarations apart from schema fields. Therefore, an interface’s names() or namesAndDescriptions() method would not only return fields, but these other declarations as well.
That would not be too useful. If you only need to know about a schema’s fields, the zope.schema package provides a few functions for introspecting schemas as listed in Table 4.3.
Table 4.3. Schema introspecting functions provided by zope.schema
Function Parameters Description
getFieldNames schema Returns a list of the fields’ names defined in schema.
getFields schema Returns a dictionary containing a mapping of field names to fields defined in schema.
getFieldsInOrder schema Returns a list of (name, field) tuples in the order the fields are defined in schema.
getFieldNamesInOrder schema Returns a list of the fields’ names in the order they are defined in schema.
Summary
• Interfaces can be used to describe data schemas.
• Instead of attribute declarations, fields expressing type and other constraints are used to describe the schema.
• The zope.schema package provides many basic field types.
• Constraints involving more than one field may be implemented using an invariant.
Writing content objects that provide a schema and the automatic validation of schema compliance will be covered in Chapter 5. Sources and vocabularies,an advanced schema-related topic, are the subject of Chapter 17.