Как я могу включить файл 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
Тег YAMLtrue
, шаблон“**”
будет соответствовать любые файлы и ноль или более каталогов и подкаталогов.- С использованием
“**”
шаблон в больших деревьях каталогов может потребовать чрезмерное количество времени из-за рекурсивного поиска.
Для того, чтобы включить 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"
Объединяя другие ответы, вот короткое решение без перегрузкиLoader
class и работает с любым загрузчиком, работающим с файлами:
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]
Хотя у меня нет онлайн-ссылки, но это работает для меня.