Как изменить привязанный скаляр в последовательности, не разрушая привязку в ruamel.yaml?

#python-3.x #ruamel.yaml

#python-3.x #ruamel.yaml

Вопрос:

При использовании ruamel.yaml версии 0.15.92 с Python 3.6.6 на CentOS 7, похоже, я не могу обновить значение привязанного скаляра в последовательности, не уничтожив саму привязку или не создав недопустимый YAML из следующего дампа.

Я попытался воссоздать исходный тип узла с новым значением (старый PlainScalarString -> новый PlainScalarString , старый FoldedScalarString -> новый FoldedScalarString и т. Д.), скопировав anchor его. Хотя это восстанавливает привязку к обновленному скалярному значению, это также создает недопустимый YAML, потому что первый псевдоним позже в файле YAML дублирует то же имя привязки и присваивает ему старое значение скаляра, которое я пытаюсь обновить.

Затем я попытался заменить все затронутые псевдонимы фактическим текстом псевдонима — like *anchor_name — но это приводит к тому, что значение становится похожим на кавычки '*anchor_name' , делая псевдоним бесполезным.

Я отменил это, а затем попытался подавить повторяющееся имя привязки (установив always_dump=False для каждого затронутого псевдонима). Хотя это подавляет повторяющееся имя привязки, оно, к сожалению, просто сбрасывает старое значение привязанного скаляра.

Все мои тестовые данные следующие; предположим, что это называется test.yaml:

 # Header comment
---
# Post-header comment

# Reusable aliases
aliases:
  - amp;plain_value This is unencrypted
  - amp;string_password ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAYnFbMveZGBgd9aw7h4VV M202zRdcP96UQs1q ViznJK2Ee08hoW9jdIqVhNaecYALUihKjVYijJa649VF7BLZXV0svLEHD8LZeduoLS3iC9uszdhDFB2Q6R/Vv/ARjHNoWc6/D0nFN9vwcrQNITnvREl0WXYpR9SmW0krUpyr90gSAxTxPNJVlEOtA0afeJiXOtQEu/b8n UDM3eXXRO 2SEXM4ub7fNcj6V9DgT3WwKBUjqzQ5DicnB19FNQ1cBGcmCo8qRv0JtbVqZ4 WJFGc06hOTcAJPsAaWWUn80ChcTnl4ELNzpJFoxAxHgepirskuIvuWZv3h/PL8Ez3NDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBSuVIsvWXMmdFJtJmtJxXxgCAGFCioe/zdphGqynmj6vVDnCjA3Xc0VPOCmmCl/cTKdg==]
  - amp;block_password >
    ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw
    DQYJKoZIhvcNAQEBBQAEggEAojErrxuNcdX6oR VA/I3PyuV2CwXx166nIUp
    asEHo1/CiCIoE3qCnjK2FJF8vg l3AqRmdb7vYrqQ 30RFfHSlB9zApSw8NW
    tnEpawX4hhKAxnTc/JKStLLu2k7iZkhkor/UA2HeVJcCzEeYAwuOQRPaolmQ
    TGHjvm2w6lhFDKFkmETD/tq4gQNcOgLmJ Pqhogr/5FmGOpJ7VGjpeUwLteM
    er3oQozp4l2bUTJ8wk9xY6cN eeOIcWXCPPdNetoKcVropiwrYH8QV4CZ2Ky
    u0vpiybEuBCKhr1EpfqhrtuG5s817eOb7 Wf5ctR0rPuxlTUqdnDY31zZ3Kb
    mcjqHDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBATq6BjaxU2bfcLL5S
    bxzsgCDsWzggzxsCw4Dp0uYLwvMKjJEpMLeFXGrLHJzTF6U2Nw==]

top_key: unencrypted value
top_alias: *plain_value

top::hash:
  ignore: more
  # This pulls its string-form value from above
  stringified_alias: *string_password
  sub:
    ignore: value
    key: unencrypted subbed-value
    # This pulls its block-form value from above
    blocked_alias: *block_password
  sub_more:
    # This is a stringified EYAML value, NOT an alias
    inline_string: ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAafmyrrae2kx8HdyPmn/RHQRcTPhqpx5Idm12hCDCIbwVM  H c620z4EN2wlugz/GcLaiGsybaVWzAZ 3r 1 EwXn5ec4dJ5TTqo7oxThwUMa SHliipDJwGoGii/H y2I 3 irhDYmACL2nyJ4dv4IUXwqkv6nh1J9MwcOkGES2SKiDm/WwfkbPIZc3ccp1FI9AX/m3SVqEcvsrAfw6HtkolM22csfuJREHkTp7nBapDvOkWn4plzfOw9VhPKhq1x9DUCVFqqG/HAKv  v4osClK6k1MmSJWaMHrW1z3n7LftV9ZZ60E0Cgro2xSaD itRwBp07H0GeWuoKB4 44TBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCRv9r2lvQ1GJMoD064EtdigCCw43EAKZWOc41yEjknjRaWDm1VUug6I90lxCsUrxoaMA==]
    # Also NOT an alias, in block form
    block_string: >
      ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw
      DQYJKoZIhvcNAQEBBQAEggEAafmyrrae2kx8HdyPmn/RHQRcTPhqpx5Idm12
      hCDCIbwVM  H c620z4EN2wlugz/GcLaiGsybaVWzAZ 3r 1 EwXn5ec4dJ5
      TTqo7oxThwUMa SHliipDJwGoGii/H y2I 3 irhDYmACL2nyJ4dv4IUXwqk
      v6nh1J9MwcOkGES2SKiDm/WwfkbPIZc3ccp1FI9AX/m3SVqEcvsrAfw6Htko
      lM22csfuJREHkTp7nBapDvOkWn4plzfOw9VhPKhq1x9DUCVFqqG/HAKv  v4
      osClK6k1MmSJWaMHrW1z3n7LftV9ZZ60E0Cgro2xSaD itRwBp07H0GeWuoK
      B4 44TBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCRv9r2lvQ1GJMoD064
      EtdigCCw43EAKZWOc41yEjknjRaWDm1VUug6I90lxCsUrxoaMA==]

# Signature line
 

Существует две формы этой проблемы, поэтому вот два примера кода для воспроизведения условий:

Во-первых, «Как мы можем наиболее просто обновить значение привязанного скаляра в последовательности, не разрушая привязку или ее псевдонимы?» Это выглядит так:

 with open('test.yaml', 'r') as f:
  yaml_data = yaml.load(f)

yaml_data['aliases'][1] = "New string password"
yaml.dump(yaml_data, sys.stdout)
 

Обратите внимание, что это разрушает привязку. Я бы очень предпочел, чтобы решение выглядело как можно более похожим на этот первый фрагмент; возможно, что-то вроде yaml_data['aliases'][1].set_value("New string password") # Changes only the scalar value while preserving the original anchor, comments, position, et al. .

Во-вторых, «Если мы должны вместо этого обернуть новое значение в какой-либо объект, чтобы сохранить привязку (и другие атрибуты заменяемой записи), каков самый простой подход, который также сохраняет все псевдонимы, которые ссылаются на него (так что они принимают обновленное значение) при сбросе?» Моя попытка решить эту проблему требует гораздо большего количества кода, включая рекурсивные функции. Поскольку рекомендации SO не рекомендуют сбрасывать большой код, я предложу соответствующие биты. Пожалуйста, предположите, что незарегистрированный код работает отлично.

 ### <snip def FindEYAMLPaths(...) returns lists of paths through the YAML to every value starting with 'ENC['>
### <snip def GetYAMLValue(...) returns the node -- as a PlainScalarString, FoldedScalarString, et al. -- identified by a path from FindEYAMLPaths>
### <snip def DisableAnchorDump(...) sets `anchor.always_dump=False` if the node has an anchor attribute>

def ReplaceYAMLValue(value, data, path=None):
  if path is None:
    return

  ref = data
  last_ref = path.pop()
  for p in path:
    ref = ref[p]

  # All I'm trying to do here is change the scalar value without disrupting its comments, anchor, positioning, or any of its aliases.
  # This succeeds in changing the scalar value and preserving its original anchor, but disrupts its aliases which insist on preserving the old value.
  if isinstance(ref[last_ref], PlainScalarString):
    ref[last_ref] = PlainScalarString(value, anchor=ref[last_ref].anchor.value)
  elif isinstance(ref[last_ref], FoldedScalarString):
    ref[last_ref] = FoldedScalarString(value, anchor=ref[last_ref].anchor.value)
  else:
    ref[last_ref] = value


with open('test.yaml', 'r') as f:
  yaml_data = yaml.load(f)

seen_anchors = []
for path in FindEYAMLPaths(yaml_data):
  if path is None:
    continue

  node = GetYAMLValue(yaml_data, deque(path))
  if hasattr(node, 'anchor'):
    test_anchor = node.anchor.value
    if test_anchor is not None:
      if test_anchor in seen_anchors:
        # This is expected to just be an alias, pointing at the newly updated anchor
        DisableAnchorDump(node)
        continue
      seen_anchors.append(test_anchor)

  ReplaceYAMLValue("New string password", yaml_data, path)

yaml.dump(yaml_data, sys.stdout)
 

Note that this produces valid YAML except that all of the affected aliases are gone, replaced instead by the old value of the anchored scalar.

I expect to be able to change the value of an aliased scalar in a sequence without disrupting any other part of the YAML content. Based on other posts I’ve seen about ruamel.yaml, I fully accept that I may need to dump the updated YAML to file and reload it for the in-memory aliases to update to the new value. I simply expect to change:

Input File

 aliases:
  - amp;some_anchor Old value

usage: *some_anchor
 

Для:

Выходной файл

 aliases:
  - amp;some_anchor NEW VALUE

usage: *some_anchor
 

Вместо этого, вот вывод из двух приведенных выше примеров:

Во-первых, обратите внимание, что исходная привязка была уничтожена, и значение for top::hash:stringified_alias: теперь содержит исходную привязку и старое значение вместо псевдонима для вновь обновленного скалярного значения в [‘aliases’][1]:

 ---
# Post-header comment

# Reusable aliases
aliases:
  - amp;plain_value This is unencrypted
  - New string password
  - amp;block_password >
    ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw
    DQYJKoZIhvcNAQEBBQAEggEAojErrxuNcdX6oR VA/I3PyuV2CwXx166nIUp
    asEHo1/CiCIoE3qCnjK2FJF8vg l3AqRmdb7vYrqQ 30RFfHSlB9zApSw8NW
    tnEpawX4hhKAxnTc/JKStLLu2k7iZkhkor/UA2HeVJcCzEeYAwuOQRPaolmQ
    TGHjvm2w6lhFDKFkmETD/tq4gQNcOgLmJ Pqhogr/5FmGOpJ7VGjpeUwLteM
    er3oQozp4l2bUTJ8wk9xY6cN eeOIcWXCPPdNetoKcVropiwrYH8QV4CZ2Ky
    u0vpiybEuBCKhr1EpfqhrtuG5s817eOb7 Wf5ctR0rPuxlTUqdnDY31zZ3Kb
    mcjqHDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBATq6BjaxU2bfcLL5S
    bxzsgCDsWzggzxsCw4Dp0uYLwvMKjJEpMLeFXGrLHJzTF6U2Nw==]

# ... snip ...

top::hash:
  ignore: more
  # This pulls its string-form value from above
  stringified_alias: amp;string_password ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAYnFbMveZGBgd9aw7h4VV M202zRdcP96UQs1q ViznJK2Ee08hoW9jdIqVhNaecYALUihKjVYijJa649VF7BLZXV0svLEHD8LZeduoLS3iC9uszdhDFB2Q6R/Vv/ARjHNoWc6/D0nFN9vwcrQNITnvREl0WXYpR9SmW0krUpyr90gSAxTxPNJVlEOtA0afeJiXOtQEu/b8n UDM3eXXRO 2SEXM4ub7fNcj6V9DgT3WwKBUjqzQ5DicnB19FNQ1cBGcmCo8qRv0JtbVqZ4 WJFGc06hOTcAJPsAaWWUn80ChcTnl4ELNzpJFoxAxHgepirskuIvuWZv3h/PL8Ez3NDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBSuVIsvWXMmdFJtJmtJxXxgCAGFCioe/zdphGqynmj6vVDnCjA3Xc0VPOCmmCl/cTKdg==]

# ... snip ...
 

Во-вторых, обратите внимание, что [‘aliases’][1] теперь выглядит правильно — это новое значение с исходным привязкой — но там, где я ожидаю увидеть псевдонимы, я вместо этого вижу старое значение. Я ожидаю увидеть *string_password вместо ENC[...] .

 ---
# Post-header comment

# Reusable aliases
aliases:
  - amp;plain_value This is unencrypted
  - amp;string_password New string password
  - amp;block_password >-
    New string password

# ... snip ...

top::hash:
  ignore: more
  # This pulls its string-form value from above
  stringified_alias: ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAYnFbMveZGBgd9aw7h4VV M202zRdcP96UQs1q ViznJK2Ee08hoW9jdIqVhNaecYALUihKjVYijJa649VF7BLZXV0svLEHD8LZeduoLS3iC9uszdhDFB2Q6R/Vv/ARjHNoWc6/D0nFN9vwcrQNITnvREl0WXYpR9SmW0krUpyr90gSAxTxPNJVlEOtA0afeJiXOtQEu/b8n UDM3eXXRO 2SEXM4ub7fNcj6V9DgT3WwKBUjqzQ5DicnB19FNQ1cBGcmCo8qRv0JtbVqZ4 WJFGc06hOTcAJPsAaWWUn80ChcTnl4ELNzpJFoxAxHgepirskuIvuWZv3h/PL8Ez3NDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBSuVIsvWXMmdFJtJmtJxXxgCAGFCioe/zdphGqynmj6vVDnCjA3Xc0VPOCmmCl/cTKdg==]

# ... snip ...
 

Ответ №1:

Если вы читаете в привязанном скаляре, таком как ваш This is unencrypted , используя ruamel.yaml , вы получаете PlainScalarString объект (или один из других ScalarString подклассов), который представляет собой чрезвычайно тонкий слой вокруг базового строкового типа. Этот слой имеет атрибут для хранения привязки, если это применимо (другие виды использования — в первую очередь для поддержания информации о стиле цитирования / буквального / складывания). И любые псевдонимы, использующие этот якорь, ссылаются на один и тот же ScalarString экземпляр.

При сбросе атрибут привязки не используется для создания псевдонимов, то есть выполняется обычным способом, имея несколько ссылок на один и тот же объект. Атрибут используется только для записи идентификатора привязки, а также делает это, если есть атрибут, но нет дополнительных ссылок (т.Е. привязка без псевдонимов).

Поэтому неудивительно, что если вы замените такой объект несколькими ссылками (либо в месте привязки, либо в любом из мест псевдонимов), ссылка исчезнет. Если вы затем также принудительно примените то же имя привязки к какому-либо другому объекту, вы получите повторяющиеся привязки, в отличие от обычной генерации привязки / псевдонима, проверка «принудительных» привязок не выполняется.

Поскольку ScalarString это такая тонкая оболочка, они по сути являются неизменяемыми объектами, как и сама строка. В отличие от псевдонимов dicts и списков, которые являются объектами коллекции, которые можно очистить, а затем заполнить (вместо замены новым экземпляром), вы не можете этого сделать string .

Реализация ScalarString , конечно, может быть изменена, поэтому вы можете использовать свой set_values() метод, но предполагает создание альтернативных классов для всех объектов ( PlainScalarString , FoldedScalarString ) . Вам нужно будет убедиться, что они используются для построения и представления, а затем предпочтительнее также вести себя как обычные строки, насколько вам это нужно, так что, по крайней мере, вы можете печатать. Это относительно легко сделать, но требует копирования и незначительного изменения нескольких десятков строк кода

Я думаю, что проще оставить ScalarStrings на месте как есть (т.Е. Быть неизменяемым) и сделать то, что вам нужно сделать, если вы хотите изменить все вхождения (т.Е. Ссылки): обновить все ссылки на оригинал. Если ваша структура данных будет содержать миллионы узлов, это может занять непомерно много времени, но все же будет зависеть от того, что потребуется для загрузки и выгрузки самого YAML:

 import sys
from pathlib import Path
import ruamel.yaml

in_file = Path('test.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))

yaml = ruamel.yaml.YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.preserve_quotes = True
data = yaml.load(in_file)

update_aliased_scalar(data, data['aliases'][1], "New string password")
update_aliased_scalar(data, data['top::hash']['sub']['blocked_alias'], "New block passwordn")

yaml.dump(data, sys.stdout)
 

что дает:

 # Post-header comment

# Reusable aliases
aliases:
  - amp;plain_value This is unencrypted
  - amp;string_password New string password
  - amp;block_password >
    New block password

top_key: unencrypted value
top_alias: *plain_value

top::hash:
  ignore: more
  # This pulls its string-form value from above
  stringified_alias: *string_password
  sub:
    ignore: value
    key: unencrypted subbed-value
    # This pulls its block-form value from above
    blocked_alias: *block_password
  sub_more:
    # This is a stringified EYAML value, NOT an alias
    inline_string: ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAafmyrrae2kx8HdyPmn/RHQRcTPhqpx5Idm12hCDCIbwVM  H c620z4EN2wlugz/GcLaiGsybaVWzAZ 3r 1 EwXn5ec4dJ5TTqo7oxThwUMa SHliipDJwGoGii/H y2I 3 irhDYmACL2nyJ4dv4IUXwqkv6nh1J9MwcOkGES2SKiDm/WwfkbPIZc3ccp1FI9AX/m3SVqEcvsrAfw6HtkolM22csfuJREHkTp7nBapDvOkWn4plzfOw9VhPKhq1x9DUCVFqqG/HAKv  v4osClK6k1MmSJWaMHrW1z3n7LftV9ZZ60E0Cgro2xSaD itRwBp07H0GeWuoKB4 44TBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCRv9r2lvQ1GJMoD064EtdigCCw43EAKZWOc41yEjknjRaWDm1VUug6I90lxCsUrxoaMA==]
    # Also NOT an alias, in block form
    block_string: >
      ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw
      DQYJKoZIhvcNAQEBBQAEggEAafmyrrae2kx8HdyPmn/RHQRcTPhqpx5Idm12
      hCDCIbwVM  H c620z4EN2wlugz/GcLaiGsybaVWzAZ 3r 1 EwXn5ec4dJ5
      TTqo7oxThwUMa SHliipDJwGoGii/H y2I 3 irhDYmACL2nyJ4dv4IUXwqk
      v6nh1J9MwcOkGES2SKiDm/WwfkbPIZc3ccp1FI9AX/m3SVqEcvsrAfw6Htko
      lM22csfuJREHkTp7nBapDvOkWn4plzfOw9VhPKhq1x9DUCVFqqG/HAKv  v4
      osClK6k1MmSJWaMHrW1z3n7LftV9ZZ60E0Cgro2xSaD itRwBp07H0GeWuoK
      B4 44TBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCRv9r2lvQ1GJMoD064
      EtdigCCw43EAKZWOc41yEjknjRaWDm1VUug6I90lxCsUrxoaMA==]

# Signature line
 

Как вы можете видеть, привязки сохраняются, и не имеет значения update_aliased_scalar , предоставляете ли вы
привязанное «место» или одно из псевдонимных мест в качестве ссылки.

Выше recurse также обрабатываются ключи с псевдонимами, поскольку для ключа в сопоставлении YAML вполне нормально иметь привязку или быть псевдонимом. Вы даже можете иметь привязанный ключ со значением, которое является псевдонимом соответствующего ключа.

Комментарии:

1. Потрясающе; спасибо! Мне пришлось внести небольшое исправление в ваше предложение, потому что некоторые значения str , которые, естественно, не имеют anchor свойства. Простой if hasattr(...) тест либо добавляет, либо опускает , anchor=... параметр.

2. @seWilliam Я обновил ответ двумя строками, которые обрабатывают случай, когда у вас есть привязки / псевдонимы в ваших ключах, и сохраняет ваш ключи в правильном порядке. При этом вы также можете обновить запись сопоставления, которая выглядит как amp;a b: *a (вероятно, никогда не понадобится).

3. Я бы хотел, чтобы что-то подобное было встроено в будущую версию ruamel.yaml! В идеале это срабатывало бы всякий раз, когда пользователь пытается присвоить новое значение существующему узлу; если узел является привязкой, автоматически обновляйте все ссылки на него. Это действительно помогло бы закрыть этот пробел для редактирования в оба конца.

4. Я переключил d.items() на d.non_merged_items(), чтобы устранить проблему с чрезмерным расширением объединенных dicts при дампе (излишне повторяющийся вывод, который был изменен в исходном привязке).).

5. Да, это было бы необходимо, я не тестировал dict с ключами слияния, только ваш test.yaml

Ответ №2:

Было бы очень неплохо иметь поддержку для модификации существующих привязанных полей на месте с типами ScalarFloat / ScalarInt и т.д. YAML часто используется для конфигурационных файлов. Один из распространенных вариантов использования, с которым я столкнулся, заключается в создании нескольких конфигурационных файлов из очень большого конфигурационного файла шаблона с небольшими изменениями, вносимыми в новые файлы. Я бы загрузил файл шаблона в CommentedMap, изменил небольшой набор ключей на месте и сбросил его обратно в новый конфигурационный файл yaml. Этот поток работает очень хорошо, если ключи, подлежащие изменению, не привязаны. Когда они привязаны, привязки дублируются в новых файлах, как сообщает OP, и делают их недействительными. Обращение к каждому привязанному ключу вручную при последующей обработке может быть сложной задачей, когда их большое количество.