J'ai donc deux fichiers YAML, "A" et "B" et je veux que le contenu de A soit inséré dans B, soit épissé dans la structure de données existante, comme un tableau, ou en tant qu'enfant d'un élément, comme la valeur pour une certaine clé de hachage.
Est-ce possible? Comment? Si ce n’est pas le cas, des indications sur une référence normative?
Non, YAML n'inclut aucun type d'instruction "import" ou "include".
Votre question ne demande pas de solution Python, mais en voici une qui utilise PyYAML .
PyYAML vous permet d’attacher des constructeurs personnalisés (tels que !include
) au chargeur YAML. J'ai inclus un répertoire racine pouvant être défini pour que cette solution prenne en charge les références de fichier relatives et absolues.
Voici une solution basée sur les classes, qui évite la variable racine globale de ma réponse d'origine.
Voir ceci Gist pour une solution Python 3 plus robuste, similaire, qui utilise une métaclasse pour 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 en utilisant:
>>> 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 Symfony de YAML , c'est possible, comme ceci:
imports:
- { resource: sub-directory/file.yml }
- { resource: sub-directory/another-file.yml }
Les actions incluses ne sont pas directement prises en charge dans YAML, autant que je sache, vous devrez fournir un mécanisme vous-même, toutefois, cela est généralement facile à faire.
J'ai utilisé YAML comme langage de configuration dans mes applications python et, dans ce cas, je définis souvent une convention comme celle-ci:
>>> main.yml <<<
includes: [ wibble.yml, wobble.yml]
Puis 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 incluses incluent toujours les variables principales, et il est impossible de modifier cette priorité en modifiant l'emplacement où l'instruction "includes: apparaît dans le fichier main.yml.
Sur un point légèrement différent, YAML ne prend pas en charge les inclus, car il n’est pas vraiment conçu aussi exclusivement comme une balise basée sur un fichier. Que signifie une inclusion si vous l'obtenez dans une réponse à une demande AJAX?
Pour en savoir plus sur la réponse de @ Josh_Bode, voici ma propre solution PyYAML, qui présente l’avantage d’être une sous-classe autonome de yaml.Loader
. Cela ne dépend pas des globales au niveau du module, ni de 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
Je pense que la solution utilisée par @ maxy-B est superbe. Cependant, cela n'a pas réussi pour moi avec des inclusions imbriquées. Par exemple, si config_1.yaml inclut config_2.yaml, ce qui inclut config_3.yaml, il y a eu un problème avec le chargeur. Cependant, si vous pointez simplement la nouvelle classe de chargeur sur elle-même, cela fonctionne! Plus précisément, si nous remplaçons l'ancienne fonction _include par 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
Après réflexion, je suis d’accord avec les autres commentaires, selon lequel le chargement imbriqué n’est pas approprié pour yaml en général, car le flux d’entrée n’est peut-être pas un fichier, mais il est très utile!
Pour les utilisateurs de Python, vous pouvez essayer 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)
Considérons que nous avons de tels YAML fichiers:
├── 0.yaml
└── include.d
├── 1.yaml
└── 2.yaml
1.yaml
:name: "1"
2.yaml
:name: "2"
Au plus haut niveau:
Si 0.yaml
était:
!include include.d/1.yaml
Nous aurons:
{"name": "1"}
En cartographie:
Si 0.yaml
était:
file1: !include include.d/1.yaml
file2: !include include.d/2.yaml
Nous aurons:
file1:
name: "1"
file2:
name: "2"
En séquence:
Si 0.yaml
était:
files:
- !include include.d/1.yaml
- !include include.d/2.yaml
Nous aurons:
files:
- name: "1"
- name: "2"
ℹ Note :
Le nom de fichier peut être absolu (comme
/usr/conf/1.5/Make.yml
) ou relatif (comme../../cfg/img.yml
).
Le nom de fichier peut contenir des caractères génériques de style shell. Les données chargées à partir des fichiers trouvés par des caractères génériques seront définies dans une séquence.
Si 0.yaml
était:
files: !include include.d/*.yaml
Nous aurons:
files:
- name: "1"
- name: "2"
ℹ Note :
- Pour
Python>=3.5
, si l'argumentrecursive
de!include
YAML tag esttrue
, le modèle“**”
correspond à tous les fichiers et à aucun ou plusieurs répertoires et sous-répertoires.- L'utilisation du modèle
“**”
dans des arborescences de répertoires volumineuses peut prendre beaucoup de temps en raison d'une recherche récursive.
Afin d'activer l'argument recursive
, nous écrirons la balise !include
en mode Mapping
ou Sequence
:
Sequence
:!include [tests/data/include.d/**/*.yaml, true]
Mapping
:!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
Malheureusement, YAML ne fournit pas cela dans sa norme.
Mais si vous utilisez Ruby, une gemme fournit la fonctionnalité que vous demandez en étendant la bibliothèque Ruby YAML: https://github.com/entwanderer/yaml_extend
Peut-être que cela pourrait vous inspirer, essayez de vous aligner sur 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
Avec Symfony , sa gestion de yaml vous permettra indirectement d’imbriquer des 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"
YAML 1.2 standard n'inclut pas nativement cette fonctionnalité. Néanmoins, de nombreuses implémentations fournissent une extension pour le faire.
Je présente un moyen de le réaliser avec Java et snakeyaml:1.24
(bibliothèque Java pour analyser/émettre des fichiers YAML) qui permet de créer une balise YAML personnalisée pour atteindre l'objectif suivant (vous verrez que je suis l’utiliser pour charger des suites de tests définies dans plusieurs fichiers YAML et que je l’ai fait fonctionner comme une liste d’inclusions pour un noeud cible test:
):
# ... yaml prev stuff
tests: !include
- '1.hello-test-suite.yaml'
- '3.foo-test-suite.yaml'
- '2.bar-test-suite.yaml'
# ... more yaml document
Voici la classe Java qui permet de traiter la balise !include
. Les fichiers sont chargés à partir de classpath (répertoire de 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));
}
}
}
}
}