Реализация функции «включения» через PyYAML и пользовательские теги - PullRequest
0 голосов
/ 03 октября 2018

Я пытаюсь реализовать синтаксис YAML следующим образом:

---
foo: bar
baz: buff
!Include: other/file

и обработать его во время загрузки с помощью PyYAML, чтобы объединить содержимое other/file.yml:

---
special: value

с содержимым исходного файла, производя:

---
foo: bar
baz: buff
special: value

до сих пор я следовал PyYAML Docs и Создание пользовательских тегов в PyYAML и смог получить несколькоНеловкая реализация:

---
foo: bar
baz: buff
included: !Include other/file

, что переводится в:

---
foo: bar
baz: buff
included:
  special: value

Я вижу, что в PyYAML Docs генерируется тег верхнего уровня, и он работает (Monster)но мой код не работает, когда я пытаюсь пойти по этому маршруту:

yaml.scanner.ScannerError: while scanning a simple key
  in "sample.yml", line 4, column 1
could not find expected ':'
  in "sample.yml", line 5, column 1

Текущий код:

import yaml
import sys
from UserDict import UserDict


class Include(yaml.YAMLObject, UserDict):
    yaml_tag = u'!Include'

    def __init__(self, path):
        self.path = path
        data = {}
        try:
            with open(self.path+'.yml', 'r') as f:
                data = yaml.load(f)
        except IOError:
            data = {}
        self.data = data

    def __str__(self):
        return str(self.data)

    def __repr__(self):
        return "{0}(path={1})".format(self.__class__.__name__, self.path)

    @classmethod
    def from_yaml(cls, loader, node):
        return Include(node.value).data

    @classmethod
    def to_yaml(cls, dumper, data):
        return dumper.represent_scalar(cls.yaml_tag, data.path)

yaml.SafeLoader.add_constructor(u'!Include', Include.from_yaml)
yaml.add_constructor(u'!Include', Include.from_yaml)
# Required for safe_dump
yaml.SafeDumper.add_multi_representer(Include, Include.to_yaml)
yaml.add_multi_representer(Include, Include.to_yaml)

if __name__ == '__main__':
    fname = sys.argv[1]
    f = open(fname, 'r')
    data = yaml.safe_load(f)
    print("{0}".format(str(data)))

1 Ответ

0 голосов
/ 03 октября 2018

Маркировка в YAML означает то же самое, что и в реальном мире: что вы снабжаете некоторый объект тегом.И тег не является заменой для этого объекта.

Когда вы делаете это: !Include: other/file, это эквивалентно выполнению !Include Null: other/file.И при разборе этого узла Null у вас нет доступа к other/file, он не анализируется и, возможно, еще даже не сканировался.

При разборе included: !Include other/file узел other/file не не имеет понятие своего контекста.Это может быть, например, в виде стека, из которого вы можете получить доступ к последнему объекту, но теперь реализован PyYAML.Это означает, что если вы сделаете это, вы можете заменить помеченный узел только структурой данных, загруженной из включаемого файла.

Что вы можете сделать, это определить «специальный ключ», например, +< изатем поместите тег !Include в сопоставление:

!Include
foo: bar
baz: buff
+<: other/file

Таким образом, вы можете реализовать конструктор для сопоставления для создания dict, но при обнаружении специального ключа используйте значение ключа, связанное со значением, в качестве именифайла для загрузки и вставки в качестве дополнительного ключа / значений в создаваемый вами dict (поэтому вам не нужно обращаться к некоторому недоступному контекстному узлу).Вам нужно было бы найти какой-то способ определения приоритета ключей, которые приходят из включенного файла, и ключей, которые исходят из фактического, включая отображение.Вы можете реализовать несколько включений, позволяя значению быть списком имен файлов.Это похоже на независимый от языка ключей слияния тип .

Вы могли бы даже сделать вышеупомянутое без использования тега вообще, добавив преобразователь для +< (возьмите один дляфункция слияния в качестве примера), подкласс SafeLoader и реализация метода flatten_mapping для поддержки этого включения.Однако это будет означать, что для других не так очевидно, что происходит что-то особенное, как при наличии тега.

Обратите внимание:

  • вы должны опустить директивуend-seperator (---), это излишне, поскольку у вас нет никаких директив.
  • ваши YAML-файлы должны иметь расширение .yaml , если это невозможно (например,потому что файловая система не поддерживает суффиксы длиннее трех символов)

В ruamel.yaml вы можете реализовать это, используя:

import sys
import ruamel.yaml
from pathlib import Path

yaml = ruamel.yaml.YAML(typ='safe', pure=True)

yaml_str = """\
!Include
foo: bar
baz: buff
+<: other/file.yaml
"""

class Include:
    @classmethod
    def from_yaml(cls, constructor, node):
        mapping = constructor.construct_mapping(node)
        file_names = mapping.get('+<')
        if file_names is None:
            return mapping
        if not isinstance(file_names, list):
            file_names = [file_names]
        y = constructor.loader
        yaml = ruamel.yaml.YAML(typ=y.typ, pure=y.pure)
        for file_name in file_names:
            for key, value in yaml.load(Path(file_name)).items():
                if key in mapping:
                    continue
                mapping[key] = value
        return mapping


yaml.register_class(Include)

data = yaml.load(yaml_str)
print(data)

, что дает:

{'foo': 'bar', 'baz': 'buff', '+<': 'other/file.yaml', 'special': 'value'}

в PyYAML вы должны быть в состоянии сделать что-то подобное, с большим количеством кода и поддержкой только спецификации YAML 1.1 (которая была заменена в 2009 году).

...