используя construct_undefined в ruamel from_yaml

Oct 16 2020

Я создаю собственный тег yaml MyTag. Он может содержать любой заданный допустимый yaml - карту, скаляр, привязку, последовательность и т. Д.

Как реализовать класс MyTag для моделирования этого тега, чтобы ruamel анализировал содержимое a !mytagточно так же, как он будет анализировать любой заданный yaml? MyTagЭкземпляра просто хранит независимо от разобранного результата содержания YAML является.

Следующий код работает, и утверждения должны точно демонстрировать, что они должны делать, и все они проходят.

Но я не уверен, работает ли он по правильным причинам. . . В частности, в from_yamlметоде класса используется commented_obj = constructor.construct_undefined(node)рекомендуемый способ достижения этой цели и потребляет 1 и только 1 из полученного генератора правильно? Это не просто случайно работает?

Должен ли я вместо этого использовать что-то вроде construct_object, или construct_mapили. . .? Примеры, которые мне удалось найти, как правило, знают, какой тип он конструирует, поэтому нужно либо использовать, construct_mapлибо construct_sequenceвыбрать тип объекта для создания. В этом случае я действительно хочу использовать обычный / стандартный синтаксический анализ ruamel для любого неизвестного типа, который там может быть, и просто сохранить его в своем собственном типе.

import ruamel.yaml
from ruamel.yaml.comments import CommentedMap, CommentedSeq, TaggedScalar


class MyTag():
    yaml_tag = '!mytag'

    def __init__(self, value):
        self.value = value

    @classmethod
    def from_yaml(cls, constructor, node):
        commented_obj = constructor.construct_undefined(node)
        flag = False
        for data in commented_obj:
            if flag:
                raise AssertionError('should only be 1 thing in generator??')
            flag = True

        return cls(data)


with open('mytag-sample.yaml') as yaml_file:
    yaml_parser = ruamel.yaml.YAML()
    yaml_parser.register_class(MyTag)
    yaml = yaml_parser.load(yaml_file)

custom_tag_with_list = yaml['root'][0]['arb']['k2']
assert type(custom_tag_with_list) is MyTag
assert type(custom_tag_with_list.value) is CommentedSeq
print(custom_tag_with_list.value)

standard_list = yaml['root'][0]['arb']['k3']
assert type(standard_list) is CommentedSeq
assert standard_list == custom_tag_with_list.value

custom_tag_with_map = yaml['root'][1]['arb']
assert type(custom_tag_with_map) is MyTag
assert type(custom_tag_with_map.value) is CommentedMap
print(custom_tag_with_map.value)

standard_map = yaml['root'][1]['arb_no_tag']
assert type(standard_map) is CommentedMap
assert standard_map == custom_tag_with_map.value

custom_tag_scalar = yaml['root'][2]
assert type(custom_tag_scalar) is MyTag
assert type(custom_tag_scalar.value) is TaggedScalar

standard_tag_scalar = yaml['root'][3]
assert type(standard_tag_scalar) is str
assert standard_tag_scalar == str(custom_tag_scalar.value)

И пример ямла:

root:
  - item: blah
    arb:
      k1: v1
      k2: !mytag
        - one
        - two
        - three-k1: three-v1
          three-k2: three-v2
          three-k3: 123 # arb comment
          three-k4: 
            - a
            - b
            - True
      k3:
        - one
        - two
        - three-k1: three-v1
          three-k2: three-v2
          three-k3: 123 # arb comment
          three-k4: 
            - a
            - b
            - True
  - item: argh
    arb: !mytag
            k1: v1
            k2: 123
            # blah line 1
            # blah line 2
            k3:
              k31: v31
              k32: 
                - False
                - string here
                - 321
    arb_no_tag:
      k1: v1
      k2: 123
      # blah line 1
      # blah line 2
      k3:
        k31: v31
        k32: 
          - False
          - string here
          - 321
  - !mytag plain scalar
  - plain scalar
  - item: no comment
    arb:
      - one1
      - two2

Ответы

Anthon Oct 16 2020 at 04:23

В YAML у вас могут быть якоря и псевдонимы, и вполне нормально, чтобы объект был дочерним по отношению к самому себе (используя псевдоним). Если вы хотите сбросить структуру данных Python data:

data = [1, 2, 4, dict(a=42)]
data[3]['b'] = data

он сбрасывается в:

&id001
- 1
- 2
- 4
- a: 42
  b: *id001

а для этого нужны якоря и псевдонимы.

При загрузке такой конструкции ruamel.yaml рекурсивно переходит во вложенные структуры данных, но если узел верхнего уровня не привел к созданию реального объекта, на который можно сделать ссылку привязкой, рекурсивный лист не может разрешить псевдоним.

Чтобы решить эту проблему, используется генератор, за исключением скалярных значений. Сначала он создает пустой объект, затем рекурсирует и обновляет его значения. В коде, вызывающем конструктор, выполняется проверка, возвращается ли генератор, и в этом случае next()выполняется с данными, и потенциальная саморекурсия «разрешается».

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

!mytag 1

без модификаций, которые проверяют, получите ли вы генератор или нет, как это сделано в коде в ruamel.yaml, вызывая различные конструкторы, чтобы он мог обрабатывать оба construct_undefinedи, например construct_yaml_int(который не является генератором).