ruamel.yaml:将注释固定到下一个数据项而不是前一个

ruamel.yaml: pin comment to next data item instead of previous one

h我在将 ruamel.yaml 与往返加载程序一起使用时观察到一些令人困惑的行为。 这可能是因为 ruamel.yaml 自动确定评论应连接到哪个数据项并非易事。

在下面的例子中,我想一直保留评论。如果我告诉 ruamel.yaml 它应该考虑所有与下一个数据项相关的评论(即评论在“其他”之前),这应该是可能的。

这可以做到吗?

如果是:如何?

data_was_a_dict = """\
---
data: was_a_dict
main_dict:
  data: 
    some: data

# this comment gets always lost
other: data
"""

data_was_a_str = """\
---
data: was_a_str
main_dict:
  data: a_string  

# this gets shifted or stays
other: data
"""

import ruamel.yaml, sys


yaml = ruamel.yaml.YAML()
for text in [data_was_a_dict, data_was_a_str]:
    for new_data in ["new_text", {"something": "else"}]:
        data = yaml.load(text)

        data["main_dict"]["data"] = new_data
        yaml.dump(data, sys.stdout)
        print("==========================")

输出:

data: was_a_dict
main_dict:
  data: new_text
other: data
==========================
data: was_a_dict
main_dict:
  data:
    something: else
other: data
==========================
data: was_a_str
main_dict:
  data: new_text

# this gets shifted or stays
other: data
==========================
data: was_a_str
main_dict:
  data:

# this gets shifted or stays
    something: else
other: data
==========================

========================================

感谢 Anthon 的更新:

def replace_dict(target, key, new_dict):
    def get_last_key(dct):
        keys = [key for key in dct.keys()]
        return keys[-1]
        
    old_dict = target[key]
    if old_dict and new_dict:
        # if new_dict is empty, we will lose the comment. 
        # That's fine for now since this should not happen in my case and I don't know yet where to attach 
        # the comment in that case
        last_comment = old_dict.ca.items.get(get_last_key(old_dict), None)
        if last_comment:
            actual_comment = last_comment[2]
            actual_comment.value = clean_comment(actual_comment.value)
            if actual_comment.value:
                if not isinstance(new_dict, ruamel.yaml.comments.CommentedMap):
                    new_dict = ruamel.yaml.comments.CommentedMap(new_dict)                
                new_dict.ca.items[get_last_key(new_dict)] = last_comment
    target[key] = new_dict
    
def clean_comment(txt: str) -> str:
    _,_,after = txt.partition("\n")
    if after:
        return "\n" + after
    return ""

data_was_a_dict = """\
---
main_dict:
  place: holder
  sub_dict: # this stays
    item1: value1
    item2: value2 # this is removed
    
# this stays    
other: data
"""

import ruamel.yaml, sys
import json

yaml = ruamel.yaml.YAML()

data = yaml.load(data_was_a_dict)
replace_dict(data["main_dict"], "sub_dict", {"item_new": "value_new"})

yaml.dump(data, sys.stdout)

给予

main_dict:
  place: holder
  sub_dict: # this stays
    item_new: value_new
# this stays    
other: data

我不确定以下内容令人困惑,documented 保留评论的行为:

This preservation is normally not broken unless you severely alter the structure of a component (delete a key in a dict, remove list entries). Reassigning values or replacing list items, etc., is fine.

在您转储的四个组合中的三个中,您首先替换了一个 简单值乘以复合值,或者删除包含的复合值 评论信息一并。

在当前的所有版本中(即 <0.18),ruamel.yaml 附上扫描件 评论解析评论时存在的标记。没有 您的下一个数据项的令牌(尚未),因此目前无法附加此 到“下一个数据项”。 ruamel.yaml<0.18中的实际评论信息为 一个扩展的行尾注释,其值类似于 "\n\n# this gets shifted or stays",因为它以换行符开头,这意味着没有实际 在与之关联的键的行末注释..

在你的data_was_a_dict评论中与键some相关联,你是否替换 CommentedMap(一个 dict 子类型,对 它的 .ca 属性)与字符串或字典没有区别,因为带有注释的数据结构已被完全替换。

在您的 data_was_a_str YAML 文档中,它与密钥 data 相关联 在“CommentedMap 级别高于其他文档”。如果你更换 它的值与另一个字符串的输出将类似于输入。如果你 添加一个全新的子结构,注释被解释为介于 键及其(复合)值。

要获得您所期望的结果,您必须检查是否有与 键 data 并将其移动到与键 something 相关联,这可能 不是普通 dict 上的键(它必须是 CommentedMap)。 在您 delete/overwrite 附加评论的数据结构的组合中,您必须检查评论并在删除之前移动它。在将简单值替换为复合值的组合中,您可以在分配后移动注释(给定一个合适的复合值,如 CommentedMap)。所以是的,你想要的是可能的,但不是微不足道的,这些将依赖于将在即将发布的版本中更改的未记录的功能。

import sys
import ruamel.yaml

data_was_a_dict = """\
data: was_a_dict
main_dict:
  data: 
    some: data

# this comment gets always lost
other: data
"""

data_was_a_str = """\
data: was_a_str
main_dict:
  data: a_string  

# this gets shifted or stays
other: data
"""

yaml = ruamel.yaml.YAML()

data = yaml.load(data_was_a_dict)
print(data['main_dict']['data'].ca)
data = yaml.load(data_was_a_str)
print(data['main_dict'].ca)

给出:

Comment(comment=None,
  items={'some': [None, None, CommentToken('\n\n# this comment gets always lost\n', line: 5, col: 0), None]})
Comment(comment=None,
  items={'data': [None, None, CommentToken('\n\n# this gets shifted or stays\n', line: 4, col: 0), None]})

我正在考虑更换 ruamel.yaml 的评论扫描和附件,这将允许 用户可选择拆分(在第一个空行),允许用户 指定将评论信息附加到“数据”之后的前面 and/or。 分配可能会受到缩进级别的影响。文档 甚至可能会更新以反映往返将支持更少 重组数据时对保留注释的严格限制。