Как ссылаться на "настройки" YAML из другого места в том же файле YAML?
У меня есть следующий YAML:
paths:
patha: /path/to/root/a
pathb: /path/to/root/b
pathc: /path/to/root/c
Как я могу "нормализовать" это, удалив /path/to/root/
из трех путей, и иметь его в качестве своего собственного параметра, что-то вроде:
paths:
root: /path/to/root/
patha: *root* + a
pathb: *root* + b
pathc: *root* + c
Очевидно, что это неверно, я просто выдумал. Какой реальный синтаксис? Это можно сделать?
10 ответов
Я не думаю, что это возможно. Вы можете повторно использовать "узел", но не его часть.
bill-to: &id001
given : Chris
family : Dumars
ship-to: *id001
Это совершенно корректно YAML и поля given
а также family
повторно используются в ship-to
блок. Вы можете повторно использовать скалярный узел таким же образом, но вы не сможете изменить то, что находится внутри, и добавить к нему эту последнюю часть пути изнутри YAML.
Если повторение беспокоит вас так много, я предлагаю, чтобы ваше приложение знало root
свойство и добавить его к каждому пути, который выглядит относительным, а не абсолютным.
Да, используя пользовательские теги. Пример в Python, делая !join
помечать строки объединения в массиве:
import yaml
## define custom tag handler
def join(loader, node):
seq = loader.construct_sequence(node)
return ''.join([str(i) for i in seq])
## register the tag handler
yaml.add_constructor('!join', join)
## using your sample data
yaml.load("""
paths:
root: &BASE /path/to/root/
patha: !join [*BASE, a]
pathb: !join [*BASE, b]
pathc: !join [*BASE, c]
""")
Что приводит к:
{
'paths': {
'patha': '/path/to/root/a',
'pathb': '/path/to/root/b',
'pathc': '/path/to/root/c',
'root': '/path/to/root/'
}
}
Массив аргументов для !join
может иметь любое количество элементов любого типа данных, если они могут быть преобразованы в строку, поэтому !join [*a, "/", *b, "/", *c]
делает то, что вы ожидаете.
Другой способ взглянуть на это - просто использовать другое поле.
paths:
root_path: &root
val: /path/to/root/
patha: &a
root_path: *root
rel_path: a
pathb: &b
root_path: *root
rel_path: b
pathc: &c
root_path: *root
rel_path: c
Я создал библиотеку, доступную на Packagist, которая выполняет эту функцию: https://packagist.org/packages/grasmash/yaml-expander
Пример файла YAML:
type: book
book:
title: Dune
author: Frank Herbert
copyright: ${book.author} 1965
protaganist: ${characters.0.name}
media:
- hardcover
characters:
- name: Paul Atreides
occupation: Kwisatz Haderach
aliases:
- Usul
- Muad'Dib
- The Preacher
- name: Duncan Idaho
occupation: Swordmaster
summary: ${book.title} by ${book.author}
product-name: ${${type}.title}
Пример логики:
// Parse a yaml string directly, expanding internal property references.
$yaml_string = file_get_contents("dune.yml");
$expanded = \Grasmash\YamlExpander\Expander::parse($yaml_string);
print_r($expanded);
Результирующий массив:
array (
'type' => 'book',
'book' =>
array (
'title' => 'Dune',
'author' => 'Frank Herbert',
'copyright' => 'Frank Herbert 1965',
'protaganist' => 'Paul Atreides',
'media' =>
array (
0 => 'hardcover',
),
),
'characters' =>
array (
0 =>
array (
'name' => 'Paul Atreides',
'occupation' => 'Kwisatz Haderach',
'aliases' =>
array (
0 => 'Usul',
1 => 'Muad\'Dib',
2 => 'The Preacher',
),
),
1 =>
array (
'name' => 'Duncan Idaho',
'occupation' => 'Swordmaster',
),
),
'summary' => 'Dune by Frank Herbert',
);
Определение YML:
dir:
default: /home/data/in/
proj1: ${dir.default}p1
proj2: ${dir.default}p2
proj3: ${dir.default}p3
Где-то в тимелист
<p th:utext='${@environment.getProperty("dir.default")}' />
<p th:utext='${@environment.getProperty("dir.proj1")}' />
Вывод:/ home / data / in / / home / data / in / p1
В некоторых языках вы можете использовать альтернативную библиотеку. Например, тампакс является реализацией переменных обработки YAML:
const tampax = require('tampax');
const yamlString = `
dude:
name: Arthur
weapon:
favorite: Excalibur
useless: knife
sentence: "{{dude.name}} use {{weapon.favorite}}. The goal is {{goal}}."`;
const r = tampax.yamlParseString(yamlString, { goal: 'to kill Mordred' });
console.log(r.sentence);
// output : "Arthur use Excalibur. The goal is to kill Mordred."
Использование ОмегаКонф
OmegaConf — это система иерархической конфигурации на основе YAML, которая поддерживает это в рамках функциональности Variable interpolation . Использование OmegaConf v2.2.2:
Создайте YAML-файлpaths.yaml
следующее:
paths:
root: /path/to/root/
patha: ${.root}a
pathb: ${.root}b
pathc: ${.root}c
то мы можем прочитать файл с переменными путями:
from omegaconf import OmegaConf
conf = OmegaConf.load("test_paths.yaml")
>>> conf.paths.root
'/path/to/root/'
>>> conf.paths.patha
'/path/to/root/a'
>>> conf.paths.pathb
'/path/to/root/b'
>>> conf.paths.pathc
'/path/to/root/c'
Глубокая и перекрестная ссылка
Можно определить более сложные (вложенные) структуры с относительной глубиной вашей переменной по отношению к другим переменным:
Создать другой файлnested_paths.yaml
:
data:
base: data
sub_dir_A:
name: a
# here we note that `base` is two levels above this variable
# hence we will use `..base` two dots but the `name` variable is
# at the same level hence a single dot `.name`
nested_dir: ${..base}/sub_dir/${.name}/last_dir
sub_dir_B:
# add another level of depth
- name: b
# due to another level of depth, we have to use three dots
# to access `base` variable as `...base`
nested_file: ${...base}/sub_dir/${.name}/dirs.txt
- name: c
# we can also make cross-references to other variables
cross_ref_dir: ${...sub_dir_A.nested_dir}/${.name}
снова можем проверить:
conf = OmegaConf.load("nested_paths.yaml")
# 1-level of depth reference
>>> conf.data.sub_dir_A.nested_dir
'data/sub_dir/a/last_dir'
# 2-levels of depth reference
>>> conf.data.sub_dir_B[0].nested_file
'data/sub_dir/b/dirs.txt'
# cross-reference example
>>> conf.data.sub_dir_B[1].cross_ref_dir
'data/sub_dir/a/last_dir/c'
В случае некорректных ссылок (таких как неправильная глубина, неправильное имя переменной) OmegaConf выдаст ошибкуomegaconf.errors.InterpolationResolutionError
. Он также используется в Hydra для настройки сложных приложений.
With Yglu, you can write your example as:
paths:
root: /path/to/root/
patha: !? .paths.root + a
pathb: !? .paths.root + b
pathc: !? .paths.root + c
Отказ от ответственности: я являюсь автором Yglu.
Ваш пример недопустим только потому, что вы выбрали зарезервированный символ для начала своих скаляров. Если вы замените *
с некоторыми другими незарезервированными символами (я склонен использовать для этого не-ASCII-символы, поскольку они редко используются как часть какой-либо спецификации), в результате вы получите совершенно допустимый YAML:
paths:
root: /path/to/root/
patha: ♦root♦ + a
pathb: ♦root♦ + b
pathc: ♦root♦ + c
Это загрузит стандартное представление для отображений на языке, который использует ваш синтаксический анализатор, и волшебным образом ничего не расширит.
Для этого используйте локальный тип объекта по умолчанию, как в следующей программе Python:
# coding: utf-8
from __future__ import print_function
import ruamel.yaml as yaml
class Paths:
def __init__(self):
self.d = {}
def __repr__(self):
return repr(self.d).replace('ordereddict', 'Paths')
@staticmethod
def __yaml_in__(loader, data):
result = Paths()
loader.construct_mapping(data, result.d)
return result
@staticmethod
def __yaml_out__(dumper, self):
return dumper.represent_mapping('!Paths', self.d)
def __getitem__(self, key):
res = self.d[key]
return self.expand(res)
def expand(self, res):
try:
before, rest = res.split(u'♦', 1)
kw, rest = rest.split(u'♦ +', 1)
rest = rest.lstrip() # strip any spaces after "+"
# the lookup will throw the correct keyerror if kw is not found
# recursive call expand() on the tail if there are multiple
# parts to replace
return before + self.d[kw] + self.expand(rest)
except ValueError:
return res
yaml_str = """\
paths: !Paths
root: /path/to/root/
patha: ♦root♦ + a
pathb: ♦root♦ + b
pathc: ♦root♦ + c
"""
loader = yaml.RoundTripLoader
loader.add_constructor('!Paths', Paths.__yaml_in__)
paths = yaml.load(yaml_str, Loader=yaml.RoundTripLoader)['paths']
for k in ['root', 'pathc']:
print(u'{} -> {}'.format(k, paths[k]))
который напечатает:
root -> /path/to/root/
pathc -> /path/to/root/c
Расширение выполняется на лету и обрабатывает вложенные определения, но вы должны быть осторожны, чтобы не вызывать бесконечную рекурсию.
Указав дампер, вы можете сбросить исходный YAML из загруженных данных из-за расширения на лету:
dumper = yaml.RoundTripDumper
dumper.add_representer(Paths, Paths.__yaml_out__)
print(yaml.dump(paths, Dumper=dumper, allow_unicode=True))
это изменит порядок отображения ключей сопоставления. Если это проблема, вы должны сделать self.d
CommentedMap
(импортировано из ruamel.yaml.comments.py
)
Я написал свою собственную библиотеку на Python для расширения переменных, загружаемых из каталогов с такой иерархией, как:
/root
|
+- /proj1
|
+- config.yaml
|
+- /proj2
|
+- config.yaml
|
... and so on ...
Ключевое отличие здесь в том, что расширение необходимо применять только после всех config.yaml
files загружается, где переменные из следующего файла могут переопределять переменные из предыдущего, поэтому псевдокод должен выглядеть так:
env = YamlEnv()
env.load('/root/proj1/config.yaml')
env.load('/root/proj1/proj2/config.yaml')
...
env.expand()
В качестве дополнительной опции xonsh
скрипт может экспортировать полученные переменные в переменные среды (см. yaml_update_global_vars
функция).
Скрипты:
https://sourceforge.net/p/contools/contools/HEAD/tree/trunk/Scripts/Tools/cmdoplib.yaml.pyhttps://sourceforge.net/p/contools/contools/HEAD/tree/trunk/Scripts/Tools/cmdoplib.yaml.xsh
Плюсы:
- простой, не поддерживает рекурсию и вложенные переменные
- может заменить неопределенную переменную на заполнитель (
${MYUNDEFINEDVAR}
->*$/{MYUNDEFINEDVAR}
) - может развернуть ссылку из переменной среды (
${env:MYVAR}
) - может заменить все
\\
к/
в переменной пути (${env:MYVAR:path}
)
Минусы:
- не поддерживает вложенные переменные, поэтому не может расширять значения во вложенных словарях (что-то вроде
${MYSCOPE.MYVAR}
не реализовано) - не обнаруживает рекурсию расширения, включая рекурсию после того, как заполнитель поместил