如何为 ruamel.yaml 创建自定义 yaml 映射转储程序?

2022-01-14 00:00:00 python yaml ruamel.yaml

问题描述

我正在尝试为某些配置对象制作自定义 YAML 转储程序/加载程序.为简单起见,假设我们要将 Hero 类的对象转储到 hero.yml 文件中.

I'm trying to make a custom YAML dumper/loader for some configuration objects. For simplicity, assuming we want to dump a object of class Hero to a hero.yml file.

class Hero:
    yaml_tag = '!Hero'
    def __init__(self, name, age):
        self.name = name
        self.age = age

然后通过ruamel.yaml

yaml.register_class(Hero)

然后尝试转储和加载:

h = Hero('Saber', 15)
with open('config.yml', 'w') as fout:
    yaml.dump(h, fout)
with open('config.yml') as fin:
    yaml.load(fin)

效果很好!

但是,当我需要更灵活的行为时,需要自定义 from_yamlto_yaml 方法,就会出现问题.

However, when I need a more flexible behavior, thus a custom from_yaml and to_yaml method is necessary, there is problem.

Hero的实现改为:

class Hero:
    yaml_tag = '!Hero'
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def to_yaml(cls, representer, data):
        return representer.represent_mapping(cls.yaml_tag, 
                                             {'name': data.name, 'age': data.age})

    @classmethod
    def from_yaml(cls, constructor, node):
        print(node) # for debug
        value = constructor.construct_mapping(node)
        return cls(**value)

翻斗机按预期工作.但是加载未能加载 YAML 文件.一个抛出异常:

The dumper works just as desired. But the load failed to load the YAML file. An Exception is thrown:

    243     def check_mapping_key(self, node, key_node, mapping, key, value):
    244         # type: (Any, Any, Any, Any, Any) -> None
--> 245         if key in mapping:
    246             if not self.allow_duplicate_keys:
    247                 args = [

TypeError: argument of type 'NoneType' is not iterable

通过标有for debugprint(node)行,加载的节点为:

By the print(node) line marked with for debug, the node loaded is:

MappingNode(tag='!Hero', value=[(ScalarNode(tag='tag:yaml.org,2002:str', value='name'), ScalarNode(tag='tag:yaml.org,2002:str', value='Saber')), (ScalarNode(tag='tag:yaml.org,2002:str', value='age'), ScalarNode(tag='tag:yaml.org,2002:int', value='15'))])

不使用默认 dumper/loader 的原因

此示例是显示问题的最小案例,在实际案例中,我试图仅转储对象的一部分,例如

Reason of not using default dumper/loader

This sample is a minimal case to show the problem, in real case, I'm trying to dump only part of the object, like

class A:
    yaml_tag = '!A'
    def __init__(self, name, age):
        self.data = {'name': name, 'age': age}

A('Saber', 15)想要的YAML文件是

!A
name: Saber
age: 15

我不知道在这种情况下如何使默认的转储器/加载器工作.

I do not know how to make the default dumper/loader work in this case.

我的错误在哪里导致失败?如何解决这个问题呢?

Where is my mistake that makes this failed? How to solve this problem?


解决方案

RoundTripConstructor.construct_mapping的定义是::

def construct_mapping(self, node, maptyp=None, deep=False)

并且它需要知道它需要什么样的映射构造.RoundTripDumper 对什么有一些期望可以附加到这样的对象,所以你最好模仿什么RoundTripDumper pass 中的例程:CommentedMap(一个普通的dict 不起作用).

and it needs to know what kind of mapping it is expected to construct. There is some expectation in the RoundTripDumper on what can be attached to such an object, so you best of emulating what the routines in the RoundTripDumper pass: CommentedMap (a normal dict is not going to work).

因此您需要执行以下操作:

So you will need to do something like:

from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap

yaml = YAML()

class Hero:
    yaml_tag = '!Hero'
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def to_yaml(cls, representer, data):
        return representer.represent_mapping(cls.yaml_tag,
                                             {'name': data.name, 'age': data.age})

    @classmethod
    def from_yaml(cls, constructor, node):
        data = CommentedMap()
        constructor.construct_mapping(node, data, deep=True)
        return cls(**data)

    def __str__(self):
        return "Hero(name -> {}, age -> {})".format(self.name, self.age)


yaml.register_class(Hero)

ante_hero = Hero('Saber', 15)
with open('config.yml', 'w') as fout:
    yaml.dump(ante_hero, fout)

with open('config.yml') as fin:
    post_hero = yaml.load(fin)

print(post_hero)

给出:

Hero(name -> Saber, age -> 15)

上面的工作是因为你的类相对简单,如果它可以有的话递归部分,您需要遵循两步创建过程,与创建的对象的初始产量,以便它可以在递归期间使用.

The above works because your class is relatively simple, if it could have recursive parts, you would need to follow a two-step creation process, with initial yield of the object created, so that it can be used during the recursion.

maptyp 默认为 None 是历史的,它必须是放.例如.construct_mapping 做的第一件事就是尝试附加评论(如果节点上有可用的评论).我将删除 0.15.55 中的默认值,这样更明智如果你把它漏掉,就会出错,就像你做的那样.

That the maptyp defaults to None is historical, it must be set. E.g. one of the first things that construct_mapping does is trying to attach comments (if any were available on the node). I'll remove the default value in 0.15.55, which gives a more sensible error if you leave it out, like you did.

相关文章