我正在尝试使用Python的PyYAML来创建一个自定义标记,这将允许我使用我的YAML检索环境变量。
import os
import yaml
class EnvTag(yaml.YAMLObject):
yaml_tag = u'!Env'
def __init__(self, env_var):
self.env_var = env_var
def __repr__(self):
return os.environ.get(self.env_var)
settings_file = open('conf/defaults.yaml', 'r')
settings = yaml.load(settings_file)
defaults.yaml
内部只是:
example: !ENV foo
我不断得到的错误:
yaml.constructor.ConstructorError:
could not determine a constructor for the tag '!ENV' in
"defaults.yaml", line 1, column 10
我计划拥有多个自定义标签(假设我可以使用这个标签)
答案 0 :(得分:5)
你的PyYAML课有一些问题:
yaml_tag
区分大小写,因此!Env
和!ENV
是不同的标记。yaml.YAMLObject
使用元类来定义自己,并为这些情况提供默认的to_yaml
和from_yaml
函数。但是,默认情况下,这些函数要求您的自定义标记(在本例中为!ENV
)的参数为映射。因此,要使用默认函数,您的defaults.yaml
文件必须如下所示(仅作为示例):
example: !ENV {env_var: "PWD", test: "test"}
您的代码将继续保持不变,在我的情况下print(settings)
现在导致{'example': /home/Fred}
但您使用的是load
而不是safe_load
- 回答如下,Anthon指出这很危险,因为解析后的YAML可以在磁盘上的任何地方覆盖/读取数据。
您仍然可以轻松使用您的YAML文件格式example: !ENV foo
- 您只需在类to_yaml
中定义适当的from_yaml
和EnvTag
,即可以解析和发出标量变量,如字符串“foo”。
所以:
import os
import yaml
class EnvTag(yaml.YAMLObject):
yaml_tag = u'!ENV'
def __init__(self, env_var):
self.env_var = env_var
def __repr__(self):
v = os.environ.get(self.env_var) or ''
return 'EnvTag({}, contains={})'.format(self.env_var, v)
@classmethod
def from_yaml(cls, loader, node):
return EnvTag(node.value)
@classmethod
def to_yaml(cls, dumper, data):
return dumper.represent_scalar(cls.yaml_tag, data.env_var)
# Required for safe_load
yaml.SafeLoader.add_constructor('!ENV', EnvTag.from_yaml)
# Required for safe_dump
yaml.SafeDumper.add_multi_representer(EnvTag, EnvTag.to_yaml)
settings_file = open('defaults.yaml', 'r')
settings = yaml.safe_load(settings_file)
print(settings)
s = yaml.safe_dump(settings)
print(s)
运行此程序时,输出:
{'example': EnvTag(foo, contains=)}
{example: !ENV 'foo'}
此代码的好处是(1)使用原始的pyyaml,因此没有额外的安装和(2)添加一个代表。 :)
答案 1 :(得分:1)
我想分享一下我如何解决这个问题,作为Anthon和Fredrick Brennan提供的上述伟大答案的附录。谢谢你的帮助。
在我看来,PyYAML文档并不清楚你何时想要通过类添加构造函数(或者#34;元类魔法"如文档中所述),这可能是涉及重新定义from_yaml
和to_yaml
,或者只是使用yaml.add_constructor
添加构造函数。
事实上,doc州:
您可以定义自己的应用程序特定标签。最简单的方法是定义yaml.YAMLObject
的子类
我认为对于更简单的用例,情况正好相反。以下是我设法实现自定义标记的方法。
<强>配置/ __初始化__。PY 强>
import yaml
import os
environment = os.environ.get('PYTHON_ENV', 'development')
def __env_constructor(loader, node):
value = loader.construct_scalar(node)
return os.environ.get(value)
yaml.add_constructor(u'!ENV', __env_constructor)
# Load and Parse Config
__defaults = open('config/defaults.yaml', 'r').read()
__env_config = open('config/%s.yaml' % environment, 'r').read()
__yaml_contents = ''.join([__defaults, __env_config])
__parsed_yaml = yaml.safe_load(__yaml_contents)
settings = __parsed_yaml[environment]
有了这个,我现在可以使用env PTYHON_ENV
(default.yaml,development.yaml,test.yaml,production.yaml)为每个环境分别创建一个yaml。现在每个人都可以参考ENV变量。
示例default.yaml:
defaults: &default
app:
host: '0.0.0.0'
port: 500
示例production.yaml:
production:
<<: *defaults
app:
host: !ENV APP_HOST
port: !ENV APP_PORT
使用:
from config import settings
"""
If PYTHON_ENV == 'production', prints value of APP_PORT
If PYTHON_ENV != 'production', prints default 5000
"""
print(settings['app']['port'])
答案 2 :(得分:1)
如果您的目标是查找并替换yaml文件中定义的环境变量(作为字符串),则可以使用以下方法:
example.yaml:
foo: !ENV "Some string with ${VAR1} and ${VAR2}"
example.py:
import yaml
# Define the function that replaces your env vars
def env_var_replacement(loader, node):
replacements = {
'${VAR1}': 'foo',
'${VAR2}': 'bar',
}
s = node.value
for k, v in replacements.items():
s = s.replace(k, v)
return s
# Define a loader class that will contain your custom logic
class EnvLoader(yaml.SafeLoader):
pass
# Add the tag to your loader
EnvLoader.add_constructor('!ENV', env_var_replacement)
# Now, use your custom loader to load the file:
with open('example.yaml') as yaml_file:
loaded_dict = yaml.load(yaml_file, Loader=EnvLoader)
# Prints: "Some string with foo and bar"
print(loaded_dict['foo'])
值得注意的是,您不一定需要创建自定义EnvLoader
类。您可以直接在add_constructor
类或SafeLoader
模块本身上调用yaml
。但是,将加载程序全局添加到所有其他依赖于这些加载程序的模块中会产生意外的副作用,如果其他模块具有用于加载!ENV
标签的自定义逻辑,则可能会引起问题。< / p>
答案 3 :(得分:0)
您的代码存在一些问题:
!Env
与代码中的!ENV
不同。classmethod
提供的from_yaml
EnvTag
。!Env
的标量,但yaml.YAMLObject
的子类化机制调用construct_yaml_object
,后者又调用construct_mapping
,因此不允许使用标量。.load()
。这是不安全,除非您现在和将来都可以完全控制YAML输入。不受控制的YAML可以例如不安全擦除或上传光盘中的任何信息。 PyYAML并没有警告你可能造成的损失。__repr__
不会返回字符串,这会引发错误。所以将代码更改为:
import sys
import os
from ruamel import yaml
yaml_str = """\
example: !Env foo
"""
class EnvTag:
yaml_tag = u'!Env'
def __init__(self, env_var):
self.env_var = env_var
def __repr__(self):
return os.environ.get(self.env_var, '')
@staticmethod
def yaml_constructor(loader, node):
return EnvTag(loader.construct_scalar(node))
yaml.add_constructor(EnvTag.yaml_tag, EnvTag.yaml_constructor,
constructor=yaml.SafeConstructor)
data = yaml.safe_load(yaml_str)
print(data)
os.environ['foo'] = 'Hello world!'
print(data)
给出:
{'example': }
{'example': Hello world!}
请注意我正在使用ruamel.yaml
(免责声明:我是该软件包的作者),因此您可以在YAML文件中使用YAML 1.2(或1.1)。只需稍作修改,您就可以使用旧的PyYAML进行上述操作。
您也可以通过YAMLObject
的子类化以安全的方式完成此操作:
import sys
import os
from ruamel import yaml
yaml_str = """\
example: !Env foo
"""
yaml.YAMLObject.yaml_constructor = yaml.SafeConstructor
class EnvTag(yaml.YAMLObject):
yaml_tag = u'!Env'
def __init__(self, env_var):
self.env_var = env_var
def __repr__(self):
return os.environ.get(self.env_var, '')
@classmethod
def from_yaml(cls, loader, node):
return EnvTag(loader.construct_scalar(node))
data = yaml.safe_load(yaml_str)
print(data)
os.environ['foo'] = 'Hello world!'
print(data)
这将为您提供与上述相同的结果。