Как я могу включить файл YAML в другой?

Таким образом, у меня есть два файла YAML, "A" и "B", и я хочу, чтобы содержимое A было вставлено внутри B, либо встроено в существующую структуру данных, например, массив, или как дочерний элемент элемента, например, значение для определенного ключа хеша.

Это вообще возможно? Как? Если нет, какие-либо указатели на нормативную ссылку?

18 ответов

Решение

Нет, YAML не содержит каких-либо операторов "import" или "include".

Ваш вопрос не требует Python-решения, но вот тот, который использует PyYAML.

PyYAML позволяет вам присоединять пользовательские конструкторы (такие как !include) к погрузчику YAML. Я включил корневой каталог, который можно настроить так, чтобы это решение поддерживало относительные и абсолютные ссылки на файлы.

Основанное на классе решение

Вот решение на основе классов, которое позволяет избежать глобальной корневой переменной моего исходного ответа.

См. Этот список аналогичного, более надежного решения Python 3, которое использует метакласс для регистрации пользовательского конструктора.

import yaml
import os.path

class Loader(yaml.SafeLoader):

    def __init__(self, stream):

        self._root = os.path.split(stream.name)[0]

        super(Loader, self).__init__(stream)

    def include(self, node):

        filename = os.path.join(self._root, self.construct_scalar(node))

        with open(filename, 'r') as f:
            return yaml.load(f, Loader)

Loader.add_constructor('!include', Loader.include)

Пример:

foo.yaml

a: 1
b:
    - 1.43
    - 543.55
c: !include bar.yaml

bar.yaml

- 3.6
- [1, 2, 3]

Теперь файлы могут быть загружены с помощью:

>>> with open('foo.yaml', 'r') as f:
>>>    data = yaml.load(f, Loader)
>>> data
{'a': 1, 'b': [1.43, 543.55], 'c': [3.6, [1, 2, 3]]}

Если вы используете версию YAML от Symfony, это возможно, например так:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }

Для пользователей Python вы можете попробовать pyyaml-include.

устанавливать

pip install pyyaml-include

использование

import yaml
from yamlinclude import YamlIncludeConstructor

YamlIncludeConstructor.add_to_loader_class(loader_class=yaml.FullLoader, base_dir='/your/conf/dir')

with open('0.yaml') as f:
    data = yaml.load(f, Loader=yaml.FullLoader)

print(data)

Считайте, что у нас есть такие файлы YAML:

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml
  • 1.yaml содержание:
name: "1"
  • 2.yaml содержание:
name: "2"

Включить файлы по имени

  • На верхнем уровне:

    Если 0.yaml было:

!include include.d/1.yaml

Мы получим:

{"name": "1"}
  • В картировании:

    Если 0.yaml было:

file1: !include include.d/1.yaml
file2: !include include.d/2.yaml

Мы получим:

  file1:
    name: "1"
  file2:
    name: "2"
  • По порядку:

    Если 0.yaml было:

files:
  - !include include.d/1.yaml
  - !include include.d/2.yaml

Мы получим:

files:
  - name: "1"
  - name: "2"

Примечание:

Имя файла может быть абсолютным (например, /usr/conf/1.5/Make.yml) или родственник (как ../../cfg/img.yml).

Включить файлы по шаблону

Имя файла может содержать подстановочные знаки в стиле оболочки. Данные, загруженные из файла (ов), найденных с помощью подстановочных знаков, будут установлены в последовательности.

Если 0.yaml было:

files: !include include.d/*.yaml

Мы получим:

files:
  - name: "1"
  - name: "2"

Примечание:

  • За Python>=3.5, если recursive аргумент !include Тег YAML true, шаблон “**” будет соответствовать любые файлы и ноль или более каталогов и подкаталогов.
  • С использованием “**” шаблон в больших деревьях каталогов может потребовать чрезмерное количество времени из-за рекурсивного поиска.

Для того, чтобы включить recursive аргумент, мы напишем !include отметить в Mapping или же Sequence Режим:

  • Аргументы в Sequence Режим:
!include [tests/data/include.d/**/*.yaml, true]
  • Аргументы в Mapping Режим:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}

Насколько я знаю, include не поддерживаются напрямую в yaml, однако вам придется предоставить механизм самостоятельно, однако это обычно легко сделать.

Я использовал yaml в качестве языка конфигурации в своих приложениях на Python, и в этом случае часто определяю такое соглашение:

>>> main.yml <<<
includes: [ wibble.yml, wobble.yml]

Затем в моем (Python) коде я делаю:

import yaml
cfg = yaml.load(open("main.yml"))
for inc in cfg.get("includes", []):
   cfg.update(yaml.load(open(inc)))

Единственным недостатком является то, что переменные в include всегда будут переопределять переменные в main, и нет никакого способа изменить этот приоритет, изменив место, где в файле main.yml появляется оператор "includes:".

С другой стороны, yaml не поддерживает включение, поскольку оно не разработано так же, как разметка на основе файлов. Что будет означать включение, если вы получите его в ответ на запрос ajax?

Стандарт YML не определяет способ сделать это. И эта проблема не ограничивается YML. JSON имеет те же ограничения.

Многие приложения, использующие конфигурации на основе YML или JSON, рано или поздно сталкиваются с этой проблемой. И когда это происходит, они составляют свое собственное соглашение.

например, для определений API чванства:

$ref: 'file.yml'

например, для конфигураций docker compose:

services:
  app:
    extends:
      file: docker-compose.base.yml

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

В продолжение ответа @Josh_Bode, вот мое собственное решение PyYAML, преимущество которого состоит в том, что он является отдельным подклассом yaml.Loader, Это не зависит ни от глобальных переменных уровня модуля, ни от изменения глобального состояния yaml модуль.

import yaml, os

class IncludeLoader(yaml.Loader):                                                 
    """                                                                           
    yaml.Loader subclass handles "!include path/to/foo.yml" directives in config  
    files.  When constructed with a file object, the root path for includes       
    defaults to the directory containing the file, otherwise to the current       
    working directory. In either case, the root path can be overridden by the     
    `root` keyword argument.                                                      

    When an included file F contain its own !include directive, the path is       
    relative to F's location.                                                     

    Example:                                                                      
        YAML file /home/frodo/one-ring.yml:                                       
            ---                                                                   
            Name: The One Ring                                                    
            Specials:                                                             
                - resize-to-wearer                                                
            Effects: 
                - !include path/to/invisibility.yml                            

        YAML file /home/frodo/path/to/invisibility.yml:                           
            ---                                                                   
            Name: invisibility                                                    
            Message: Suddenly you disappear!                                      

        Loading:                                                                  
            data = IncludeLoader(open('/home/frodo/one-ring.yml', 'r')).get_data()

        Result:                                                                   
            {'Effects': [{'Message': 'Suddenly you disappear!', 'Name':            
                'invisibility'}], 'Name': 'The One Ring', 'Specials':              
                ['resize-to-wearer']}                                             
    """                                                                           
    def __init__(self, *args, **kwargs):                                          
        super(IncludeLoader, self).__init__(*args, **kwargs)                      
        self.add_constructor('!include', self._include)                           
        if 'root' in kwargs:                                                      
            self.root = kwargs['root']                                            
        elif isinstance(self.stream, file):                                       
            self.root = os.path.dirname(self.stream.name)                         
        else:                                                                     
            self.root = os.path.curdir                                            

    def _include(self, loader, node):                                    
        oldRoot = self.root                                              
        filename = os.path.join(self.root, loader.construct_scalar(node))
        self.root = os.path.dirname(filename)                           
        data = yaml.load(open(filename, 'r'))                            
        self.root = oldRoot                                              
        return data                                                      

С помощью Yglu вы можете импортировать другие файлы, например:

А.ямл

foo: !? $import('B.yaml')

Б.Ямл

bar: Hello
$ yglu A.yaml
foo:
  bar: Hello

В качестве $import является функцией, вы также можете передать выражение в качестве аргумента:

  dep: !- b
  foo: !? $import($_.dep.toUpper() + '.yaml')

Это даст тот же результат, что и выше.

Отказ от ответственности: я являюсь автором Yglu.

Стандартный YAML 1.2 не включает эту функцию изначально. Тем не менее, многие реализации предоставляют для этого некоторые расширения.

Я представляю способ достижения этого с помощью Java и snakeyaml:1.24 (Библиотека Java для синтаксического анализа / выдачи файлов YAML), которая позволяет создать собственный тег YAML для достижения следующей цели (вы увидите, что я использую его для загрузки наборов тестов, определенных в нескольких файлах YAML, и что я заставил его работать как список включает для цели test: узел):

# ... yaml prev stuff

tests: !include
  - '1.hello-test-suite.yaml'
  - '3.foo-test-suite.yaml'
  - '2.bar-test-suite.yaml'

# ... more yaml document

Вот одноклассная Java, которая позволяет обрабатывать !includeтег. Файлы загружаются из пути к классам (каталог ресурсов Maven):

/**
 * Custom YAML loader. It adds support to the custom !include tag which allows splitting a YAML file across several
 * files for a better organization of YAML tests.
 */
@Slf4j   // <-- This is a Lombok annotation to auto-generate logger
public class MyYamlLoader {

    private static final Constructor CUSTOM_CONSTRUCTOR = new MyYamlConstructor();

    private MyYamlLoader() {
    }

    /**
     * Parse the only YAML document in a stream and produce the Java Map. It provides support for the custom !include
     * YAML tag to split YAML contents across several files.
     */
    public static Map<String, Object> load(InputStream inputStream) {
        return new Yaml(CUSTOM_CONSTRUCTOR)
                .load(inputStream);
    }


    /**
     * Custom SnakeYAML constructor that registers custom tags.
     */
    private static class MyYamlConstructor extends Constructor {

        private static final String TAG_INCLUDE = "!include";

        MyYamlConstructor() {
            // Register custom tags
            yamlConstructors.put(new Tag(TAG_INCLUDE), new IncludeConstruct());
        }

        /**
         * The actual include tag construct.
         */
        private static class IncludeConstruct implements Construct {

            @Override
            public Object construct(Node node) {
                List<Node> inclusions = castToSequenceNode(node);
                return parseInclusions(inclusions);
            }

            @Override
            public void construct2ndStep(Node node, Object object) {
                // do nothing
            }

            private List<Node> castToSequenceNode(Node node) {
                try {
                    return ((SequenceNode) node).getValue();

                } catch (ClassCastException e) {
                    throw new IllegalArgumentException(String.format("The !import value must be a sequence node, but " +
                            "'%s' found.", node));
                }
            }

            private Object parseInclusions(List<Node> inclusions) {

                List<InputStream> inputStreams = inputStreams(inclusions);

                try (final SequenceInputStream sequencedInputStream =
                             new SequenceInputStream(Collections.enumeration(inputStreams))) {

                    return new Yaml(CUSTOM_CONSTRUCTOR)
                            .load(sequencedInputStream);

                } catch (IOException e) {
                    log.error("Error closing the stream.", e);
                    return null;
                }
            }

            private List<InputStream> inputStreams(List<Node> scalarNodes) {
                return scalarNodes.stream()
                        .map(this::inputStream)
                        .collect(toList());
            }

            private InputStream inputStream(Node scalarNode) {
                String filePath = castToScalarNode(scalarNode).getValue();
                final InputStream is = getClass().getClassLoader().getResourceAsStream(filePath);
                Assert.notNull(is, String.format("Resource file %s not found.", filePath));
                return is;
            }

            private ScalarNode castToScalarNode(Node scalarNode) {
                try {
                    return ((ScalarNode) scalarNode);

                } catch (ClassCastException e) {
                    throw new IllegalArgumentException(String.format("The value must be a scalar node, but '%s' found" +
                            ".", scalarNode));
                }
            }
        }

    }

}

К сожалению, YAML не предоставляет этого в своем стандарте.

Но если вы используете Ruby, есть жемчужина, предоставляющая требуемую функциональность путем расширения библиотеки ruby ​​YAML: https://github.com/entwanderer/yaml_extend

Приведу несколько примеров для справки.

import yaml

main_yaml = """
Package:
 - !include _shape_yaml    
 - !include _path_yaml
"""

_shape_yaml = """
# Define
Rectangle: &id_Rectangle
    name: Rectangle
    width: &Rectangle_width 20
    height: &Rectangle_height 10
    area: !product [*Rectangle_width, *Rectangle_height]

Circle: &id_Circle
    name: Circle
    radius: &Circle_radius 5
    area: !product [*Circle_radius, *Circle_radius, pi]

# Setting
Shape:
    property: *id_Rectangle
    color: red
"""

_path_yaml = """
# Define
Root: &BASE /path/src/

Paths: 
    a: &id_path_a !join [*BASE, a]
    b: &id_path_b !join [*BASE, b]

# Setting
Path:
    input_file: *id_path_a
"""


# define custom tag handler
def yaml_import(loader, node):
    other_yaml_file = loader.construct_scalar(node)
    return yaml.load(eval(other_yaml_file), Loader=yaml.SafeLoader)


def yaml_product(loader, node):
    import math
    list_data = loader.construct_sequence(node)
    result = 1
    pi = math.pi
    for val in list_data:
        result *= eval(val) if isinstance(val, str) else val
    return result


def yaml_join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])


def yaml_ref(loader, node):
    ref = loader.construct_sequence(node)
    return ref[0]


def yaml_dict_ref(loader: yaml.loader.SafeLoader, node):
    dict_data, key, const_value = loader.construct_sequence(node)
    return dict_data[key] + str(const_value)


def main():
    # register the tag handler
    yaml.SafeLoader.add_constructor(tag='!include', constructor=yaml_import)
    yaml.SafeLoader.add_constructor(tag='!product', constructor=yaml_product)
    yaml.SafeLoader.add_constructor(tag='!join', constructor=yaml_join)
    yaml.SafeLoader.add_constructor(tag='!ref', constructor=yaml_ref)
    yaml.SafeLoader.add_constructor(tag='!dict_ref', constructor=yaml_dict_ref)

    config = yaml.load(main_yaml, Loader=yaml.SafeLoader)

    pk_shape, pk_path = config['Package']
    pk_shape, pk_path = pk_shape['Shape'], pk_path['Path']
    print(f"shape name: {pk_shape['property']['name']}")
    print(f"shape area: {pk_shape['property']['area']}")
    print(f"shape color: {pk_shape['color']}")

    print(f"input file: {pk_path['input_file']}")


if __name__ == '__main__':
    main()

выход

shape name: Rectangle
shape area: 200
shape color: red
input file: /path/src/a

Обновление 2

и вы можете комбинировать это, вот так

# xxx.yaml
CREATE_FONT_PICTURE:
  PROJECTS:
    SUNG: &id_SUNG
      name: SUNG
      work_dir: SUNG
      output_dir: temp
      font_pixel: 24


  DEFINE: &id_define !ref [*id_SUNG]  # you can use config['CREATE_FONT_PICTURE']['DEFINE'][name, work_dir, ... font_pixel]
  AUTO_INIT:
    basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # SUNG30

# ↓ This is not correct.
# basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # It will build by Deep-level. id_define is Deep-level: 2. So you must put it after 2. otherwise, it can't refer to the correct value.

Я думаю, что решение, используемое @maxy-B, выглядит великолепно. Однако для меня это не удалось с вложенными включениями. Например, если config_1.yaml включает config_2.yaml, который включает config_3.yaml, возникла проблема с загрузчиком. Однако, если вы просто указываете новый класс загрузчика на себя при загрузке, он работает! В частности, если мы заменим старую функцию _include на слегка измененную версию:

def _include(self, loader, node):                                    
     oldRoot = self.root                                              
     filename = os.path.join(self.root, loader.construct_scalar(node))
     self.root = os.path.dirname(filename)                           
     data = yaml.load(open(filename, 'r'), loader = IncludeLoader)                            
     self.root = oldRoot                                              
     return data

Поразмыслив, я согласен с другими комментариями, что вложенная загрузка не подходит для yaml в целом, поскольку входной поток может не быть файлом, но это очень полезно!

С Symfony его обработка yaml косвенно позволит вам вкладывать файлы yaml. Хитрость заключается в том, чтобы использовать parameters вариант. например:

common.yml

parameters:
    yaml_to_repeat:
        option: "value"
        foo:
            - "bar"
            - "baz"

config.yml

imports:
    - { resource: common.yml }
whatever:
    thing: "%yaml_to_repeat%"
    other_thing: "%yaml_to_repeat%"

Результат будет таким же, как:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"

Объединяя другие ответы, вот короткое решение без перегрузкиLoaderclass и работает с любым загрузчиком, работающим с файлами:

      import json
from pathlib import Path
from typing import Any

import yaml


def yaml_include_constructor(loader: yaml.BaseLoader, node: yaml.Node) -> Any:
    """Include file referenced with !include node"""

    # noinspection PyTypeChecker
    fp = Path(loader.name).parent.joinpath(loader.construct_scalar(node)).resolve()
    fe = fp.suffix.lstrip(".")

    with open(fp, 'r') as f:
        if fe in ("yaml", "yml"):
            return yaml.load(f, type(loader))
        elif fe in ("json", "jsn"):
            return json.load(f)
        else:
            return f.read()


def main():
    loader = yaml.SafeLoader  # Works with any loader
    loader.add_constructor("!include", yaml_include_constructor)

    with open(...) as f:
        yml = yaml.load(f, loader)

# noinspection PyTypeCheckerесть ли предупреждение PEP-проверки Ожидаемый тип 'ScalarNode', вместо этого получен 'Node' при передачеnode: yaml.Nodeкloader.construct_scalar().

Это решение не работает, еслиyaml.loadвходной поток не является файловым потоком, так какloader.nameне содержит пути в этом случае:

      class Reader(object):
    ...
    def __init__(self, stream):
        ...
        if isinstance(stream, str):
            self.name = "<unicode string>"
            ...
        elif isinstance(stream, bytes):
            self.name = "<byte string>"
            ...
        else:
            self.name = getattr(stream, 'name', "<file>")
            ...

В моем случае использования я знаю, что будут включены только файлы YAML, поэтому решение можно упростить еще больше:

      def yaml_include_constructor(loader: yaml.Loader, node: yaml.Node) -> Any:
    """Include YAML file referenced with !include node"""
    with open(Path(loader.name).parent.joinpath(loader.construct_yaml_str(node)).resolve(), 'r') as f:
        return yaml.load(f, type(loader))


Loader = yaml.SafeLoader  # Works with any loader
Loader.add_constructor("!include", yaml_include_constructor)


def main():
    with open(...) as f:
        yml = yaml.load(f, Loader=Loader)

или даже однострочный с использованием лямбда:

      Loader = yaml.SafeLoader  # Works with any loader
Loader.add_constructor("!include",
                       lambda l, n: yaml.load(Path(l.name).parent.joinpath(l.construct_scalar(n)).read_text(), type(l)))

На основании предыдущих сообщений:

        class SimYamlLoader(yaml.SafeLoader):
        '''
        Simple custom yaml loader that supports include, e.g:

        main.yaml:

        - !include file1.yaml
        - !include dir/file2.yaml

        '''

        def __init__(self, stream):
            self.root = os.path.split(stream.name)[0]
            super().__init__(stream)

    def _include(loader, node):
        filename = os.path.join(loader.root, loader.construct_scalar(node))
        with open(filename, 'r') as f:
            return yaml.load(f, SimYamlLoader)
    SimYamlLoader.add_constructor('!include', _include)

    # example:
    with open('main.yaml', 'r') as f:
        lists = yaml.load(f, SimYamlLoader)
        # if you want to merge the lists
        data = functools.reduce(
            lambda x, y: x if y is None else {**x, **dict(y)}, lists, {})
        # python 3.10+:lambda x, y: x if y is None else x | dict(y), lists, {})

Добавив первоначальный ответ @Joshbode выше, я немного изменил фрагмент, чтобы он поддерживал шаблоны подстановочных знаков в стиле UNIX.

Я не тестировал в окнах, хотя. Я столкнулся с проблемой разделения массива в большом yaml на несколько файлов для удобства обслуживания и искал решение для ссылки на несколько файлов в одном и том же массиве базового yaml. Отсюда приведенное ниже решение. Решение не поддерживает рекурсивную ссылку. Он поддерживает только подстановочные знаки на заданном уровне каталога, указанном в базовом yaml.

      import yaml
import os
import glob


# Base code taken from below link :-
# Ref:https://stackoverflow.com/a/9577670
class Loader(yaml.SafeLoader):

    def __init__(self, stream):

        self._root = os.path.split(stream.name)[0]

        super(Loader, self).__init__(stream)

    def include(self, node):
        consolidated_result = None
        filename = os.path.join(self._root, self.construct_scalar(node))

        # Below section is modified for supporting UNIX wildcard patterns
        filenames = glob.glob(filename)
        
        # Just to ensure the order of files considered are predictable 
        # and easy to debug in case of errors.
        filenames.sort()
        for file in filenames:
            with open(file, 'r') as f:
                result = yaml.load(f, Loader)

            if isinstance(result, list):
                if not isinstance(consolidated_result, list):
                    consolidated_result = []
                consolidated_result += result
            elif isinstance(result, dict):
                if not isinstance(consolidated_result, dict):
                    consolidated_result = {}
                consolidated_result.update(result)
            else:
                consolidated_result = result

        return consolidated_result


Loader.add_constructor('!include', Loader.include)

Применение

      a:
  !include a.yaml

b:
  # All yamls included within b folder level will be consolidated
  !include b/*.yaml

Может быть, это может вдохновить вас, попробуйте привести в соответствие с соглашениями JBB:

https://docs.openstack.org/infra/jenkins-job-builder/definition.html

- job: name: test-job-include-raw-1 builders: - shell: !include-raw: include-raw001-hello-world.sh

Возможно, это не было поддержано, когда был задан вопрос, но вы можете импортировать другой файл YAML в один:

imports: [/your_location_to_yaml_file/Util.area.yaml]

Хотя у меня нет онлайн-ссылки, но это работает для меня.

Другие вопросы по тегам