在PyYAML中创建自定义标签

dor*_*emi 3 python tags pyyaml

我正在尝试使用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)
Run Code Online (Sandbox Code Playgroud)

里面defaults.yaml就是:

example: !ENV foo
Run Code Online (Sandbox Code Playgroud)

我不断得到的错误:

yaml.constructor.ConstructorError: 
could not determine a constructor for the tag '!ENV' in 
"defaults.yaml", line 1, column 10
Run Code Online (Sandbox Code Playgroud)

我计划也有多个自定义标签(假设我可以使这一标签正常工作)

Fre*_*nan 7

您的PyYAML类存在一些问题:

  1. yaml_tag是区分大小写的,!Env并且!ENV是不同的标记。
  2. 因此,根据文档,yaml.YAMLObject使用元类来定义自身,并具有针对这些情况的默认值to_yamlfrom_yaml功能。但是,默认情况下,这些函数要求您对自定义标记(在这种情况下!ENV)的参数是mapping。因此,要使用默认功能,您的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)
Run Code Online (Sandbox Code Playgroud)

运行该程序时,它输出:

{'example': EnvTag(foo, contains=)}
{example: !ENV 'foo'}
Run Code Online (Sandbox Code Playgroud)

此代码的优点是(1)使用原始的pyyaml,因此无需额外安装,并且(2)添加了一个表示符。:)


dor*_*emi 5

作为 Anthon 和 Fredrick Brennan 提供的上述出色答案的附录,我想分享我是如何解决这个问题的。感谢您的帮助。

在我看来,PyYAML 文档并不清楚您何时可能想要通过类(或文档中描述的“元类魔术”)添加构造函数,这可能涉及重新定义from_yamlto_yaml,或者只是添加一个构造函数使用yaml.add_constructor.

事实上,该文档指出:

您可以定义自己的特定于应用程序的标签。最简单的方法是定义 yaml.YAMLObject 的子类

我认为对于更简单的用例,情况正好相反。这是我如何设法实现我的自定义标签。

配置/__init__.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]
Run Code Online (Sandbox Code Playgroud)

有了这个,我现在可以使用 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
Run Code Online (Sandbox Code Playgroud)

示例 production.yaml:

production:
  <<: *defaults
  app:
    host: !ENV APP_HOST
    port: !ENV APP_PORT
Run Code Online (Sandbox Code Playgroud)

使用:

from config import settings
"""
If PYTHON_ENV == 'production', prints value of APP_PORT
If PYTHON_ENV != 'production', prints default 5000
"""
print(settings['app']['port'])
Run Code Online (Sandbox Code Playgroud)