usando construct_undefined en ruamel from_yaml
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
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).