Как использовать собственный класс словаря при загрузке yaml?

В настоящее время я загружаю такой файл YAML

 import yaml
 yaml.load('''level0:
                 stuff: string0
                 level1: 
                     stuff: string1
                     level2: ...''')

Приведенный выше код создает вложенные словари. Вместо создания вложенных словарей я хочу создавать вложенные экземпляры FancyDict объектов.

class FancyDict(collections.MutableMapping):
   def __init__(self, *args, **kwargs):
       for name in kwargs:
          setattr(self, name, kwargs[name])

В разделе Конструкторы, Представители, Резолверы, похоже, не рассматривается этот случай, когда я хочу глобально переопределить конструкцию класса для всех словарей вместо специальных тегированных.

Мне просто нужен хук, который будет называться созданным / завершенным объектом (узлом?).
Есть ли простой способ сделать это или мне просто нужно пройти по вложенным словарям, которые возвращает мне yaml.load, и исправить их сам?


person Pushpendre    schedule 14.04.2016    source источник


Ответы (2)


Этого хука нет, создаваемый тип жестко запрограммирован в construct.BaseConstructor.construct_mapping().

Чтобы решить эту проблему, создайте свой собственный конструктор и на его основе свой собственный загрузчик и передайте его в качестве опции для load():

import sys
import collections
import ruamel.yaml as yaml

yaml_str = """\
level0:
  stuff: string0
  level1:
    stuff: string1
    level2: ...
"""

from ruamel.yaml.reader import Reader
from ruamel.yaml.scanner import Scanner
from ruamel.yaml.parser import Parser
from ruamel.yaml.composer import Composer
from ruamel.yaml.constructor import SafeConstructor
from ruamel.yaml.resolver import Resolver
from ruamel.yaml.nodes import MappingNode


class FancyDict(collections.MutableMapping):
    def __init__(self, *args, **kwargs):
        for name in kwargs:
            setattr(self, name, kwargs[name])

    # provide the missing __getitem__, __setitem__, __delitem__, __iter__, and __len__.

class MyConstructor(SafeConstructor):
    def construct_mapping(self, node, deep=False):
        res = SafeConstructor.construct_mapping(self, node, deep)
        assert isinstance(res, dict)
        return FancyDict(**res)


class MyLoader(Reader, Scanner, Parser, Composer, MyConstructor, Resolver):
    def __init__(self, stream, version=None):
        Reader.__init__(self, stream)
        Scanner.__init__(self)
        Parser.__init__(self)
        Composer.__init__(self)
        MyConstructor.__init__(self)
        Resolver.__init__(self)


data = yaml.load(yaml_str, Loader=MyLoader)

Когда вы запустите это, вы получите сообщение об ошибке, что FancyDict - это абстрактный класс, который не может быть создан:

TypeError: невозможно создать экземпляр абстрактного класса FancyDict с абстрактными методами __delitem__, __getitem__, __iter__, __len__, __setitem__

Я предполагаю, что в вашем настоящем FancyDict они реализованы.


ruamel.yaml - это библиотека YAML, которая поддерживает YAML 1.2 (я рекомендую использовать ее, но тогда я являюсь автором пакета). PyYAML поддерживает только (большую часть) YAML 1.1. Более проблематично то, что у него разные constructor.py файлы для Python2 и Python3, из-за этого вы не сможете добавить приведенный выше код в PyYAML.

person Anthon    schedule 14.04.2016
comment
Спасибо за развернутый ответ - person Pushpendre; 14.04.2016
comment
Извините, что не ответил быстрее ;-) - person Anthon; 14.04.2016

Я нашел решение, которое действительно работает на PyYaml.

class Loader(yaml.FullLoader):

    def construct_yaml_map(self, node):
        data = MyDictionaryClass()
        yield data
        value = self.construct_mapping(node)
        data.update(value)

Loader.add_constructor(
    'tag:yaml.org,2002:map',
    Loader.construct_yaml_map
)

Проблема с использованием решения из этого ответа заключается в том, что PyYaml преобразует отображение обратно в словарь для функции construct_yaml_map. Просто заменить эту функцию в подклассе недостаточно, поскольку для SafeLoader добавлен пользовательский add_constructor, поэтому вы можете перезаписать его, чтобы использовать новый construct_yaml_map для вашего класса.

person JBernardo    schedule 25.04.2021