using construct_undefined in ruamel from_yaml

Oct 16 2020

Ich erstelle ein benutzerdefiniertes Yaml-Tag MyTag. Es kann eine beliebige gültige yaml - Karte, Skalar, Anker, Sequenz usw. enthalten.

Wie implementiere ich die Klasse MyTag, um dieses Tag so zu modellieren, dass ruamel den Inhalt von a genauso analysiert !mytag, wie es ein bestimmtes yaml analysieren würde? Die MyTagInstanz speichert nur das analysierte Ergebnis des Yaml-Inhalts.

Der folgende Code funktioniert, und die Asserts sollten genau zeigen, was er tun soll , und alle werden bestanden.

Aber ich bin mir nicht sicher, ob es aus den richtigen Gründen funktioniert. . . from_yamlVerwendet speziell in der Klassenmethode commented_obj = constructor.construct_undefined(node)eine empfohlene Methode, um dies zu erreichen, und ist es richtig, 1 und nur 1 vom ausgegebenen Generator zu verbrauchen? Es funktioniert nicht nur aus Versehen?

Sollte ich stattdessen so etwas wie construct_objectoder construct_mapoder verwenden? . .? Die Beispiele, die ich finden konnte, neigen dazu zu wissen, welchen Typ es konstruiert, also würden sie entweder den zu konstruierenden Objekttyp verwenden construct_mapoder construct_sequenceauswählen. In diesem Fall möchte ich die übliche / Standard-Ruamel-Analyse für jeden unbekannten Typ, der sich dort befindet, effektiv huckepack nehmen und sie einfach in ihrem eigenen Typ speichern.

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)

Und einige Beispiele 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

Antworten

Anthon Oct 16 2020 at 04:23

In YAML können Sie Anker und Aliase haben, und es ist vollkommen in Ordnung, wenn ein Objekt ein Kind von sich selbst ist (unter Verwendung eines Alias). Wenn Sie die Python-Datenstruktur sichern möchten data:

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

es wird ausgegeben an:

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

und dafür sind Anker und Aliase notwendig.

Beim Laden eines solchen Konstrukts wird ruamel.yaml in die verschachtelten Datenstrukturen zurückgeführt. Wenn der Knoten auf oberster Ebene jedoch nicht dazu geführt hat, dass ein reales Objekt erstellt wurde, auf das der Anker als Referenz verwendet werden kann, kann das rekursive Blatt den Alias ​​nicht auflösen.

Um dies zu lösen, wird ein Generator verwendet, mit Ausnahme von Skalarwerten. Es erstellt zuerst ein leeres Objekt, rekursiert es dann und aktualisiert seine Werte. Beim Aufrufen des Konstruktors durch Code wird geprüft, ob ein Generator zurückgegeben wird. In diesem Fall werden next()die Daten überprüft und die mögliche Selbstrekursion "aufgelöst".

Weil Sie anrufen construct_undefined(), bekommen Sie immer einen Generator. Praktisch könnte diese Methode einen Wert zurückgeben, wenn sie einen Skalarknoten erkennt (der natürlich nicht wiederkehren kann), aber nicht. In diesem Fall könnte Ihr Code das folgende YAML-Dokument nicht laden:

!mytag 1

ohne Modifikationen, die testen, ob Sie einen Generator erhalten oder nicht, wie im Code in ruamel.yaml beschrieben, der die verschiedenen Konstruktoren aufruft, damit er beide construct_undefinedund z. B. construct_yaml_int(was kein Generator ist) verarbeiten kann.