Как я могу включить файл YAML в другой?
-
22-08-2019 - |
Вопрос
Итак, у меня есть два файла YAML, «A» и «B», и я хочу, чтобы содержимое A было вставлено внутрь B, либо вставлено в существующую структуру данных, например массив, либо как дочерний элемент элемента, например, значение. для определенного хеш-ключа.
Это вообще возможно?Как?Если нет, есть ли какие-нибудь ссылки на нормативную ссылку?
Решение
Нет, YAML не содержит никаких операторов «импорта» или «включения».
Другие советы
Ваш вопрос не требует решения Python, но вот одно из них, использующее ПиЯМЛ.
PyYAML позволяет вам присоединять собственные конструкторы (например, !include
) в загрузчик YAML.Я включил корневой каталог, который можно настроить так, чтобы это решение поддерживало относительные и абсолютные ссылки на файлы.
Классовое решение
Вот решение на основе классов, которое позволяет избежать глобальной корневой переменной моего исходного ответа.
Видеть это суть для аналогичного, более надежного решения Python 3, которое использует метакласс для регистрации пользовательского конструктора.
import yaml
import os
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 }
Насколько мне известно, включения не поддерживаются напрямую в 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)))
Единственным недостатком является то, что переменные в включении всегда будут переопределять переменные в основном, и нет способа изменить этот приоритет, изменив место расположения «includes:оператор появляется в файле main.yml.
С другой стороны, YAML не поддерживает включения, поскольку на самом деле он не предназначен исключительно для разметки на основе файлов.Что будет означать включение, если вы получите его в ответ на запрос AJAX?
Продолжая ответ @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
Пользователи Python могут попробовать pyyaml-включить.
Установить
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)
Считайте, что у нас есть такие ЯМЛ файлы:
├── 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
ЯМЛ тег этоtrue
, шаблон“**”
будет соответствовать любым файлам и нулю или более каталогам и подкаталогам.- Используя
“**”
шаблон в больших деревьях каталогов может занимать слишком много времени из-за рекурсивного поиска.
Чтобы включить recursive
аргумент, мы напишем !include
отмечать в Mapping
или Sequence
режим:
- Аргументы в
Sequence
режим:
!include [tests/data/include.d/**/*.yaml, true]
- Аргументы в
Mapping
режим:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
К сожалению, YAML не предусматривает этого в своем стандарте.
Но если вы используете Ruby, есть драгоценный камень, обеспечивающий запрашиваемую вами функциональность путем расширения библиотеки Ruby YAML:https://github.com/entwanderer/yaml_extend
Я думаю, что решение, использованное @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, поскольку входной поток может не быть файлом, но это очень полезно!
Возможно, это могло бы вас вдохновить, попробуйте следовать соглашениям jbb:
https://docs.openstack.org/infra/jenkins-job-builder/definition.html#inclusion-tags
- job:
name: test-job-include-raw-1
builders:
- shell:
!include-raw: include-raw001-hello-world.sh
Стандартный 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 косвенно позволит вам вкладывать файлы 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"
Вероятно, он не поддерживался, когда был задан вопрос, но вы можете импортировать другой файл YAML в один:
imports: [/your_location_to_yaml_file/Util.area.yaml]
Хотя у меня нет онлайн-ссылок, но это работает для меня.