问题
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