usando construct_undefined em ruamel from_yaml

Oct 16 2020

Estou criando uma tag personalizada do yaml MyTag. Ele pode conter qualquer yaml válido - mapa, escalar, âncora, sequência etc.

Como faço para implementar a classe MyTag para modelar essa tag de forma que ruamel analise o conteúdo de a !mytagexatamente da mesma maneira que faria para qualquer yaml? A MyTaginstância apenas armazena o resultado analisado do conteúdo do yaml.

O código a seguir funciona e as declarações devem demonstrar exatamente o que deve ser feito e todos passam.

Mas não tenho certeza se está funcionando pelos motivos certos. . . Especificamente no from_yamlmétodo de classe, usar commented_obj = constructor.construct_undefined(node)uma maneira recomendada de conseguir isso e consumir 1 e apenas 1 do gerador gerado está correto? Não é apenas trabalhar por acaso?

Em vez disso, devo usar algo como construct_object, ou construct_mapou. . .? Os exemplos que consegui encontrar tendem a saber que tipo ele está construindo, então usaria construct_mapou construct_sequenceescolheria qual tipo de objeto construir. Neste caso, eu efetivamente quero pegar carona na análise de ruamel usual / padrão para qualquer tipo desconhecido que possa haver lá, e apenas armazená-lo em seu próprio tipo.

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)

E alguns exemplos de yaml:

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

Respostas

Anthon Oct 16 2020 at 04:23

Em YAML você pode ter âncoras e apelidos, e é perfeitamente normal ter um objeto filho de si mesmo (usando um apelido). Se você deseja descartar a estrutura de dados Python data:

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

ele despeja para:

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

e para isso âncoras e apelidos são necessários.

Ao carregar tal construção, ruamel.yaml recursa nas estruturas de dados aninhadas, mas se o nó de nível superior não tiver feito um objeto real ser construído para o qual a âncora pode ser uma referência, a folha recursiva não pode resolver o alias.

Para resolver isso, um gerador é usado, exceto para valores escalares. Ele primeiro cria um objeto vazio e, em seguida, retorna e atualiza seus valores. Ao chamar o construtor de código, uma verificação é feita para ver se um gerador é retornado e, nesse caso, next()é feita nos dados, e a potencial auto-recursão "resolvida".

Porque você liga construct_undefined(), você sempre tem um gerador. Praticamente esse método poderia retornar um valor se detectar um nó escalar (que é claro não pode ser recorrente), mas não o faz. Em caso afirmativo, seu código não carregaria o seguinte documento YAML:

!mytag 1

sem modificações que testam se você obtém um gerador ou não, como é feito no código em ruamel.yaml chamando os vários construtores para que possa lidar com ambos construct_undefinede eg construct_yaml_int(que não é um gerador).