ruamel.yaml 递归函数:意外更新具有相同值的非字符串数据的每个实例

ruamel.yaml recurse function: Unexpectedly Updates Every Instance of Non-String Data with the Same Value

我应 ruamel.yaml 作者的要求在 提出这个问题。

在回答中给出了以下代码来解决如何更新ruamel.yaml数据中的别名值的问题:

def update_aliased_scalar(data, obj, val):
    def recurse(d, ref, nv):
        if isinstance(d, dict):
            for i, k in [(idx, key) for idx, key in enumerate(d.keys()) if key is ref]:
                d.insert(i, nv, d.pop(k))
            for k, v in d.non_merged_items():
                if v is ref:
                    d[k] = nv
                else:
                    recurse(v, ref, nv)
        elif isinstance(d, list):
            for idx, item in enumerate(d):
                if item is ref:
                    d[idx] = nv
                else:
                    recurse(item, ref, nv)

    if hasattr(obj, 'anchor'):
        recurse(data, obj, type(obj)(val, anchor=obj.anchor.value))
    else:
        recurse(data, obj, type(obj)(val))

这段出色的贡献代码运行得非常好,以至于我将它包装在一个函数中并在我的项目中使用它来处理对数据执行 所有 更改,如此处所示(轻度重命名以适应粘贴到的代码的样式):https://github.com/wwkimball/yamlpath/blob/319585620abfab199f3e15c87e0a2dc2c900aa1d/yamlpath/processor.py#L739-L781

这对我项目的大多数用例都非常有效。也就是说,从那以后我一直在生产中使用这个代码并取得了巨大的成功。我使用的值几乎完全是字符串数据,并且偶然地,任何非字符串数据恰好被别名,因为它通常是重复使用的服务端口号。

基于这些成功而不是批判性地阅读代码(我完全相信作者比我更了解 ruamel.yaml),我错误地认为此代码仅更新 目标节点和任何对 it 的引用。因此,我也认为使用此代码更新非别名数据也是安全的。我错了。这是我的错。

事实证明,每当传递非字符串值以更新此函数时,它不仅会替换该目标节点,还会替换 每个 节点相同的值,即使它们不是彼此的引用。因此,当数据如下所示时:

---
key: 42
other_key: 42

调用将 key: 42 更改为 key: 5280 的函数不仅进行了预期的更改,而且还将 other_key: 更改为 5280不会发生当被更改的值是非别名字符串数据时,无论有多少其他节点具有相同的值(这是什么让我相信使用此函数更新 any 值是安全的,无论是否使用别名)。当值为布尔值时,也会发生这种情况。

我不明白代码实际上在做什么。我以非设计的方式使用代码。

我需要的是接受节点更改的功能,然后仅更改那个节点,当它是一个非别名值时and 当它 别名值时,也会更改所有其他别名节点 而不会 影响其他不相关别名的节点(*alias1 与 *alias2) 具有相同的值,或者是恰好与正在更新的别名相同的非别名值。 当我调用该函数时,我只有全部数据,目标节点,以及它的预期新值。

如果函数在调用时需要更多信息,我愿意重构我自己的代码。

您的问题是由于递归函数中的限制(cq 错误)引起的,如 另一个答案。

当您加载示例 YAML 时,值 42 的两个 "nodes" 具有相同的 ID。这是一个 Python 优化,它适用于布尔值、整数的子集(最多 100 IIRC)等。 由于 recurse 测试身份(使用 is),if v is ref 匹配两次。

这本质上是因为传入了对象obj,并且 您需要的是父对象和该对象上的 key/index:

import sys
import ruamel.yaml

yaml_str = """\
- key: 42
  other_key: 42
  k: &xx 196
  l: *xx
"""

def update_aliased_scalar(data, parent, key_index, val):
    def recurse(d, parent, key_index, ref, nv):
        if isinstance(d, dict):
            for i, k in [(idx, key) for idx, key in enumerate(d.keys()) if key is ref]:
                d.insert(i, nv, d.pop(k))
            for k, v in d.non_merged_items():
                if v is ref:
                    if hasattr(v, 'anchor') or (d is parent and k == key_index):
                        d[k] = nv
                else:
                    recurse(v, parent, key_index, ref, nv)
        elif isinstance(d, list):
            for idx, item in enumerate(d):
                if item is ref:
                    d[idx] = nv
                else:
                    recurse(item, parent, key_index, ref, nv)

    obj = parent[key_index]
    if hasattr(obj, 'anchor'):
        recurse(data, parent, key_index, obj, type(obj)(val, anchor=obj.anchor.value))
    else:
        recurse(data, parent, key_index, obj, type(obj)(val))

yaml = ruamel.yaml.YAML()
data = yaml.load(yaml_str)

update_aliased_scalar(data, data[0], 'key', 43)
update_aliased_scalar(data, data[0], 'k', 197)
yaml.dump(data, sys.stdout)

给出:

- key: 43
  other_key: 42
  k: &xx 197
  l: *xx