Comment puis-je inclure un fichier YAML dans un autre?

288

J'ai donc deux fichiers YAML, "A" et "B" et je veux que le contenu de A soit inséré à l'intérieur de B, soit épissé dans la structure de données existante, comme un tableau, ou comme enfant d'un élément, comme la valeur pour une certaine clé de hachage.

Est-ce possible? Comment? Sinon, des indications sur une référence normative?

kch
la source
1
J'ai récemment rencontré HiYaPyCo pour Python qui fait exactement cela. Vous pouvez fusionner différents fichiers YAML ensemble. C'est un très bon module Python qui mérite d'être connu.
nowox

Réponses:

326

Non, YAML n'inclut aucun type de déclaration "import" ou "include".

jameshfisher
la source
8
Vous pouvez créer un gestionnaire! Include <nomfichier>.
Clarkevans
5
@clarkevans bien sûr, mais cette construction serait "en dehors" du langage YAML.
jameshfisher
2
C'est désormais possible. J'ai ajouté une réponse ci-dessous ... j'espère que cela aide.
daveaspinall
1
Si vous utilisez Rails, vous pouvez insérer <% = 'fdsa fdsa'%> la syntaxe ERB et cela fonctionnera
gleenn
9
Je pense que cette réponse devrait être reformulée comme "Non, YAML standard n'inclut pas cette fonction. Néanmoins, de nombreuses implémentations fournissent une extension pour le faire."
Franklin Yu
112

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 qui peut être défini pour que cette solution prenne en charge les références de fichiers relatives et absolues.

Solution basée sur la classe

Voici une solution basée sur les classes, qui évite la variable racine globale de ma réponse d'origine.

Voir cet essentiel pour une solution Python 3 similaire et plus robuste qui utilise une métaclasse pour enregistrer 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]]}
Josh Bode
la source
C'est une fonctionnalité intéressante, merci. Mais quel est le but de toutes ces manipulations avec root / old_root? Je suppose que le code de includefonction peut être simplifié: `def include (loader, node):" "" Inclure un autre fichier YAML. "" "Filename = loader.construct_scalar (node) data = yaml.load (open (filename))`
Aliaksei Ramanau
La racine globale est là pour que relative inclue le travail à n'importe quelle profondeur, par exemple lorsque les fichiers inclus assis dans un répertoire différent incluent un fichier relatif à ce répertoire. Les inclusions absolues devraient également fonctionner. Il existe probablement un moyen plus propre de le faire sans variable globale, peut-être en utilisant une classe yaml.Loader personnalisée.
Josh Bode
2
Est-il également possible d'avoir quelque chose comme ceci: foo.yaml: a: bla bar.yaml: `! Include foo.yaml b: blubb` Pour que le résultat soit:` {{a ': bla,' b ': blubb}
Martin
3
Ce devrait être la réponse acceptée. En outre, un choix de sécurité, vous devez utiliser yaml.safeload au lieu de yaml.load, pour éviter que yaml spécialement conçu ne soit propriétaire de votre service.
danielpops
1
@JoshBode, cela devrait fonctionner pour vous: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops
32

Si vous utilisez la version Symfony de YAML , c'est possible, comme ceci:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }
daveaspinall
la source
34
Ceci est spécifique à la façon dont Symfony interprète YAML, plutôt qu'une partie de YAML lui-même.
jameshfisher
9
Oui, c'est pourquoi j'ai publié le lien vers les documents Symfony. La question demande "Est-ce possible? Comment?" ... c'est ainsi. Ne voyez aucune raison pour un downvote.
daveaspinall
4
Je ne vous ai pas déçu; Je souligne simplement que cela est spécifique à Symfony YAML.
jameshfisher
9
Il n'y a pas de "version Symfony de YAML" ... il s'agit simplement d'une bibliothèque compatible YAML spécifique au fournisseur qui contient des éléments supplémentaires qui ne font pas partie de YAML.
dreftymac
3
Aucune raison de voter contre cette réponse si la réponse "basée sur la classe" est surévaluée.
Mikhail
13

Les inclusions ne sont pas directement prises en charge dans YAML pour autant que je sache, vous devrez fournir un mécanisme vous-même cependant, 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]

Ensuite, 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 dans les inclus prévaudront toujours sur les variables dans main, et il n'y a aucun moyen de modifier cette priorité en changeant l'endroit 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 inclusions car elles ne sont pas vraiment conçues de manière aussi exclusive qu'une annotation basée sur un fichier. Que signifierait une inclusion si vous l'avez obtenue en réponse à une demande AJAX?

clh
la source
3
cela ne fonctionne que lorsque le fichier yaml ne contient pas de configuration imbriquée.
Liberté le
10

Pour les utilisateurs de Python, vous pouvez essayer pyyaml-include .

Installer

pip install pyyaml-include

Usage

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érez que nous avons de tels fichiers YAML :

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml
  • 1.yaml contenu:
name: "1"
  • 2.yaml contenu:
name: "2"

Inclure les fichiers par nom

  • Au niveau supérieur:

    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"

Remarque :

Le nom de fichier peut être absolu (comme /usr/conf/1.5/Make.yml) ou relatif (comme ../../cfg/img.yml).

Inclure des fichiers par caractères génériques

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 les 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"

Remarque :

  • Car Python>=3.5, si l' recursiveargument de la balise !include YAML est true, le modèle “**”correspondra à tous les fichiers et à zéro ou plusieurs répertoires et sous-répertoires.
  • L'utilisation du “**”modèle dans de grandes arborescences de répertoires peut consommer un temps excessif en raison de la recherche récursive.

Afin d'activer l' recursiveargument, nous écrirons la !includebalise en mode Mappingou Sequence:

  • Arguments en Sequencemode:
!include [tests/data/include.d/**/*.yaml, true]
  • Arguments en Mappingmode:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
xqliang
la source
Cela ne répond pas vraiment à la question. Il s'agit d'une solution Python, pas une utilisant le format standardisé YAML.
oligofren
@oligofren Les gestionnaires de balises personnalisées sont une fonctionnalité de YAML, permettant aux parseurs d'étendre YAML pour spécifier des types et implémenter des comportements personnalisés comme ceux-ci. Il serait long pour la spécification YAML elle-même d'aller jusqu'à prescrire comment l'inclusion de fichiers devrait fonctionner avec toutes les spécifications de chemin d'accès au système d'exploitation, les systèmes de fichiers, etc.
Anton Strogonoff
@AntonStrogonoff Merci d'avoir porté cela à mon attention. Pourriez-vous m'indiquer un tel endroit dans le RFC? Il ne fait aucune mention du mot "coutume". Ref yaml.org/spec/1.2/spec.html
oligofren
1
@oligofren Vous êtes les bienvenus. Recherchez les balises «spécifiques à l'application» .
Anton Strogonoff
8

En développant la réponse de @ Josh_Bode, voici ma propre solution PyYAML, qui a l'avantage d'être une sous-classe autonome de yaml.Loader. Cela ne dépend d'aucuns globaux au niveau du module, ni de la modification de l'état global du yamlmodule.

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                                                      
Maxy-B
la source
2
J'ai finalement réussi à ajouter l'approche basée sur les classes à ma réponse, mais vous m'avez battu au poinçon :) Remarque: Si vous utilisez à l' yaml.load(f, IncludeLoader)intérieur, _includevous pouvez éviter d'avoir à remplacer la racine. De plus, à moins que vous ne le fassiez, la solution ne fonctionnera pas sur plus d'un niveau, car les données incluses utilisent la yaml.Loaderclasse régulière .
Josh Bode
J'ai dû supprimer le mot clé rootde kwargsafter setting self.rootpour le faire fonctionner avec des chaînes. J'ai déplacé le bloc if-else au-dessus de l' superappel. Peut-être que quelqu'un d'autre peut confirmer ma découverte ou me montrer comment utiliser la classe avec des chaînes et le rootparamètre.
Woltan
1
Malheureusement, cela ne fonctionne pas avec des références telles que `` `inclus: & inclus comprennent la fusion inner.yaml: <<: * INCLUS` ``!
antony
2

Je fais quelques exemples pour votre référence.

import yaml

main_yaml = """
Package:
 - !include _shape_yaml    
 - !include _path_yaml
"""

_shape_yaml = """
# Define
Rectangle: &id_Rectangle
    name: Rectangle
    width: &Rectangle_width 20
    height: &Rectangle_height 10
    area: !product [*Rectangle_width, *Rectangle_height]

Circle: &id_Circle
    name: Circle
    radius: &Circle_radius 5
    area: !product [*Circle_radius, *Circle_radius, pi]

# Setting
Shape:
    property: *id_Rectangle
    color: red
"""

_path_yaml = """
# Define
Root: &BASE /path/src/

Paths: 
    a: &id_path_a !join [*BASE, a]
    b: &id_path_b !join [*BASE, b]

# Setting
Path:
    input_file: *id_path_a
"""


# define custom tag handler
def yaml_import(loader, node):
    other_yaml_file = loader.construct_scalar(node)
    return yaml.load(eval(other_yaml_file), Loader=yaml.SafeLoader)


def yaml_product(loader, node):
    import math
    list_data = loader.construct_sequence(node)
    result = 1
    pi = math.pi
    for val in list_data:
        result *= eval(val) if isinstance(val, str) else val
    return result


def yaml_join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])


def yaml_ref(loader, node):
    ref = loader.construct_sequence(node)
    return ref[0]


def yaml_dict_ref(loader: yaml.loader.SafeLoader, node):
    dict_data, key, const_value = loader.construct_sequence(node)
    return dict_data[key] + str(const_value)


def main():
    # register the tag handler
    yaml.SafeLoader.add_constructor(tag='!include', constructor=yaml_import)
    yaml.SafeLoader.add_constructor(tag='!product', constructor=yaml_product)
    yaml.SafeLoader.add_constructor(tag='!join', constructor=yaml_join)
    yaml.SafeLoader.add_constructor(tag='!ref', constructor=yaml_ref)
    yaml.SafeLoader.add_constructor(tag='!dict_ref', constructor=yaml_dict_ref)

    config = yaml.load(main_yaml, Loader=yaml.SafeLoader)

    pk_shape, pk_path = config['Package']
    pk_shape, pk_path = pk_shape['Shape'], pk_path['Path']
    print(f"shape name: {pk_shape['property']['name']}")
    print(f"shape area: {pk_shape['property']['area']}")
    print(f"shape color: {pk_shape['color']}")

    print(f"input file: {pk_path['input_file']}")


if __name__ == '__main__':
    main()

production

shape name: Rectangle
shape area: 200
shape color: red
input file: /path/src/a

Update 2

et vous pouvez le combiner comme ça

# xxx.yaml
CREATE_FONT_PICTURE:
  PROJECTS:
    SUNG: &id_SUNG
      name: SUNG
      work_dir: SUNG
      output_dir: temp
      font_pixel: 24


  DEFINE: &id_define !ref [*id_SUNG]  # you can use config['CREATE_FONT_PICTURE']['DEFINE'][name, work_dir, ... font_pixel]
  AUTO_INIT:
    basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # SUNG30

# ↓ This is not correct.
# basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # It will build by Deep-level. id_define is Deep-level: 2. So you must put it after 2. otherwise, it can't refer to the correct value.
Carson
la source
1

Malheureusement, YAML ne fournit pas cela dans sa norme.

Mais si vous utilisez Ruby, il y a une gemme fournissant la fonctionnalité que vous demandez en étendant la bibliothèque YAML ruby: https://github.com/entwanderer/yaml_extend

user8419486
la source
1

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, 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, que le chargement imbriqué n'est pas approprié pour yaml en général car le flux d'entrée peut ne pas être un fichier, mais il est très utile!

PaddyM
la source
1

La norme YML ne spécifie pas de manière de procéder. Et ce problème ne se limite pas à YML. JSON a les mêmes limitations.

De nombreuses applications qui utilisent des configurations basées sur YML ou JSON rencontrent éventuellement ce problème. Et lorsque cela se produit, ils établissent leur propre convention .

par exemple pour les définitions d'API swagger:

$ref: 'file.yml'

par exemple pour les configurations de composition de docker:

services:
  app:
    extends:
      file: docker-compose.base.yml

Alternativement, si vous souhaitez diviser le contenu d'un fichier yml en plusieurs fichiers, comme une arborescence de contenu, vous pouvez définir votre propre convention de structure de dossiers et utiliser un script de fusion (existant).

bvdb
la source
0

La norme YAML 1.2 n'inclut pas nativement cette fonctionnalité. Néanmoins, de nombreuses implémentations fournissent une extension pour ce faire.

Je présente un moyen d'y parvenir 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 l'utilise 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 test:nœud 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 à une classe qui permet de traiter la !includebalise. Les fichiers sont chargés depuis 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));
                }
            }
        }

    }

}
Gerard Bosch
la source
0

Avec Yglu , vous pouvez importer d'autres fichiers comme celui-ci:

A.yaml

foo: !? $import('B.yaml')

B.yaml

bar: Hello
$ yglu A.yaml
foo:
  bar: Hello

Comme $importc'est une fonction, vous pouvez également passer une expression comme argument:

  dep: !- b
  foo: !? $import($_.dep.toUpper() + '.yaml')

Cela donnerait la même sortie que ci-dessus.

Avertissement: je suis l'auteur d'Yglu.

lbovet
la source
-1

Avec Symfony , sa gestion de yaml vous permettra indirectement d'imbriquer des fichiers yaml. L'astuce consiste à utiliser l' parametersoption. 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"
jxmallett
la source
-6

Il n'était probablement pas pris en charge lorsque la question a été posée, mais vous pouvez importer un autre fichier YAML dans un:

imports: [/your_location_to_yaml_file/Util.area.yaml]

Bien que je n'ai pas de référence en ligne, cela fonctionne pour moi.

Sankalp
la source
4
Cela ne fait rien du tout. Il crée un mappage avec une séquence constituée d'une seule chaîne "/your_location_to_yaml_file/Util.area.yaml", comme valeur pour la clé imports.
Anthon