usando construct_undefined en ruamel from_yaml

Oct 16 2020

Estoy creando una etiqueta yaml personalizada MyTag. Puede contener cualquier yaml válido dado: mapa, escalar, ancla, secuencia, etc.

¿Cómo implemento la clase MyTag para modelar esta etiqueta de modo que ruamel analice el contenido de !mytaga exactamente de la misma manera que analizaría cualquier yaml dado? La MyTaginstancia simplemente almacena el resultado analizado del contenido de yaml.

El siguiente código funciona, y las afirmaciones deberían demostrar exactamente lo que debería hacer y todas pasan.

Pero no estoy seguro de si funciona por las razones correctas. . . Específicamente en el from_yamlmétodo de clase, ¿se está utilizando commented_obj = constructor.construct_undefined(node)una forma recomendada de lograr esto y consumir 1 y solo 1 del generador generado es correcto? ¿No es solo trabajar por accidente?

¿Debería usar algo como construct_object, o construct_mapo. . .? Los ejemplos que he podido encontrar tienden a saber qué tipo está construyendo, por lo que usaría construct_mapo construct_sequenceelegiría qué tipo de objeto construir. En este caso, efectivamente quiero aprovechar el análisis ruamel habitual / estándar para cualquier tipo desconocido que pueda haber allí, y simplemente almacenarlo en su propio 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)

Y algunos ejemplos 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

Respuestas

Anthon Oct 16 2020 at 04:23

En YAML puedes tener anclas y alias, y está perfectamente bien que un objeto sea hijo de sí mismo (usando un alias). Si desea volcar la estructura de datos de Python data:

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

se vuelca a:

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

y para eso se necesitan anclas y alias.

Al cargar una construcción de este tipo, ruamel.yaml recurre a las estructuras de datos anidadas, pero si el nodo de nivel superior no ha provocado la construcción de un objeto real al que se pueda hacer referencia al ancla, la hoja recursiva no puede resolver el alias.

Para resolver eso, se usa un generador, excepto para valores escalares. Primero crea un objeto vacío, luego recurre y actualiza sus valores. En el código que llama al constructor, se realiza una verificación para ver si se devuelve un generador y, en ese caso, next()se realiza en los datos y se "resuelve" la posible autorrecursión.

Porque llamas construct_undefined(), siempre obtienes un generador. Prácticamente, ese método podría devolver un valor si detecta un nodo escalar (que por supuesto no puede recurrir), pero no lo hace. Si lo hiciera, su código no podría cargar el siguiente documento YAML:

!mytag 1

sin modificaciones que prueben si obtienes un generador o no, como se hace en el código en ruamel.yaml llamando a los distintos constructores para que pueda manejar ambos construct_undefinedy eg construct_yaml_int(que no es un generador).