How to create a custom yaml mapping dumper for ruamel.yaml?

人走茶凉 提交于 2019-12-01 07:57:47

问题


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.

The example which works with default dumper/loader

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

Then add the default loader/dumper by ruamel.yaml

yaml.register_class(Hero)

And try dump and load:

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

It works perfectly!

But failed with custom to_yaml and from_yaml methods

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

The implementation of Hero is changed to:

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)

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

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'))])

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}

The desired YAML file of A('Saber', 15) is

!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?


回答1:


The defintion for the RoundTripConstructor.construct_mapping is::

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

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)

which gives:

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.

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.



来源:https://stackoverflow.com/questions/46806918/how-to-create-a-custom-yaml-mapping-dumper-for-ruamel-yaml

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!