en utilisant construct_undefined dans ruamel from_yaml

Oct 16 2020

Je crée une balise yaml personnalisée MyTag. Il peut contenir n'importe quel yaml valide donné - carte, scalaire, ancre, séquence, etc.

Comment implémenter la classe MyTag pour modéliser cette balise afin que ruamel analyse le contenu d'un !mytagexactement de la même manière qu'il analyserait n'importe quel yaml donné? L' MyTaginstance stocke simplement le résultat analysé du contenu yaml.

Le code suivant fonctionne et les assertions doivent montrer exactement ce qu'il doit faire et elles passent toutes.

Mais je ne suis pas sûr que cela fonctionne pour les bonnes raisons. . . Plus précisément, dans la from_yamlméthode de classe, est-ce que l'utilisation d' commented_obj = constructor.construct_undefined(node)une méthode recommandée pour y parvenir, et est-ce que la consommation de 1 et seulement 1 du générateur produit est correcte? Cela ne fonctionne pas simplement par accident?

Dois-je plutôt utiliser quelque chose comme construct_object, ou construct_mapou. . .? Les exemples que j'ai pu trouver ont tendance à savoir quel type il est en train de construire, donc utiliserait construct_mapou construct_sequencechoisirait le type d'objet à construire. Dans ce cas, je veux effectivement utiliser l'analyse ruamel habituelle / standard pour tout type inconnu qui pourrait y être, et simplement le stocker dans son propre type.

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)

Et quelques exemples 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

Réponses

Anthon Oct 16 2020 at 04:23

Dans YAML, vous pouvez avoir des ancres et des alias, et il est parfaitement normal qu'un objet soit un enfant de lui-même (en utilisant un alias). Si vous souhaitez vider la structure de données Python data:

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

il se décharge dans:

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

et pour cela, des ancres et des alias sont nécessaires.

Lors du chargement d'une telle construction, ruamel.yaml revient dans les structures de données imbriquées, mais si le nœud de niveau supérieur n'a pas provoqué la construction d'un objet réel auquel l'ancre peut être référencée, la feuille récursive ne peut pas résoudre l'alias.

Pour résoudre cela, un générateur est utilisé, sauf pour les valeurs scalaires. Il crée d'abord un objet vide, puis récurent et met à jour ses valeurs. Dans le code appelant le constructeur, une vérification est effectuée pour voir si un générateur est retourné, et dans ce cas, elle next()est effectuée sur les données, et l'auto-récursivité potentielle est "résolue".

Parce que vous appelez construct_undefined(), vous obtenez toujours un générateur. Pratiquement, cette méthode pourrait retourner une valeur si elle détecte un nœud scalaire (qui bien sûr ne peut pas récurer), mais ce n'est pas le cas. Si tel était le cas, votre code pourrait ne pas charger le document YAML suivant:

!mytag 1

sans modifications qui testent si vous obtenez un générateur ou non, comme cela se fait dans le code de ruamel.yaml appelant les différents constructeurs pour qu'il puisse gérer les deux construct_undefinedet par exemple construct_yaml_int(qui n'est pas un générateur).