Как изменить привязанный скаляр в последовательности, не разрушая якорь в ruamel.yaml? - PullRequest
1 голос
/ 16 апреля 2019

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

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

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

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

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

# Header comment
---
# Post-header comment

# Reusable aliases
aliases:
  - &plain_value This is unencrypted
  - &string_password ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAYnFbMveZGBgd9aw7h4VV+M202zRdcP96UQs1q+ViznJK2Ee08hoW9jdIqVhNaecYALUihKjVYijJa649VF7BLZXV0svLEHD8LZeduoLS3iC9uszdhDFB2Q6R/Vv/ARjHNoWc6/D0nFN9vwcrQNITnvREl0WXYpR9SmW0krUpyr90gSAxTxPNJVlEOtA0afeJiXOtQEu/b8n+UDM3eXXRO+2SEXM4ub7fNcj6V9DgT3WwKBUjqzQ5DicnB19FNQ1cBGcmCo8qRv0JtbVqZ4+WJFGc06hOTcAJPsAaWWUn80ChcTnl4ELNzpJFoxAxHgepirskuIvuWZv3h/PL8Ez3NDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBSuVIsvWXMmdFJtJmtJxXxgCAGFCioe/zdphGqynmj6vVDnCjA3Xc0VPOCmmCl/cTKdg==]
  - &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

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

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

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..

Second , "Если мы вместо этого должны обернуть новое значение в некоторый объект, чтобы сохранить привязку (и другие атрибуты заменяемой записи), чтосамый простой подход, который также сохраняет все псевдонимы, которые ссылаются на него (так что они принимают обновленное значение) при выгрузке? "Моя попытка решить эту проблему требует гораздо больше кода, включая рекурсивные функции.Поскольку директивы 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)

Обратите внимание, что это создает действительный YAML, за исключением того, что все затронутые псевдонимы пропали, вместо этого вместо old значения привязанногоscalar.

Я ожидаю, что смогу изменить значение псевдонима скаляра в последовательности, не нарушая любую другую часть содержимого YAML.Основываясь на других постах, которые я видел о ruamel.yaml, я полностью согласен с тем, что мне может потребоваться выгрузить обновленный YAML в файл и перезагрузить его, чтобы псевдонимы в памяти обновились до нового значения.Я просто ожидаю изменить:

Входной файл

aliases:
  - &some_anchor Old value

usage: *some_anchor

на:

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

aliases:
  - &some_anchor NEW VALUE

usage: *some_anchor

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

Первый , обратите внимание, что исходный якорь был уничтожен, а значение top::hash:stringified_alias: теперь содержит исходный якорь, а старое значение вместо псевдонима для недавно обновленного скалярного значения в ['aliases'] [1]:

---
# Post-header comment

# Reusable aliases
aliases:
  - &plain_value This is unencrypted
  - New string password
  - &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: &string_password ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAYnFbMveZGBgd9aw7h4VV+M202zRdcP96UQs1q+ViznJK2Ee08hoW9jdIqVhNaecYALUihKjVYijJa649VF7BLZXV0svLEHD8LZeduoLS3iC9uszdhDFB2Q6R/Vv/ARjHNoWc6/D0nFN9vwcrQNITnvREl0WXYpR9SmW0krUpyr90gSAxTxPNJVlEOtA0afeJiXOtQEu/b8n+UDM3eXXRO+2SEXM4ub7fNcj6V9DgT3WwKBUjqzQ5DicnB19FNQ1cBGcmCo8qRv0JtbVqZ4+WJFGc06hOTcAJPsAaWWUn80ChcTnl4ELNzpJFoxAxHgepirskuIvuWZv3h/PL8Ez3NDBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBSuVIsvWXMmdFJtJmtJxXxgCAGFCioe/zdphGqynmj6vVDnCjA3Xc0VPOCmmCl/cTKdg==]

# ... snip ...

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

---
# Post-header comment

# Reusable aliases
aliases:
  - &plain_value This is unencrypted
  - &string_password New string password
  - &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 ...

Ответы [ 2 ]

1 голос
/ 17 апреля 2019

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

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

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

Поскольку ScalarStringтакая тонкая обертка, они по сути неизменные объекты, как и сама строка.В отличие от псевдонимов и списков с псевдонимами, которые являются объектами коллекции, которые могут быть очищены и затем заполнены (вместо замены новым экземпляром), вы не можете сделать это с помощью 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 password\n")

yaml.dump(data, sys.stdout)

, что дает:

# Post-header comment

# Reusable aliases
aliases:
  - &plain_value This is unencrypted
  - &string_password New string password
  - &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 отлично иметь привязку или псевдоним.Вы даже можете иметь привязанный ключ со значением, которое является псевдонимом соответствующего ключа.

0 голосов
/ 19 июня 2019

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

...