Ответ 1
Нет, YAML не содержит никаких выражений "import" или "include".
Итак, у меня есть два файла YAML: "A" и "B", и я хочу, чтобы содержимое A было вставлено внутри B, либо сплайсировано в существующую структуру данных, как массив, либо как дочерний элемент элемента, как значение для определенного хеш-ключа.
Возможно ли это вообще? Как? Если нет, любые указатели на нормативную ссылку?
Нет, YAML не содержит никаких выражений "import" или "include".
Ваш вопрос не требует решения на Python, но вот тот, который использует PyYAML.
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]]}
Если вы используете Symfony версию YAML, это возможно, например:
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)))
Единственным недостатком является то, что переменные во включениях всегда будут переопределять переменные в main, и нет никакого способа изменить этот приоритет, изменив место, где в файле main.yml появляется оператор "includes:".
С другой стороны, YAML не поддерживает include, так как он на самом деле не разработан так же эксклюзивно, как разметка на основе файлов. Что будет означать включение, если вы получите его в ответ на запрос 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 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-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"
ℹ Note:
Имя файла может быть абсолютным (например,
/usr/conf/1.5/Make.yml
) или относительным (например,../../cfg/img.yml
).
Имя файла может содержать подстановочные знаки в стиле оболочки. Данные, загруженные из файла (-ов), найденных с помощью подстановочных знаков, будут заданы в последовательности.
Если 0.yaml
было:
files: !include include.d/*.yaml
Мы получим:
files:
- name: "1"
- name: "2"
ℹ Note:
- Для
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}
К сожалению, YAML не предоставляет это в своем стандарте.
Но если вы используете Ruby, есть камень, обеспечивающий требуемую функциональность, расширяя библиотеку YAML ruby: 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
. Файлы загружаются из classpath (каталог ресурсов 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));
}
}
}
}
}
С 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"
Вероятно, он не поддерживался, когда задавался вопрос, но вы можете импортировать другой файл YAML в один:
imports: [/your_location_to_yaml_file/Util.area.yaml]
Хотя у меня нет онлайн-ссылки, но это работает для меня.
Здесь я использовал mako
это верхняя часть файла a.
<% include file = 'b.txt'/" >
это нижняя часть файла
это файл b
from mako.template import Template
from mako.lookup import TemplateLookup
import os
directory = os.path.dirname( os.path.abspath( __file__ ) )
mylookup = TemplateLookup(directories=[directory])
mytemplate = Template(filename="a.txt", lookup=mylookup)
finalsrc = mytemplate.render()
# finalsrc can be treated as yaml or whatever you like
$ python test.py
это верхняя часть файла
это файл b
это нижняя часть файла