yaml / pyyaml

Canonical source repository for PyYAML
MIT License
2.54k stars 515 forks source link

Odd behaviour of `add_constructor` when applied within a loop #704

Closed ckaipf closed 1 year ago

ckaipf commented 1 year ago

Hi!

I've stumbled across some odd behavior of the add_constructor method when applied in a loop. I'm a little curious if you knew anything about this or if I made a mistake.

Unlike the other implementations, the constructor when added within a loop is overwritten with the one from the last iteration.


import abc

import yaml

class A(abc.ABC):
    pass

class B(A):
    pass

class C(A):
    pass

def a_representer(dumper, a):
    return dumper.represent_mapping(
        "!" + a.__class__.__name__,
        {},
    )

def get_dumper():
    safe_dumper = yaml.SafeDumper
    for subclass in A.__subclasses__():
        safe_dumper.add_representer(subclass, a_representer)
    return safe_dumper

def get_loader_plain():
    loader = yaml.SafeLoader

    subclasses = A.__subclasses__()
    loader.add_constructor(
        "!" + subclasses[0].__name__,
        lambda loader, node: subclasses[0](**loader.construct_mapping(node)),
    )
    loader.add_constructor(
        "!" + subclasses[1].__name__,
        lambda loader, node: subclasses[1](**loader.construct_mapping(node)),
    )
    return loader

def get_loader_for_loop():
    loader = yaml.SafeLoader

    subclasses = A.__subclasses__()
    for subclass in subclasses:
        loader.add_constructor(
            "!" + subclass.__name__,
            lambda loader, node: subclass(**loader.construct_mapping(node)),
        )
    return loader

def get_loader_recursive():
    loader = yaml.SafeLoader

    def f(ls):
        head, *tail = ls
        loader.add_constructor(
            "!" + head.__name__,
            lambda loader, node: head(**loader.construct_mapping(node)),
        )
        if tail:
            return f(tail)
        else:
            pass

    subclasses = A.__subclasses__()
    f(subclasses)

    return loader

dump = yaml.dump([B(), C()], Dumper=get_dumper())
print(yaml.load(dump, Loader=get_loader_plain()))
print(yaml.load(dump, Loader=get_loader_for_loop()))
print(yaml.load(dump, Loader=get_loader_recursive()))
plain
[<__main__.B object at 0x7f1e1ee26860>, <__main__.C object at 0x7f1e1ee26890>]
for loop
[<__main__.C object at 0x7f1e1ee26650>, <__main__.C object at 0x7f1e1ee262f0>]
recursive
[<__main__.B object at 0x7f1e1ee268f0>, <__main__.C object at 0x7f1e1ee26920>]
Debian 11.6 (WSL)
Python 3.10.9
PyYAML 6.0

Thank you for any insights!

ckaipf commented 1 year ago

This was due to the behaviour described here, thanks to @mephenor