Comment puis-je inclure un fichier YAML dans un autre?
-
22-08-2019 - |
Question
J'ai donc deux fichiers YAML, « A » et « B » et je veux le contenu de A à insérer à l'intérieur de B, soit épissés dans la structure de données existantes, comme un tableau, ou comme un enfant d'un élément, comme la valeur d'une certaine clé de hachage.
Est-ce possible? Comment? Dans le cas contraire, tous les pointeurs à une référence normative?
La solution
Non, YAML ne comprend aucune sorte de déclaration « importation » ou « inclure ».
Autres conseils
Votre question ne demande pas une solution Python, mais voici une en utilisant PyYAML .
PyYAML vous permet d'attacher des constructeurs personnalisés (tels que !include
) au chargeur YAML. J'ai inclus un répertoire racine qui peut être réglé de telle sorte que cette solution prend en charge les références de fichiers relatifs et absolus.
Solution Class-Based
Voici une solution de classe, qui évite la variable globale racine de ma réponse originale.
Voir cette essentiel pour un similaire, Python plus robuste 3 solution qui utilise un métaclasse pour vous inscrire le constructeur personnalisé.
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)
Un exemple:
foo.yaml
a: 1
b:
- 1.43
- 543.55
c: !include bar.yaml
bar.yaml
- 3.6
- [1, 2, 3]
Maintenant, les fichiers peuvent être chargés à l'aide:
>>> 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]]}
Si vous utilisez la version de YAML, cela est possible, comme Symfony ceci:
imports:
- { resource: sub-directory/file.yml }
- { resource: sub-directory/another-file.yml }
Comprend ne sont pas directement pris en charge dans YAML pour autant que je sache, vous devrez fournir un mécanisme vous cependant, ce qui est généralement facile à faire.
Je l'ai utilisé YAML comme langue de configuration dans mes applications de python, et dans ce cas définissent souvent une convention comme ceci:
>>> main.yml <<<
includes: [ wibble.yml, wobble.yml]
Alors dans mon code (python) Je fais:
import yaml
cfg = yaml.load(open("main.yml"))
for inc in cfg.get("includes", []):
cfg.update(yaml.load(open(inc)))
Le seul inconvénient est que les variables includes toujours passer outre les variables principales, et il n'y a aucun moyen de changer cette priorité en changeant où « comprend: la déclaration apparaît dans le fichier main.yml
.Sur un point légèrement différent, YAML ne prend pas en charge inclut comme pas vraiment conçu comme comme exclusivement comme une marque à base de fichiers vers le haut. Que serait un moyen inclure si vous avez obtenu une réponse à une requête AJAX?
Développant @ réponse de Josh_Bode, voici ma propre solution PyYAML, qui a l'avantage d'être une sous-classe autonome de yaml.Loader
. Il ne dépend pas de tout niveau de GLOBALS module ou sur la modification de l'état global du module 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
Pour les utilisateurs de Python, vous pouvez essayer PyYAML-include .
Installer
pip install pyyaml-include
Utilisation
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)
Tenez compte que nous avons des fichiers YAML:
├── 0.yaml
└── include.d
├── 1.yaml
└── 2.yaml
- Contenu de l »
1.yaml
:
name: "1"
- Contenu de l »
2.yaml
:
name: "2"
Inclure les fichiers par nom
-
Le haut niveau:
Si
0.yaml
était:
!include include.d/1.yaml
Nous allons obtenir:
{"name": "1"}
-
mapping:
Si
0.yaml
était:
file1: !include include.d/1.yaml
file2: !include include.d/2.yaml
Nous allons obtenir:
file1:
name: "1"
file2:
name: "2"
-
Dans la séquence:
Si
0.yaml
était:
files:
- !include include.d/1.yaml
- !include include.d/2.yaml
Nous allons obtenir:
files:
- name: "1"
- name: "2"
ℹ Remarque :
Nom du fichier peut être absolu (comme
/usr/conf/1.5/Make.yml
) ou d'un parent (comme../../cfg/img.yml
).
Inclure des fichiers par des jokers
Nom du fichier peut contenir des caractères génériques de style shell. Les données chargées à partir du fichier (s) trouvé par des jokers seront définies dans une séquence.
Si 0.yaml
était:
files: !include include.d/*.yaml
Nous allons obtenir:
files:
- name: "1"
- name: "2"
ℹ Remarque :
- Pour
Python>=3.5
, si l'argument derecursive
de!include
tag YAML esttrue
, le motif“**”
correspondra à tous les fichiers et zéro ou plusieurs répertoires et sous-répertoires.- En utilisant le modèle de
“**”
dans les grands arbres de répertoire peut consommer une quantité excessive de temps en raison de la recherche récursive.
Pour permettre argument recursive
, nous écrirons la balise !include
en mode Mapping
ou Sequence
:
- Arguments en mode
Sequence
:
!include [tests/data/include.d/**/*.yaml, true]
- Arguments en mode
Mapping
:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
Malheureusement YAML ne fournit pas dans sa norme.
Mais si vous utilisez Ruby, il y a un petit bijou offrant la fonctionnalité que vous demandez par l'extension de la bibliothèque YAML rubis: https://github.com/entwanderer/yaml_extend
Je pense que la solution utilisée par @ Maxy-B ressemble beaucoup. Cependant, il n'a pas réussi pour moi avec des inclusions imbriquées. Par exemple, si config_1.yaml comprend config_2.yaml, qui comprend config_3.yaml il y avait un problème avec le chargeur. Toutefois, si vous pointez simplement la nouvelle classe de chargement à la charge elle-même, ça marche! Plus précisément, si l'on remplace l'ancienne fonction _include avec la version très légèrement modifiée:
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
À la réflexion, je suis d'accord avec les autres commentaires, que le chargement imbriqué ne convient pas à yaml en général que le flux d'entrée ne peut pas être un fichier, mais il est très utile!
Peut-être que cela pourrait vous inspirer, essayer d'aligner les conventions de 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
Standard YAML 1.2 ne comprend pas nativement cette fonctionnalité. Néanmoins, de nombreuses implémentations fournit une extension de le faire.
Je vous présente une façon d'y parvenir avec Java et snakeyaml:1.24
(bibliothèque Java pour analyser / émettre des fichiers YAML) qui permet de créer une étiquette de YAML personnalisé pour atteindre l'objectif suivant (vous verrez que je l'utilise pour charger des suites de test définies dans plusieurs fichiers YAML et que je l'ai fait travailler comme une liste de comprend un noeud test:
cible):
# ... yaml prev stuff
tests: !include
- '1.hello-test-suite.yaml'
- '3.foo-test-suite.yaml'
- '2.bar-test-suite.yaml'
# ... more yaml document
Voici le Java d'une classe qui permet le traitement de la balise !include
. Les fichiers sont chargés à partir classpath (répertoire des ressources 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 , sa gestion de YAML indirectement vous permettra d'imbriquer les fichiers YAML. L'astuce consiste à utiliser l'option parameters
. par exemple:
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%"
Le résultat sera le même que:
whatever:
thing:
option: "value"
foo:
- "bar"
- "baz"
other_thing:
option: "value"
foo:
- "bar"
- "baz"
Probablement, il n'a pas été soutenu quand a posé la question, mais vous pouvez importer un autre fichier YAML en un seul:
imports: [/your_location_to_yaml_file/Util.area.yaml]
Bien que je n'ai aucune référence en ligne, mais cela fonctionne pour moi.