【问题标题】:How do I avoid global state when using custom constructors in ruamel.yaml?在 ruamel.yaml 中使用自定义构造函数时如何避免全局状态?
【发布时间】:2021-07-06 12:29:44
【问题描述】:

我正在使用 ruamel.yaml 解析复杂的 YAML 文档,其中某些标记节点需要特殊处理。按照已发布示例的建议,我使用add_multi_constructor 注入我的自定义解析逻辑。问题是我需要根据外部状态动态更改注入的逻辑,但是像add_multi_constructor 这样的装饰方法会修改全局状态,这会在逻辑上不相关的实例之间引入不可接受的耦合。这是 MWE:

import ruamel.yaml

def get_loader(parameter):
    def construct_node(constructor: ruamel.yaml.Constructor, tag: str, node: ruamel.yaml.Node):
        return parameter(tag.lstrip("!"), str(node.value))

    loader = ruamel.yaml.YAML()
    loader.constructor.add_multi_constructor("", construct_node)
    return loader

foo = get_loader(lambda tag, node: f"foo: {tag}, {node}")
bar = get_loader(lambda tag, node: f"bar: {tag}, {node}")
print(foo.load("!abc 123"), bar.load("!xyz 456"), sep="\n")

输出:

bar: abc, 123
bar: xyz, 456

预期:

foo: abc, 123
bar: xyz, 456

我做了以下解决方法,动态创建新的类实例以打破耦合:

def get_loader(parameter):
    def construct_node(constructor: ruamel.yaml.Constructor, tag: str, node: ruamel.yaml.Node):
        return parameter(tag.lstrip("!"), str(node.value))

    # Create a new class to prevent state sharing through class attributes.
    class ConstructorWrapper(ruamel.yaml.constructor.RoundTripConstructor):
        pass

    loader = ruamel.yaml.YAML()
    loader.Constructor = ConstructorWrapper
    loader.constructor.add_multi_constructor("", construct_node)
    return loader

我的问题是:

  • 我是否在滥用该库? 全局影响是一个巨大的危险信号,表明我使用 API 不正确,但该库缺少任何 API 文档,所以我不确定会是什么正确的方法。

  • 在 API 损坏的意义上它是否安全?由于没有记录在案的 API,我不确定这是否可以安全投入生产。

【问题讨论】:

    标签: python yaml ruamel.yaml shared-state


    【解决方案1】:

    IMO 您并没有滥用该库,只是解决了它当前的缺点/不完整。

    ruamel.yaml 获得带有YAML() 实例的API 之前,它具有功能 基于 PyYAML 的 API 和一些扩展,其他 PyYAML 的问题必须在一个 类似的不自然方式。例如。我恢复到拥有可以调用其实例的类(使用 __call__()) 然后可以将哪些方法更改为只能访问 从文档解析的 YAML 文档版本(因为 ruamel.yaml 支持 YAML 1.2 和 1.1 和 PyYAML 仅(部分)支持 1.1)。

    但在 ruamel.yaml 的 YAML() 实例下,并不是所有的都得到了改善。代码 继承自 PyYAML 存储各种构造函数的信息 在 class 属性中作为查找表(在 yaml_constructor resp yaml_multi_constructor),并且 ruamel.yaml 仍然这样做(作为完整的旧 PyYAML-escque API 实际上仍然存在,只有 0.17 版才有未来 弃用警告)。

    到目前为止,您的方法很有趣,因为您这样做了:

    loader.constructor.add_multi_constructor("", construct_node)
    

    代替:

    loader.Constructor.add_multi_constructor("", construct_node)
    

    (你可能知道loader.constructor是一个实例化的属性 loader.Constructor 如有必要,但此答案的其他读者可能不会)

    甚至:

    def get_loader(parameter):
        def construct_node(constructor: ruamel.yaml.Constructor, tag: str, node: ruamel.yaml.Node):
            return parameter(tag.lstrip("!"), str(node.value))
    
        # Create a new class to prevent state sharing through class attributes.
        class ConstructorWrapper(ruamel.yaml.constructor.RoundTripConstructor):
            pass
    
        ConstructorWrapper.add_multi_constructor("", construct_node)
    
        loader = ruamel.yaml.YAML()
        loader.Constructor = ConstructorWrapper
        return loader
    

    您的代码有效,是因为构造函数存储在类属性中,因为.add_multi_constructor() 是一个类方法。

    因此,就 API 损坏而言,您所做的事情并不完全安全。 ruamel.yaml 不是版本 1.0,并且可能会破坏您的代码的(API)更改可能伴随着任何 次要版本号更改。您应该相应地设置您的版本依赖项 您的生产代码(例如 ruamel.yaml<0.18 ),并仅在使用带有新次要版本号的 ruamel.yaml 版本进行测试后更新该次要版本号。


    可以通过更新透明地改变类属性的使用 类方法 add_constructor()add_multi_constructor() 为“正常” 方法并在__init__() 中完成查找表的初始化。 您调用实例的两个示例:

    loader.constructor.add_multi_constructor("", construct_node)
    

    会得到想要的结果,但 ruamel.yaml 的行为不会改变 在类上调用add_multi_constructor 时使用:

    loader.Constructor.add_multi_constructor("", construct_node)
    

    但是更改类方法 add_constructor()add_multi_constructor() 以这种方式影响 所有代码,恰好提供了一个实例 而不是类(并且所述代码对结果很好)。

    更有可能是两个新的 实例方法将被添加到 Constructor 类和 YAML() 实例中,并且类方法将 被逐步淘汰或更改为检查一个类而不是一个实例 在带有警告的弃用期之后传入(从 PyYAML 继承的全局函数 add_constructor()add_multi_constructor() 也是如此)。

    主要建议,除了将您的生产代码固定在次要上 版本号,是为了确保您的测试代码显示 PendingDeprecationWarning。如果您使用的是pytest,则为the case by default。 这应该给你足够的时间来调整你的代码以适应警告 推荐。

    如果 ruamel.yaml 的作者不再懒惰,他可能会提供 此类 API 添加/更改的一些文档。

    import ruamel.yaml
    import types
    import inspect
    
    
    class MyConstructor(ruamel.yaml.constructor.RoundTripConstructor):
        _cls_yaml_constructors = {}
        _cls_yaml_multi_constructors = {}
    
        def __init__(self, *args, **kw):
            self._yaml_constructors = {
                'tag:yaml.org,2002:null': self.__class__.construct_yaml_null,
                'tag:yaml.org,2002:bool': self.__class__.construct_yaml_bool,
                'tag:yaml.org,2002:int': self.__class__.construct_yaml_int,
                'tag:yaml.org,2002:float': self.__class__.construct_yaml_float,
                'tag:yaml.org,2002:binary': self.__class__.construct_yaml_binary,
                'tag:yaml.org,2002:timestamp': self.__class__.construct_yaml_timestamp,
                'tag:yaml.org,2002:omap': self.__class__.construct_yaml_omap,
                'tag:yaml.org,2002:pairs': self.__class__.construct_yaml_pairs,
                'tag:yaml.org,2002:set': self.__class__.construct_yaml_set,
                'tag:yaml.org,2002:str': self.__class__.construct_yaml_str,
                'tag:yaml.org,2002:seq': self.__class__.construct_yaml_seq,
                'tag:yaml.org,2002:map': self.__class__.construct_yaml_map,
                None: self.__class__.construct_undefined
            }
            self._yaml_constructors.update(self._cls_yaml_constructors)
            self._yaml_multi_constructors = self._cls_yaml_multi_constructors.copy()
            super().__init__(*args, **kw)
    
        def construct_non_recursive_object(self, node, tag=None):
            # type: (Any, Optional[str]) -> Any
            constructor = None  # type: Any
            tag_suffix = None
            if tag is None:
                tag = node.tag
            if tag in self._yaml_constructors:
                constructor = self._yaml_constructors[tag]
            else:
                for tag_prefix in self._yaml_multi_constructors:
                    if tag.startswith(tag_prefix):
                        tag_suffix = tag[len(tag_prefix) :]
                        constructor = self._yaml_multi_constructors[tag_prefix]
                        break
                else:
                    if None in self._yaml_multi_constructors:
                        tag_suffix = tag
                        constructor = self._yaml_multi_constructors[None]
                    elif None in self._yaml_constructors:
                        constructor = self._yaml_constructors[None]
                    elif isinstance(node, ScalarNode):
                        constructor = self.__class__.construct_scalar
                    elif isinstance(node, SequenceNode):
                        constructor = self.__class__.construct_sequence
                    elif isinstance(node, MappingNode):
                        constructor = self.__class__.construct_mapping
            if tag_suffix is None:
                data = constructor(self, node)
            else:
                data = constructor(self, tag_suffix, node)
            if isinstance(data, types.GeneratorType):
                generator = data
                data = next(generator)
                if self.deep_construct:
                    for _dummy in generator:
                        pass
                else:
                    self.state_generators.append(generator)
            return data
    
        def get_args(*args, **kw):
            if kw:
                raise NotImplementedError('can currently only handle positional arguments')
            if len(args) == 2:
                return MyConstructor, args[0], args[1]
            else:
                return args[0], args[1], args[2]
    
        def add_constructor(self, tag, constructor):
            self, tag, constructor = MyConstructor.get_args(*args, **kw)
            if inspect.isclass(self):
                self._cls_yaml_constructors[tag] = constructor
                return
            self._yaml_constructors[tag] = constructor
    
        def add_multi_constructor(*args, **kw): # self, tag_prefix, multi_constructor):
            self, tag_prefix, multi_constructor = MyConstructor.get_args(*args, **kw)
            if inspect.isclass(self):
                self._cls_yaml_multi_constructors[tag_prefix] = multi_constructor
                return
            self._yaml_multi_constructors[tag_prefix] = multi_constructor
    
    def get_loader_org(parameter):
        def construct_node(constructor: ruamel.yaml.Constructor, tag: str, node: ruamel.yaml.Node):
            return parameter(tag.lstrip("!"), str(node.value))
    
        loader = ruamel.yaml.YAML()
        loader.Constructor = MyConstructor
        loader.constructor.add_multi_constructor("", construct_node)
        return loader
    
    foo = get_loader_org(lambda tag, node: f"foo: {tag}, {node}")
    bar = get_loader_org(lambda tag, node: f"bar: {tag}, {node}")
    print('>org<', foo.load("!abc 123"), bar.load("!xyz 456"), sep="\n")
    
    
    def get_loader_instance(parameter):
        def construct_node(constructor: ruamel.yaml.Constructor, tag: str, node: ruamel.yaml.Node):
            return parameter(tag.lstrip("!"), str(node.value))
    
        # Create a new class to prevent state sharing through class attributes.
        class ConstructorWrapper(MyConstructor):
            pass
    
        loader = ruamel.yaml.YAML()
        loader.Constructor = ConstructorWrapper
        loader.constructor.add_multi_constructor("", construct_node)
        return loader
    
    foo = get_loader_instance(lambda tag, node: f"foo: {tag}, {node}")
    bar = get_loader_instance(lambda tag, node: f"bar: {tag}, {node}")
    print('>instance<', foo.load("!abc 123"), bar.load("!xyz 456"), sep="\n")
    
    
    def get_loader_cls(parameter):
        def construct_node(constructor: ruamel.yaml.Constructor, tag: str, node: ruamel.yaml.Node):
            return parameter(tag.lstrip("!"), str(node.value))
    
        # Create a new class to prevent state sharing through class attributes.
        class ConstructorWrapper(MyConstructor):
            pass
    
        loader = ruamel.yaml.YAML()
        loader.Constructor = ConstructorWrapper
        loader.Constructor.add_multi_constructor("", construct_node)
        #      ^ using the virtual class method
        return loader
    
    foo = get_loader_cls(lambda tag, node: f"foo: {tag}, {node}")
    bar = get_loader_cls(lambda tag, node: f"bar: {tag}, {node}")
    print('>cls<', foo.load("!abc 123"), bar.load("!xyz 456"), sep="\n")
    

    给出:

    >org<
    foo: abc, 123
    bar: xyz, 456
    >instance<
    foo: abc, 123
    bar: xyz, 456
    >cls<
    bar: abc, 123
    bar: xyz, 456
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-07
      • 1970-01-01
      • 2016-01-23
      • 1970-01-01
      • 2015-05-13
      • 2019-06-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多