在 ruamel.yaml 中使用自定义构造函数时如何避免全局状态?

如何解决在 ruamel.yaml 中使用自定义构造函数时如何避免全局状态?

我正在使用 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,456

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

def get_loader(parameter):
    def construct_node(constructor: ruamel.yaml.Constructor,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 文档,我不确定将其投入生产是否安全。

解决方法

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_constructoryaml_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 版本进行测试后更新该次要版本号。


可以通过更新透明地改变类属性的使用 classmethods 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,2002:int': self.__class__.construct_yaml_int,2002:float': self.__class__.construct_yaml_float,2002:binary': self.__class__.construct_yaml_binary,2002:timestamp': self.__class__.construct_yaml_timestamp,2002:omap': self.__class__.construct_yaml_omap,2002:pairs': self.__class__.construct_yaml_pairs,2002:set': self.__class__.construct_yaml_set,2002:str': self.__class__.construct_yaml_str,2002:seq': self.__class__.construct_yaml_seq,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,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,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,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,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}")
bar = get_loader_instance(lambda tag,{node}")
print('>instance<',sep="\n")


def get_loader_cls(parameter):
    def construct_node(constructor: ruamel.yaml.Constructor,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}")
bar = get_loader_cls(lambda tag,{node}")
print('>cls<',sep="\n")

给出:

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?