跳转到主要内容

允许自定义模板的框架

项目描述

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 函数,用于执行此操作。此函数在出现问题时引发错误(CompileErrorRenderError),如果没有问题,则静默传递。

让我们首先用损坏的模板试一试

>>> 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 (26.3 kB 查看哈希)

上传时间

由以下机构支持