在Python代码中嵌入配置信息。
项目描述
一个从Python代码中解析配置的库。
火星人教程
简介
“有很多东西要理解,但能理解的却很少。” —— 罗伯特·A·海因莱因的《异乡异客》
火星人提供了Python代码声明性配置的基础设施。火星人特别适用于需要提供灵活插件基础设施的框架构建。火星人实际上并不提供插件注册的基础设施(除了自身)。许多框架都有自己的系统,如果您需要通用系统,您可能想考虑zope.component。火星人仅仅允许您使插件注册更加简洁。
您可以将火星人看作是做了一些您也可以用元类解决的问题,具有以下优点
框架的开发者不再需要编写大量的自定义元类;相反,我们提供了一个使生活更轻松的基础设施。
配置不需要在导入时发生,但可以在程序启动时发生。这也使得配置对开发者来说更容易处理。
我们不干扰使用框架的开发者,因为元类有时会带来令人惊讶的行为。用户必须处理的类是普通类。
为什么这个包命名为火星人?在小说《异乡异客》中,动词grok被引入
Grok意味着理解得如此透彻,以至于观察者成为被观察者的一部分——融合,混合,联姻,在团体经历中失去个性。
在这个包的上下文中,“grokking”代表从Python代码中推断声明性配置动作的过程。在小说中,grokking最初是一个来自火星的概念。火星人grok。因为这个包帮助您理解代码,所以命名为火星人。
火星人提供了一个框架,允许使用声明性Python代码来表示配置。这些声明通常可以从代码本身的结构中推断出来。想法是使这些声明尽可能简单、易于阅读,即使广泛的配置也不会过度负担与代码一起工作的程序员。
火星人包是从Grok项目中分离出来的,在这个项目中,这个代码库最初被开发。虽然Grok使用了它,但代码完全独立于Grok。
动机
“从Python代码中推断声明性配置动作”——这听起来非常抽象。它实际上意味着什么?什么是配置?什么是声明性配置?为了解释这一点,我们首先来看一下配置。
较大的框架通常提供了很多可以修改其行为的地方:将自身组件与您提供的组件结合起来构建更大的应用程序的方式。一个框架提供了可以配置的插件代码的配置点。当您将一些代码插入到插件点时,它会导致某个注册表中新插件的更新。当框架使用插件时,它首先会在注册表中查找。将某些组件注册到注册表中的操作可以称为“配置”。
让我们看看一个提供插件点的示例框架。我们介绍一个非常简单的框架,用于插入不同的模板语言,每种模板语言都使用自己的扩展。然后,您可以向框架提供模板主体、模板扩展和一些数据,并渲染模板。
让我们看看这个框架
>>> import string >>> class templating(FakeModule): ... ... class InterpolationTemplate(object): ... "Use %(foo)s for dictionary interpolation." ... def __init__(self, text): ... self.text = text ... def render(self, **kw): ... return self.text % kw ... ... class TemplateStringTemplate(object): ... "PEP 292 string substitutions." ... def __init__(self, text): ... self.template = string.Template(text) ... def render(self, **kw): ... return self.template.substitute(**kw) ... ... # the registry, we plug in the two templating systems right away ... extension_handlers = { '.txt': InterpolationTemplate, ... '.tmpl': TemplateStringTemplate } ... ... def render(data, extension, **kw): ... """Render the template at filepath with arguments. ... ... data - the data in the file ... extension - the extension of the file ... keyword arguments - variables to interpolate ... ... In a real framework you could pass in the file path instead of ... data and extension, but we don't want to open files in our ... example. ... ... Returns the rendered template ... """ ... template = extension_handlers[extension](data) ... return template.render(**kw)
由于我们通常无法在doctest中创建模块,所以我们使用FakeModule类模拟了Python模块的模板。每当您看到FakeModule子类时,想象您正在查看一个.py文件中的模块定义。现在我们已经定义了一个名为templating的模块,我们还需要能够导入它。假模块总是自动放置在martiantest.fake命名空间中,因此您可以从那里导入它们
>>> from martiantest.fake import templating
现在让我们尝试注册模板类型的render函数,以演示我们的框架是否工作
>>> templating.render('Hello %(name)s!', '.txt', name="world") 'Hello world!' >>> templating.render('Hello ${name}!', '.tmpl', name="universe") 'Hello universe!'
我们不认识的文件扩展名会导致抛出KeyError
>>> templating.render('Hello', '.silly', name="test") Traceback (most recent call last): ... KeyError: '.silly'
我们现在想将这个文件处理器框架集成进去,并为.silly文件提供一个处理器。由于我们正在编写一个插件,我们不能直接更改templating模块。让我们编写一个扩展模块
>>> class sillytemplating(FakeModule): ... class SillyTemplate(object): ... "Replace {key} with dictionary values." ... def __init__(self, text): ... self.text = text ... def render(self, **kw): ... text = self.text ... for key, value in kw.items(): ... text = text.replace('{%s}' % key, value) ... return text ... ... templating.extension_handlers['.silly'] = SillyTemplate >>> from martiantest.fake import sillytemplating
在扩展模块中,我们操作templating模块的extension_handlers字典(在正常代码中我们需要先导入它),并插入我们自己的函数。.silly处理现在正常工作
>>> templating.render('Hello {name}!', '.silly', name="galaxy") 'Hello galaxy!'
如上所述,我们使用Python代码将我们的extension_handler注册表集成在一起。使用单独的代码手动将组件钩接到注册表中可能会变得相当繁琐——每次您编写一个插件时,您也需要记住您需要注册它。
在Python代码中进行模板注册也可能会带来维护风险。在Python代码中开始进行复杂的操作,如条件配置,可能会使得程序的配置状态难以理解。另一个问题是,在导入时进行配置也可能导致导入时的副作用,以及顺序问题,您可能希望导入一个需要配置状态的模块,而该模块是在稍后导入的。最后,它还可能使代码更难测试,因为配置总是在导入模块时加载,即使可能在您的测试中您并不希望这样。
Martian提供了一个框架,允许使用声明性Python代码表达配置。Martian基于这样的认识:通常可以从Python代码的结构中推断出配置在哪里,特别是当它可以添加额外的声明时。这个想法是使编写和注册插件变得非常容易,以至于即使是大量的配置也不会过度负担开发者。
配置操作在单独的阶段(“grok时间”)执行,而不是在导入时间,这使得推理更容易,也更容易测试。
使用Martian方式配置
现在让我们将上面的templating模块和sillytemplating模块转换为使用Martian。首先我们必须认识到,每个模板语言都配置为针对特定的扩展工作。使用Martian,我们使用这个配置信息注解类本身。注解通过使用在类体中的函数调用样式的指令来发生。
让我们创建一个可以接受单个字符串作为参数的extension指令,这个参数是要为模板类注册的文件扩展名
>>> import martian >>> class extension(martian.Directive): ... scope = martian.CLASS ... store = martian.ONCE ... default = None
我们还需要一种方法来轻松识别所有模板类。在Martian中,这种做法通常是通过使用一个基类,所以让我们定义一个Template基类
>>> class Template(object): ... pass
我们现在有了足够的基础设施,可以让我们更改代码以使用Martian风格的基类和注解
>>> class templating(FakeModule): ... ... class InterpolationTemplate(Template): ... "Use %(foo)s for dictionary interpolation." ... extension('.txt') ... def __init__(self, text): ... self.text = text ... def render(self, **kw): ... return self.text % kw ... ... class TemplateStringTemplate(Template): ... "PEP 292 string substitutions." ... extension('.tmpl') ... def __init__(self, text): ... self.template = string.Template(text) ... def render(self, **kw): ... return self.template.substitute(**kw) ... ... # the registry, empty to start with ... extension_handlers = {} ... ... def render(data, extension, **kw): ... # this hasn't changed ... template = extension_handlers[extension](data) ... return template.render(**kw) >>> from martiantest.fake import templating
如您所见,变化非常少
我们将模板类继承自Template。
我们在模板类中使用了extension指令。
我们已停止预先填充 extension_handlers 字典。
那么我们该如何用正确的模板语言填充 extension_handlers 字典呢?现在我们可以使用 Martian。我们定义了一个用于 Template 的 grokker,它在 extension_handlers 注册表中注册模板类。
>>> class meta(FakeModule): ... class TemplateGrokker(martian.ClassGrokker): ... martian.component(Template) ... martian.directive(extension) ... def execute(self, class_, extension, **kw): ... templating.extension_handlers[extension] = class_ ... return True >>> from martiantest.fake import meta
这会做什么?ClassGrokker 的 execute 方法会在 martian.component 指令所指示的子类上被调用。您也可以通过使用 martian.directive()(即 directive 指令!)一次或多次来声明 ClassGrokker 对此组件期望的指令。
execute 方法接收要解析的类作为第一个参数,并通过额外的参数将使用指令的值传递给 execute 方法。框架也可以在解析过程中传递任意数量的额外关键字参数,因此我们需要声明 **kw 以确保我们可以处理这些。
我们所有的 grokkers 都将收集到一个特殊的 Martian 特定注册表中。
>>> reg = martian.GrokkerRegistry()
首先,我们需要确保系统知道在 meta 模块中定义的 TemplateGrokker,所以让我们先注册它。我们可以通过简单地解析 meta 模块来完成此操作。
>>> reg.grok('meta', meta) True
因为 TemplateGrokker 现已注册,我们的注册表现在知道如何解析 Template 子类。让我们解析 templating 模块。
>>> reg.grok('templating', templating) True
让我们再次尝试 templating 的 render 函数,以演示我们已经成功解析了模板类。
>>> templating.render('Hello %(name)s!', '.txt', name="world") 'Hello world!' >>> templating.render('Hello ${name}!', '.tmpl', name="universe") 'Hello universe!'
.silly 尚未注册。
>>> templating.render('Hello', '.silly', name="test") Traceback (most recent call last): ... KeyError: '.silly'
现在让我们从扩展模块中注册 .silly。
>>> class sillytemplating(FakeModule): ... class SillyTemplate(Template): ... "Replace {key} with dictionary values." ... extension('.silly') ... def __init__(self, text): ... self.text = text ... def render(self, **kw): ... text = self.text ... for key, value in kw.items(): ... text = text.replace('{%s}' % key, value) ... return text >>> from martiantest.fake import sillytemplating
如您所见,使用框架的开发者不再需要了解 templating.extension_handlers。相反,我们可以简单地解析模块,使 SillyTemplate 正确注册。
>>> reg.grok('sillytemplating', sillytemplating) True
现在我们也可以使用 .silly 模板引擎了。
>>> templating.render('Hello {name}!', '.silly', name="galaxy") 'Hello galaxy!'
诚然,用如此小的示例很难很好地展示 Martian。毕竟,我们实际上编写的代码比基本框架更多。但即使在这个小示例中,templating 和 sillytemplating 模块已经变得更加声明性。使用框架的开发者不再需要了解诸如 templating.extension_handlers 或注册东西的 API 等事情。相反,开发人员可以在任何地方注册新的模板系统,只要他继承自 Template,并且只要他的代码被系统解析。
最后,请注意 Martian 是如何被用来定义 TemplateGrokker 的。以这种方式,Martian 可以使用自己来扩展自己。
解析实例
上面我们看到了如何解析类。Martian 还提供了一种解析实例的方法。这在典型框架中不太常见,并且有一个缺点,即不能使用类级别的指令,但仍然很有用。
让我们想象一个场景,我们有一个动物园框架,其中有一个 Animal 类,我们想要跟踪它的实例。
>>> class Animal(object): ... def __init__(self, name): ... self.name = name >>> class zoo(FakeModule): ... horse = Animal('horse') ... chicken = Animal('chicken') ... elephant = Animal('elephant') ... lion = Animal('lion') ... animals = {} >>> from martiantest.fake import zoo
我们定义一个用于解析 Animal 实例的 InstanceGrokker 子类。
>>> class meta(FakeModule): ... class AnimalGrokker(martian.InstanceGrokker): ... martian.component(Animal) ... def execute(self, instance, **kw): ... zoo.animals[instance.name] = instance ... return True >>> from martiantest.fake import meta
让我们创建一个新的包含 AnimalGrokker 的注册表。
>>> reg = martian.GrokkerRegistry() >>> reg.grok('meta', meta) True
现在我们可以解析 zoo 模块。
>>> reg.grok('zoo', zoo) True
动物现在将存储在 animals 字典中
>>> sorted(zoo.animals.items()) [('chicken', <Animal object at ...>), ('elephant', <Animal object at ...>), ('horse', <Animal object at ...>), ('lion', <Animal object at ...>)]
更多信息
有关更多种类的grokkers的详细信息,请参阅 src/martian/core.txt。有关指令的更多信息,请参阅 src/martian/directive.txt。
变更记录
2.0.post1 (2023-03-23)
在 setup.py 中添加缺失的 python_requires,以防止在过旧的版本上安装。
2.0 (2023-03-23)
添加对 Python 3.11 的支持。
停止支持 Python 2.7、3.5 和 3.6。
修复测试套件与 zope.interface >= 6.0 的不兼容性。
1.5 (2022-02-11)
标题指令现在可以正确处理非ASCII字符。
修复测试套件与 zope.interface >= 5.0 的不兼容性。
停止支持 Python 3.4。
添加对 Python 3.9 和 3.10 的支持。
1.4 (2020-02-23)
在检查 __builtin__(Python 2)的所有地方检查 builtins(Python 3)。
1.3.post1 (2019-03-14)
修复 PyPI 页面的渲染。
1.3 (2019-03-14)
添加对 Python 3.7 和 3.8 的支持。
1.2 (2018-05-09)
添加一个新的指令 martian.ignore(),以显式地在模块中不处理某些内容。
class Example: pass martian.ignore('Example')
修复代码以符合 pep 8 标准。
1.1 (2018-01-25)
绕过引导,将覆盖率添加到 tox。
修复 python3 中 inspect.getargspec() 的弃用。
1.0 (2017-10-19)
添加对 Python 3.5、3.6、PyPy2 和 PyPy3 的支持。
停止支持 Python 2.6 和 3.3。
0.15 (2015-04-21)
python 3 兼容性
调整 egg 以与较新的 setuptools 版本兼容
修复 doctests 在 Python-2.7 下的编码问题。
0.14 (2010-11-03)
功能变更
现在可以在指令类定义内部定义指令的默认值计算。每当存在 get_default 类方法时,它将用于计算默认值
class name(Directive): scope = CLASS store = ONCE @classmethod def get_default(cls, component, module=None, **data): return component.__name__.lower()
在绑定指令时,可以通过传递 get_default 函数来覆盖默认默认行为
def another_default(component, module=None, **data): return component.__name__.lower() name.bind(get_default=another_default).get(some_component)
将默认行为内置于指令中,可以防止在获取值时(例如在 grokkers 中)反复传递 get_default 函数。
0.13 (2010-11-01)
功能变更
忽略所有 __main__ 模块。
将 zope.testing 列为测试依赖项。
0.12 (2009-06-29)
功能变更
为了更好地支持与指令结合使用的各种继承场景,进行了以下更改。
CLASS_OR_MODULE 范围指令将了解模块范围内定义的值的继承。考虑以下情况
module a: some_directive('A') class Foo(object): pass module b: import a class Bar(a.Foo): pass
如之前所述,Foo 将为其配置值 A。由于 Bar 继承自 Foo,因此它将继承此值。
CLASS_OR_MODULE 和 CLASS 范围指令将了解计算默认值的继承。考虑以下情况
module a: class Foo(object): pass module b: import a class Bar(a.Foo): pass def get_default(component, module, **data): if module.__name__ == 'a': return "we have a default value for module a" return martian.UNKNOWN
当我们现在这样做时
some_directive.bind(get_default=get_default).get(b.Bar)
我们将得到“我们为模块 a 定义了默认值”的值。这是因为当尝试为 Bar 计算默认值时,我们返回 martian.UNKNOWN 以指示值尚未找到。然后系统查看基类并再次尝试,在这种情况下它成功了(因为模块名称是 a)。
martian.ONCE_IFACE 存储选项,允许创建将其值存储在 zope.interface 接口上的指令。这最初在 grokcore.view 中,但具有更广泛的使用价值。
修复的 bug
忽略那些看起来像是Python模块和包但实际上不是的东西。这些有时是由编辑器、操作系统和网络文件系统创建的,我们不希望混淆它们。
如果 ignore_nonsource 参数为 True,则忽略没有匹配 .py 文件的 .pyc 和 .pyo 文件,通过 module_info_from_dotted_name。默认值为 True。要恢复到更早的行为,即尊重 .pyc 文件,请传递 ignore_nonsource=False。
当它递归调用自身时,将 exclude_filter(以及新的 ignore_nonsource 标志)传递给 ModuleInfo 构造函数。
将 fake_import 替换为测试中导入假模块的真实 Python 导入语句(from martiantest.fake import my_fake_module)。这是通过引入一个 FakeModule 的元类来实现的,该元类会自动将其注册为模块。这种讽刺意味逃不过我们的眼睛。这也意味着现在 martian.scan.resolve() 也可以在假模块上工作。
0.11 (2008-09-24)
功能变更
为指令存储添加了 MULTIPLE_NOBASE 选项。这类似于 MULTIPLE,但不从基类继承信息。
0.10 (2008-06-06)
功能变更
为指令添加了一个 validateClass 验证函数。
将 FakeModule 和 fake_import 移入一个 martian.testing 模块,以便外部包可以重用它们。
将新的教程文本作为 README.txt 引入。之前在 README.txt 中的文本对教程来说太详细了,因此已移动到 core.txt。
引入了一个 GrokkerRegistry 类,它是一个具有 MetaMultiGrokker 的 ModuleGrokker。这是开始使用 Grok 的便捷方式,并在教程中进行了演示。
引入了三个新的 Martian-specific 指令:martian.component、martian.directive 和 martian.priority。这些指令替换了 component_class、directives 和 priority 类级别属性。这样,Grokkers 的外观与它们解析的内容相同。这又破坏了向后兼容性,但它是一种简单的替换操作。请注意,martian.directive 接收指令本身作为参数,然后可选地接收与指令的 bind 方法相同的参数(name、default 和 get_default)。它可以多次使用。请注意,martian.baseclass 已经是一个 Martian-specific 指令,并且这一部分没有改变。
为了对称性,为 InstanceGrokker 添加了一个 execute 方法。
0.9.7 (2008-05-29)
功能变更
为那些想要解析类的方法而不是整个类本身的 Grokkers 添加了一个 MethodGrokker 基类。它在大体上与 ClassGrokker 相似,关于指令定义,但指令不仅评估类(和可能模块)级别,也针对每个方法。这样,如果它们支持的话,指令也可以应用于方法(作为装饰器)。
0.9.6 (2008-05-14)
功能变更
再次重构了 martian.Directive 基类,以允许在 Grokkers 中有更多声明性(而不是命令性)的使用。指令本身不再有 get() 方法或默认值工厂(get_default())。相反,您必须首先“绑定”指令,这通常在 Grokker 中完成。
扩展了基类 ClassGrokker,添加了一个标准的 grok() 方法,允许您简单地声明一组在解析的类上使用的指令。然后,您只需要实现一个 execute() 方法,该方法将接收来自这些指令的关键字参数。这大大简化了类解析器的实现。
0.9.5 (2008-05-04)
scan_for_classes 只需要一个指定接口的单个第二个参数。由于 grokcore.component 的变化,已删除直接扫描子类的支持。
0.9.4 (2008-05-04)
特性变更
将各种指令基类替换为单个 martian.Directive 基类
现在使用 scope 类属性定义指令作用域,可以是 martian.CLASS、martian.MODULE 或 martian.CLASS_OR_MODULE 之一。
使用 store 类属性定义存储类型,可以是 martian.ONCE、martian.MULTIPLE 或 martian.DICT 之一。
现在指令获得了使用 get() 方法读取它们在组件或模块上设置值的权限。因此,已经移除了 class_annotation 和 class_annotation_list 辅助工具。
将 baseclass() 指令从 Grok 移至 Martian。
添加了一个与 check_implements_one 类似的辅助工具 martian.util.check_provides_one。
scan_for_classes 辅助工具现在也接受一个 interface 参数,这使得您可以根据接口而不是基类来扫描类。
错误修复
向 BuiltinModuleInfo 添加了虚拟的 package_dotted_name。这允许使用 Grok 的 grok.testing.grok_component 在测试代码中解析视图,而不会在设置静态属性时失败。
不再使用以 -Base 结尾的类将被视为基类的约定。现在必须显式使用 grok.baseclass() 指令。
类类型检查现在使用 isinstance() 而不是 type(),这意味着 Grok 可以与 Zope 2 扩展类和元类编程一起工作。
0.9.3 (2008-01-26)
功能变更
添加了 OptionalValueDirective,允许构建接受零个或一个参数的指令。如果没有提供参数,则调用指令的 default_value 方法。子类需要重写此方法以返回要使用的默认值。
重构
将一些真正的 grok 特定的 util 函数从 Martian 移回 Grok。
0.9.2 (2007-11-20)
错误修复
当 scan.module_info_from_dotted_name() 遇到 __builtin__ 时,现在具有特殊行为。以前,它会因错误而崩溃。现在,它将返回 BuiltinModuleInfo 的实例。这是一个非常简单的实现,仅提供足够的信息使客户端代码正常工作。通常,此类客户端代码与测试相关,因此模块上下文将是 __builtin__。
0.9.1 (2007-10-30)
功能变更
Grokkers 现在接收一个 module_info 关键字参数。由于不显式接受 module_info 的 grokkers 会将额外的参数吸收到 **kw 中,因此此更改与向后兼容性完全一致。
0.9 (2007-10-02)
功能变更
撤销了默认跳过调用测试或 ftests 模块的默认行为,并添加了一个 API 以提供用于跳过要解析的模块的过滤函数。
0.8.1 (2007-08-13)
功能变更
不解析测试或 ftests 模块。
修复的 bug
修复了一个错误,该错误会导致如果类有多个基类,则最终列表中会多次出现。
0.8 (2007-07-02)
功能变更
初始公共版本。
项目详情
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。