开发者

Default constructor parameters in pyyaml

开发者 https://www.devze.com 2023-03-31 14:33 出处:网络
I haven\'t been able to find out how to do this in the PyYAML documentation. I want to represent python classes I\'ve defined in YAML, and have a default value given to a parameter in the constructor

I haven't been able to find out how to do this in the PyYAML documentation. I want to represent python classes I've defined in YAML, and have a default value given to a parameter in the constructor if it's not specified in the YAML. For examp开发者_JAVA百科le:

>>> class Test(yaml.YAMLObject):
...     yaml_tag = u"!Test"
...     def __init__(self, foo, bar=3):
...             self.foo = foo
...             self.bar = bar
...     def __repr__(self):
...             return "%s(foo=%r, bar=%r)" % (self.__class__.__name__, self.foo, self.bar)
... 
>>> yaml.load("""
... --- !Test
... foo: 5
... """)
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 7, in __repr__
AttributeError: 'Test' object has no attribute 'bar'

I expected that it would create a Test object with bar=3, but I guess it bypasses my constructor when it creates the object. If I include a mapping for bar in the YAML, everything works as expected:

>>> yaml.load("""
... --- !Test
... foo: 5
... bar: 42
... """)
Test(foo=5, bar=42)

Does anyone know how I can have it use a default value?


I encountered the same problem: yaml_tag doesn't work for some reason. So I used alternative approach:

import yaml

def constructor(loader, node) :
    fields = loader.construct_mapping(node)
    return Test(**fields)

yaml.add_constructor('!Test', constructor)

class Test(object) :
    def __init__(self, foo, bar=3) :
        self.foo = foo
        self.bar = bar
    def __repr__(self):
        return "%s(foo=%r, bar=%r)" % (self.__class__.__name__, self.foo, self.bar)

print yaml.load("""
- !Test { foo: 1 }
- !Test { foo: 10, bar: 20 }""")

Output:

[Test(foo=1, bar=3), Test(foo=10, bar=20)]


Based on alexanderlukanin13's answer. Here's my cut.

import yaml

YAMLObjectTypeRegistry = {}

def register_type(target):
    if target.__name__ in YAMLObjectTypeRegistry:
        print "{0} already in registry.".format(target.__name__)
    elif 'yaml_tag' not in target.__dict__.keys():
        print target.__dict__
        raise TypeError("{0} must have yaml_tag attribute".format(
            target.__name__))
    elif target.__dict__['yaml_tag'] is None:
        pass
    else:
        YAMLObjectTypeRegistry[target.__name__] = target
        yaml.add_constructor(
                target.__dict__['yaml_tag'],
                lambda loader, node: target(**loader.construct_mapping(node)))
        print "{0} added to registry.".format(target.__name__)

class RegisteredYAMLObjectType(type):
    def __new__(meta, name, bases, class_dict):
        cls = type.__new__(meta, name, bases, class_dict)
        register_type(cls)
        return cls

class RegisteredYAMLObject(object):
    __metaclass__=RegisteredYAMLObjectType
    yaml_tag = None

You can then use it like this:

class MyType(registry.RegisteredYAMLObject):
    yaml_tag = u'!mytype'
    def __init__(self, name, attr1='default1', attr2='default2'):
        super(MyType, self).__init__()
        self.name = name
        self.attr1 = attr1
        self.attr2 = attr2


The answers above work well, but here is a way to make initialisation work fully with a class based approach:

UNSPECIFIED = object()

class SomeYAMLObject(yaml.YAMLObject):
    @classmethod 
    def from_yaml(cls, loader, node):
        arg_spec = inspect.getfullargspec(cls.__init__)
        arg_spec.args.remove("self")

        arg_defaults = reversed(list(
                zip_longest(
                    reversed(arg_spec.args),
                    reversed(arg_spec.defaults or []),
                    fillvalue=UNSPECIFIED)))

        kwarg_defaults = reversed(list(
                    zip_longest(
                        reversed(arg_spec.kwonlyargs),
                        reversed(arg_spec.kwonlydefaults or []),
                        fillvalue=UNSPECIFIED)))

        node_mapping = loader.construct_mapping(node)
        used_nodes = set()

        # fill args first
        args = []
        for a,d in arg_defaults:
            if a in node_mapping:
                args.append(node_mapping[a])
                used_nodes.add(a)
            elif d is not UNSPECIFIED:
                args.append(d)
            else:
                raise Exception(f"Tag {cls.yaml_tag} is missing '{a}' argument")
        
        # then kwargs
        kwargs = {}
        for a,d in kwarg_defaults:
            if a in node_mapping:
                kwargs[a] = node_mapping[a]
                used_nodes.add(a)
            elif d is not UNSPECIFIED:
                args[a] = d
        
        # if it accepts additional kwargs, fill with leftover kwargs
        if arg_spec.varkw and len(used_nodes) != len(node_mapping):
            for k,v in node_mapping:
                if k not in used_nodes:
                    kwargs[k] = v

        return cls(*args,**kwargs)

It's a bit lengthy, but gives a nice error if required positional arguments without a default value are missing.

0

精彩评论

暂无评论...
验证码 换一张
取 消