允许自定义模板的框架
项目描述
hurry.custom
简介
此包包含一个用于自定义模板的基础设施和API。此系统支持的自定义模板语言是“纯推送”语言,在执行时不会调用任意的Python代码。此类语言示例包括json-template(开箱即用支持)和XSLT。此类语言的优势在于它们在通过Web进行定制时相对安全,无需复杂的网络安全基础设施。
让我们看看这个系统必须支持的用例
模板存储在文件系统中,并默认使用。
模板可以自定义。
这种自定义可以存储在另一个数据库中(ZODB、文件系统、关系数据库等);这取决于集成 hurry.custom 的人。
如果数据库中的模板发生变化,则自动更新模板。
可以检索模板源(用于在UI中显示或稍后用于例如在Web浏览器中进行客户端渲染)。
支持模板的服务器端渲染(生成HTML或电子邮件消息等)。输入特定于模板语言(但应被视为不可变)。
提供(静态)输入示例(如JSON或XML文件),以便更容易编辑和测试模板。这些输入示例可以添加到文件系统以及数据库中。
往返支持。可以从数据库中检索自定义模板和示例,并将它们导出到文件系统。当模板需要在自定义期结束后纳入版本控制时,这很有用。
该包对此(这些是可插拔的)一无所知
用于存储模板或其样本自定义的数据库。
使用的特定推送式模板语言。
本包不提供用户界面。它仅提供API,允许您构建此类用户界面。
创建和注册模板语言
为了注册一个新的推送式模板,我们需要提供一个工厂,该工厂接受模板文本(可能需要进一步编译)。实例化工厂应生成一个可调用的对象,该对象接受输入数据(以模板语言的本地格式)。ITemplate 接口定义了此类对象
>>> from hurry.custom.interfaces import ITemplate, CompileError, RenderError
为了演示本包中的功能,我们提供了一个非常简单的推送式模板语言,它基于Python string 模块提供的模板字符串。
>>> import string >>> from zope.interface import implements >>> class StringTemplate(object): ... implements(ITemplate) ... def __init__(self, text): ... if '&' in text: ... raise CompileError("& in template!") ... self.source = text ... self.template = string.Template(text) ... def __call__(self, input): ... try: ... return self.template.substitute(input) ... except KeyError, e: ... raise RenderError(unicode(e))
让我们来演示一下。要渲染模板,只需将数据作为参数调用它。
>>> template = StringTemplate('Hello $thing') >>> template({'thing': 'world'}) 'Hello world'
注意我们在 __init__ 中放置了一些特殊逻辑,如果模板中找到字符串 &,则触发一个 CompileError 错误。这样我们就可以轻松地演示损坏的模板 - 将带有 & 的模板视为具有语法(编译)错误的模板。让我们试试
>>> template = StringTemplate('Hello & bye') Traceback (most recent call last): ... CompileError: & in template!
我们还确保捕获了可能的运行时错误(在这种情况下,输入字典中缺少键时发生的 KeyError),并将其作为 RenderError 抛出。
>>> template = StringTemplate('Hello $thing') >>> template({'thang': 'world'}) Traceback (most recent call last): ... RenderError: 'thing'
模板类定义了一个模板语言。让我们注册模板语言,以便系统了解它,并将文件系统上的 .st 文件视为字符串模板。
>>> from hurry import custom >>> custom.register_language(StringTemplate, extension='.st')
从文件系统中加载模板
hurry.custom 假设所有可定制的模板主要位于文件系统上,并与应用程序的源代码一起分发。它们形成 集合。集合只是一个包含模板的目录(可能包含子目录)。
让我们在文件系统中创建一个模板集合。
>>> import tempfile, os >>> templates_path = tempfile.mkdtemp(prefix='hurry.custom')
我们现在创建一个单独的模板,test1.st。
>>> test1_path = os.path.join(templates_path, 'test1.st') >>> f = open(test1_path, 'w') >>> f.write('Hello $thing') >>> f.close()
我们还创建了一个额外的模板。
>>> test2_path = os.path.join(templates_path, 'test2.st') >>> f = open(test2_path, 'w') >>> f.write("It's full of $thing") >>> f.close()
为了让系统正常工作,我们需要在文件系统中注册这个模板集合。我们需要提供一个全局唯一的集合ID、模板路径,以及(可选)一个标题。
>>> custom.register_collection(id='templates', path=templates_path)
现在我们可以渲染模板。
>>> custom.render('templates', 'test1.st', {'thing': 'world'}) u'Hello world'
我们将尝试另一个模板。
>>> custom.render('templates', 'test2.st', {'thing': 'stars'}) u"It's full of stars"
我们还可以查找模板对象。
>>> template = custom.lookup('templates', 'test1.st')
我们得到了正确的模板。
>>> template({'thing': 'world'}) u'Hello world'
模板还有一个 source 属性。
>>> template.source u'Hello $thing'
模板的源文本被解释为UTF-8字符串。模板源应始终为Unicode格式(或纯ASCII)。
除非在文件系统中更改,否则底层模板不会重新加载。
>>> orig = template.template
当我们触发潜在的重新加载时,没有任何操作发生 - 模板在文件系统中没有更改。
>>> template.source u'Hello $thing' >>> template.template is orig True
但是,当模板在文件系统中更改时,它会自动重新加载模板。我们将通过修改文件来演示这一点。
>>> f = open(test1_path, 'w') >>> f.write('Bye $thing') >>> f.close()
不幸的是,在测试中这不会工作,因为某些平台上的文件修改时间有秒级粒度,太长,以至于延迟测试。因此,我们将手动更新最后更新时间作为一种权宜之计。
>>> template._last_updated -= 1
现在模板已经更改。
>>> template.source u'Bye $thing' >>> template({'thing': 'world'}) u'Bye world'
自定义数据库
到目前为止,我们所有的操作都是在根(文件系统)数据库中完成的。我们现在可以获取它。
>>> root_db = custom.root_collection('templates')
在注册任何自定义数据库之前,我们还可以使用 custom.collection 来获取它,这会获取上下文中的集合。
>>> custom.collection('templates') is root_db True
现在让我们为我们的收藏在特定站点注册一个定制数据库。这意味着在该站点,将使用新的定制模板数据库(如果找不到定制或在使用定制时出现错误,将回退到原始数据库)。
首先创建一个站点
>>> site1 = DummySite(id=1)
我们为名为templates的收藏注册一个定制数据库。出于测试目的,我们将使用内存数据库
>>> mem_db = custom.InMemoryTemplateDatabase('templates', 'Templates') >>> from hurry.custom.interfaces import ITemplateDatabase >>> sm1 = site1.getSiteManager() >>> sm1.registerUtility(mem_db, provided=ITemplateDatabase, ... name='templates')
我们进入这个站点
>>> setSite(site1)
现在我们可以使用custom.collection找到这个收藏
>>> custom.collection('templates') is mem_db True
下面的收藏是根收藏
>>> custom.next_collection('templates', mem_db) is root_db True
在这之下没有收藏,我们将得到一个查找错误
>>> custom.next_collection('templates', root_db) Traceback (most recent call last): ... ComponentLookupError: No collection available for: templates
我们还没有在定制数据库中放置任何定制,所以当我们查找模板时,我们会看到之前相同的内容
>>> custom.render('templates', 'test1.st', {'thing': "universe"}) u'Bye universe'
模板定制
现在我们有一个本地设置的定制数据库,我们可以定制test1.st模板。
在这个定制中,我们将“Bye”更改为“Goodbye”
>>> source = root_db.get_source('test1.st') >>> source = source.replace('Bye', 'Goodbye')
现在我们需要更新数据库,使其包含模板的此定制版本。我们通过在数据库上调用带有模板ID和新的源头的update方法来完成此操作。
默认文件系统数据库不支持此更新操作
>>> root_db.update('test1.st', source) Traceback (most recent call last): ... NotSupported: Cannot update templates in FilesystemTemplateDatabase.
不过,它支持我们刚刚安装的站点本地的内存数据库
>>> mem_db.update('test1.st', source)
要连接自己的数据库,只需实现ITemplateDatabase接口并注册它(全局或站点本地的)。
现在让我们看看是否得到了定制的模板
>>> custom.render('templates', 'test1.st', {'thing': 'planet'}) u'Goodbye planet'
损坏的定制模板
如果定制模板无法编译,系统将回退到文件系统模板。我们通过向其中添加&来构建一个损坏的定制模板
>>> original_source = root_db.get_source('test2.st') >>> source = original_source.replace('full of', 'filled with &') >>> mem_db.update('test2.st', source)
我们尝试渲染此模板,但我们会看到原始模板
>>> custom.render('templates', 'test2.st', {'thing': 'planets'}) u"It's full of planets"
也可能出现的情况是,定制模板可以编译,但不能渲染。让我们构建一个期望thang而不是thing的模板
>>> source = original_source.replace('$thing', '$thang') >>> mem_db.update('test2.st', source)
在渲染时,系统会注意到RenderError并回退到原始未定制的模板进行渲染
>>> custom.render('templates', 'test2.st', {'thing': 'planets'}) u"It's full of planets"
检查哪些模板语言被识别
我们可以检查哪些模板语言被识别
>>> languages = custom.recognized_languages() >>> sorted(languages) [(u'.st', <class 'StringTemplate'>)]
当我们注册另一种语言时
>>> class StringTemplate2(StringTemplate): ... pass >>> custom.register_language(StringTemplate2, extension='.st2')
它也会显示出来
>>> languages = custom.recognized_languages() >>> sorted(languages) [(u'.st', <class 'StringTemplate'>), (u'.st2', <class 'StringTemplate2'>)]
检索哪些模板可以定制
对于文件系统级别的模板,可以得到一个数据结构,指示哪些模板可以定制。这在构建UI时很有用。这个数据结构设计得易于作为JSON使用,以便客户端UI可以构建。
让我们检索我们收藏的定制数据库
>>> l = custom.structure('templates') >>> from pprint import pprint >>> pprint(l) [{'extension': '.st', 'name': 'test1', 'path': 'test1.st', 'template': 'test1.st'}, {'extension': '.st', 'name': 'test2', 'path': 'test2.st', 'template': 'test2.st'}]
示例
在定制用户界面中,能够测试模板很有用。有时这可以通过来自软件的实时数据来完成,但在其他情况下,在代表样本数据上尝试它更方便。这些样本数据需要以调用模板时的参数所期望的格式存在。
就像模板语言以纯文本形式存储在文件系统上一样,样本数据也可以以纯文本形式存储在文件系统上。这种纯文本的格式就是它的数据语言。数据语言的例子包括JSON和XML。
为了演示的目的,我们将定义一个简单的数据语言,它可以转换成具有如下键值对的数据文件的字典
>>> data = """\ ... a: b ... c: d ... e: f ... """
现在我们定义一个可以将此数据解析为字典的函数
>>> def parse_dict_data(data): ... result = {} ... for line in data.splitlines(): ... key, value = line.split(':') ... key = key.strip() ... value = value.strip() ... result[key] = value ... return result >>> d = parse_dict_data(data) >>> sorted(d.items()) [('a', 'b'), ('c', 'd'), ('e', 'f')]
这个想法是,我们可以要求特定的模板为其可用的样本输入提供样本。例如,让我们检查 test1.st 模板可用的样本输入。
>>> root_db.get_samples('test1.st') {}
目前还没有。
为了使样本工作,我们首先需要注册数据语言
>>> custom.register_data_language(parse_dict_data, '.d')
现在,扩展名为 .d 的文件可以识别为包含样本数据。
我们还需要告诉系统,特别是StringTemplate模板预计可以找到具有此扩展名的样本数据。为了表达这一点,我们需要再次注册StringTemplate语言,并使用一个额外的参数来指示这一点(sample_extension)
>>> custom.register_language(StringTemplate, ... extension='.st', sample_extension='.d')
现在我们实际上可以查找样本。当然,由于我们还没有创建任何 .d 文件,所以还没有任何样本
>>> root_db.get_samples('test1.st') {}
我们需要一个模式,将样本数据文件与模板文件关联起来。使用的约定是样本数据文件位于与模板文件相同的目录中,并以模板的名称开头,后面跟着一个连字符(-)。连字符后面应该是样本本身的名称。最后,扩展名应该是样本扩展名。这里我们为 test1.st 模板创建一个样本文件
>>> test1_path = os.path.join(templates_path, 'test1-sample1.d') >>> f = open(test1_path, 'w') >>> f.write('thing: galaxy') >>> f.close()
现在,当我们要求我们 test1 模板的样本时,我们应该看到 sample1
>>> r = root_db.get_samples('test1.st') >>> r {'sample1': {'thing': 'galaxy'}}
根据定义,我们可以使用模板的样本数据并将其传递给模板本身
>>> template = custom.lookup('templates', 'test1.st') >>> template(r['sample1']) u'Goodbye galaxy'
测试模板
在用户界面中,能够测试模板是否编译和渲染是有用的。hurry.custom 因此实现了一个 check 函数,用于执行此操作。此函数在出现问题时引发错误(CompileError 或 RenderError),如果没有问题,则静默传递。
让我们首先用损坏的模板试一试
>>> custom.check('templates', 'test1.st', 'foo & bar') Traceback (most recent call last): ... CompileError: & in template!
我们将现在用一个可以编译但不与 sample1 一起工作的模板试一试,因为没有提供 something
>>> custom.check('templates', 'test1.st', 'hello $something') Traceback (most recent call last): ... RenderError: 'something'
错误处理
让我们尝试在一个不存在的集合中渲染一个模板。我们得到一个消息,表示无法找到模板数据库
>>> custom.render('nonexistent', 'dummy.st', {}) Traceback (most recent call last): ... ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplateDatabase>, 'nonexistent')
让我们在一个现有的数据库中渲染一个不存在的模板。我们得到最深数据库的查找错误,假设是文件系统
>>> custom.render('templates', 'nonexisting.st', {}) Traceback (most recent call last): ... IOError: [Errno 2] No such file or directory: '.../nonexisting.st'
让我们尝试渲染一个具有无法识别扩展名的模板
>>> custom.render('templates', 'dummy.unrecognized', {}) Traceback (most recent call last): ... ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplate>, '.unrecognized')
模板语言 .unrecognized 找不到。让我们让文件存在;我们应该得到相同的结果
>>> unrecognized = os.path.join(templates_path, 'dummy.unrecognized') >>> f = open(unrecognized, 'w') >>> f.write('Some weird template language') >>> f.close()
现在我们再次看看
>>> template = custom.render('templates', 'dummy.unrecognized', {}) Traceback (most recent call last): ... ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplate>, '.unrecognized')
如果我们尝试在一个带有 CompileError 的根集合中查找模板,我们将得到一个 CompileError
>>> compile_error = os.path.join(templates_path, 'compileerror.st') >>> f = open(compile_error, 'w') >>> f.write('A & compile error') >>> f.close() >>> compile_error_template = custom.lookup('templates', 'compileerror.st') Traceback (most recent call last): ... CompileError: & in template!
尝试渲染它也适用相同的规则
>>> custom.render('templates', 'compileerror.st', {}) Traceback (most recent call last): ... CompileError: & in template!
如果我们尝试在根集合中渲染模板,我们得到一个 RenderError
>>> render_error = os.path.join(templates_path, 'rendererror.st') >>> f = open(render_error, 'w') >>> f.write('A $thang') >>> f.close() >>> custom.render('templates', 'rendererror.st', {'thing': 'thing'}) Traceback (most recent call last): ... RenderError: u'thang'
如果我们尝试查找一个具有未知 id 的集合,我们将得到一个 ComponentLookupError
>>> custom.collection('unknown_id') Traceback (most recent call last): ... ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplateDatabase>, 'unknown_id')
如果指定的 id 是未知的,我们也不能查找下一个集合
>>> custom.next_collection('unknown_id', mem_db) Traceback (most recent call last): ... ComponentLookupError: No more utilities for <InterfaceClass hurry.custom.interfaces.ITemplateDatabase>, 'unknown_id' have been found.
同样,如果 id 是未知的,我们也不能获取根集合
>>> custom.root_collection('unknown_id') Traceback (most recent call last): ... ComponentLookupError: (<InterfaceClass hurry.custom.interfaces.ITemplateDatabase>, 'unknown_id')
更改
0.6.2 (2009-06-15)
RenderError 和 CompileError 都是从一个共同的 Error 基类派生出来的。
0.6.1 (2009-06-15)
structure 功能现在跳过以点开头的目录和文件
如果不知道数据语言,不要返回任何样本。
0.6 (2009-06-10)
介绍《编译错误》和《渲染错误》的概念。当模板无法解析或编译时,应抛出《编译错误》。如果在模板渲染过程中出现任何运行时错误,应抛出《渲染错误》。
在API中介绍《render》功能,并淡化《lookup》的使用。通常通过调用《render》来渲染模板。
当查找模板并在其创建过程中出现《编译错误》时,回退到原始模板。
当使用顶级《render》函数渲染模板并在渲染过程中出现《渲染错误》时,回退到原始模板。
从《IManagedTemplate》接口中删除《original_source》和《samples》方法。这些方法更适合通过直接使用《ITemplateDatabase》API来处理。
在接口中进行了某些修复,使它们更符合代码。
公开《collection》、《next_collection》和《root_collection》函数。
0.5 (2009-05-22)
首次公开发布。
下载
项目详情
hurry.custom-0.6.2.tar.gz的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | f87e1bba62b8da2b55e0887cd8465e144e88d1aa3bac02f1b4619185a9b32977 |
|
MD5 | 625b638fa2f6b6dc3b04819885c7fa3e |
|
BLAKE2b-256 | df7ccbfac198086bebc8fddb7463bd4719b0699104598ec275072352af37daef |