在PyYAML中创建自定义标记

时间:2017-03-27 23:21:58

标签: 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)

defaults.yaml内部只是:

example: !ENV foo

我不断得到的错误:

yaml.constructor.ConstructorError: 
could not determine a constructor for the tag '!ENV' in 
"defaults.yaml", line 1, column 10

我计划拥有多个自定义标签(假设我可以使用这个标签)

4 个答案:

答案 0 :(得分:5)

你的PyYAML课有一些问题:

  1. yaml_tag区分大小写,因此!Env!ENV是不同的标记。
  2. 因此,根据文档,yaml.YAMLObject使用元类来定义自己,并为这些情况提供默认的to_yamlfrom_yaml函数。但是,默认情况下,这些函数要求您的自定义标记(在本例中为!ENV)的参数为映射。因此,要使用默认函数,您的defaults.yaml文件必须如下所示(仅作为示例):
  3.   

    example: !ENV {env_var: "PWD", test: "test"}

    您的代码将继续保持不变,在我的情况下print(settings)现在导致{'example': /home/Fred} 但您使用的是load而不是safe_load - 回答如下,Anthon指出这很危险,因为解析后的YAML可以在磁盘上的任何地方覆盖/读取数据。

    您仍然可以轻松使用您的YAML文件格式example: !ENV foo - 您只需在类to_yaml中定义适当的from_yamlEnvTag,即可以解析和发出标量变量,如字符串“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_yamlto_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)

您的代码存在一些问题:

    您的YAML文件中的
  • !Env与代码中的!ENV不同。
  • 您错过了必须为classmethod提供的from_yaml EnvTag
  • 您的YAML文档指定!Env的标量,但yaml.YAMLObject的子类化机制调用construct_yaml_object,后者又调用construct_mapping,因此不允许使用标量。
  • 您正在使用.load()。这是不安全,除非您现在和将来都可以完全控制YAML输入。不受控制的YAML可以例如不安全擦除或上传光盘中的任何信息。 PyYAML并没有警告你可能造成的损失。
  • PyYAML仅支持大部分YAML 1.1,最新的YAML规范是1.2(从2009年开始)。
  • 您应该始终在每个级别的4个空格(或3个空格,但在第一个空间不是4个,在下一个级别为3个)缩进代码。
  • 如果未设置环境变量,则__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)

这将为您提供与上述相同的结果。