Como posso incluir um arquivo YAML dentro de outro?

288

Então, eu tenho dois arquivos YAML, "A" e "B" e quero que o conteúdo de A seja inserido dentro de B, emendado na estrutura de dados existente, como uma matriz ou como filho de um elemento, como o valor para uma determinada chave de hash.

Isso é possível em tudo? Quão? Caso contrário, algum ponteiro para uma referência normativa?

kch
fonte
1
Recentemente, encontrei o HiYaPyCo para Python que faz exatamente isso. Você pode mesclar arquivos YAML diferentes. É um módulo Python muito bom que vale a pena conhecer.
nowox 23/11/2015

Respostas:

326

Não, o YAML não inclui nenhum tipo de declaração "import" ou "include".

jameshfisher
fonte
8
Você pode criar um manipulador! Include <filename>.
clarkevans
5
@clarkevans, mas essa construção seria "fora" da linguagem YAML.
Jameshfisher
2
Isso agora é possível. Adicionei uma resposta abaixo ... espero que ajude.
daveaspinall
1
Se você estiver usando Rails, você pode inserir <% = 'fdsa fdsa' %> ERB sintaxe e ele vai funcionar
gleenn
9
Penso que esta resposta deve ser reformulada como "Não, o YAML padrão não inclui essa função. No entanto, muitas implementações fornecem alguma extensão para isso".
Franklin Yu
112

Sua pergunta não pede uma solução Python, mas aqui está uma usando PyYAML .

O PyYAML permite anexar construtores personalizados (como !include) ao carregador YAML. Incluí um diretório raiz que pode ser definido para que esta solução suporte referências de arquivo relativas e absolutas.

Solução baseada em classe

Aqui está uma solução baseada em classe, que evita a variável raiz global da minha resposta original.

Veja esta essência para uma mais robusta solução semelhante, Python 3 que usa um metaclass para registrar o construtor personalizado.

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)

Um exemplo:

foo.yaml

a: 1
b:
    - 1.43
    - 543.55
c: !include bar.yaml

bar.yaml

- 3.6
- [1, 2, 3]

Agora os arquivos podem ser carregados usando:

>>> 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
fonte
Este é um recurso interessante, thanx. Mas qual é o propósito de todas essas manipulações com root / old_root? Suponho que o código da includefunção possa ser simplificado: `def include (loader, node):" "" Inclua outro arquivo YAML. "" "Filename = loader.construct_scalar (node) data = yaml.load (open (filename))`
Aliaksei Ramanau
A raiz global existe para que relativo inclua trabalho em qualquer profundidade, por exemplo, quando arquivos incluídos em um diretório diferente incluem um arquivo relativo a esse diretório. Inclusões absolutas também devem funcionar. Provavelmente, existe uma maneira mais limpa de fazer isso sem uma variável global, talvez usando uma classe yaml.Loader personalizada.
precisa saber é o seguinte
2
Também é possível ter algo parecido com isto: foo.yaml: a: bla bar.yaml: `! Inclua foo.yaml b: blubb` Para que o resultado seja:` {'a': bla, 'b': blubb}
Martin
3
Essa deve ser a resposta aceita. Além disso, um ponto de segurança, você deve usar o yaml.safeload em vez do yaml.load, para evitar que o yaml especialmente criado seja o proprietário do seu serviço.
Danielpops
1
@JoshBode isso deve funcionar para você: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops
32

Se você estiver usando a versão do YAML do Symfony , isso é possível, assim:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }
daveaspinall
fonte
34
Isso é específico de como o Symfony interpreta o YAML, em vez de fazer parte do próprio YAML.
Jameshfisher
9
Sim, foi por isso que postei o link nos documentos do Symfony. A pergunta é "Isso é possível? Como?" ... é assim. Não há motivo para um voto negativo.
daveaspinall
4
Eu não te votei; Estou apenas apontando que isso é específico para o Symfony YAML.
Jameshfisher 19/08/2015
9
Não existe uma "versão Symfony do YAML" ... essa é simplesmente uma biblioteca compatível com o YAML específica do fornecedor que possui itens extras que não fazem parte do YAML.
dreftymac
3
Não há razão para reduzir a votação desta resposta se a resposta "baseada em classe" for votada.
21817 Mikhail
13

As inclusões não são diretamente suportadas no YAML, tanto quanto eu sei, você precisará fornecer um mecanismo, no entanto, isso geralmente é fácil de fazer.

Eu usei o YAML como uma linguagem de configuração nos meus aplicativos python e, nesse caso, frequentemente defina uma convenção como esta:

>>> main.yml <<<
includes: [ wibble.yml, wobble.yml]

Então, no meu código (python), eu faço:

import yaml
cfg = yaml.load(open("main.yml"))
for inc in cfg.get("includes", []):
   cfg.update(yaml.load(open(inc)))

A única desvantagem é que as variáveis ​​no include sempre substituem as variáveis ​​no main e não há como alterar essa precedência alterando onde a instrução "includes: aparece no arquivo main.yml.

Em um ponto um pouco diferente, o YAML não suporta inclusões, pois não foram realmente projetadas tão exclusivamente quanto uma marcação baseada em arquivo. O que um include incluiria se você o obtivesse em resposta a uma solicitação AJAX?

clh
fonte
3
isso funciona apenas quando o arquivo yaml não contém configuração aninhada.
Freedom
10

Para usuários do Python, você pode tentar pyyaml-include .

Instalar

pip install pyyaml-include

Uso

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)

Considere que temos esses arquivos YAML :

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml
  • 1.yaml conteúdo:
name: "1"
  • 2.yaml conteúdo:
name: "2"

Incluir arquivos por nome

  • No nível superior:

    Se 0.yamlfoi:

!include include.d/1.yaml

Nós conseguiremos:

{"name": "1"}
  • No mapeamento:

    Se 0.yamlfoi:

file1: !include include.d/1.yaml
file2: !include include.d/2.yaml

Nós conseguiremos:

  file1:
    name: "1"
  file2:
    name: "2"
  • Em sequência:

    Se 0.yamlfoi:

files:
  - !include include.d/1.yaml
  - !include include.d/2.yaml

Nós conseguiremos:

files:
  - name: "1"
  - name: "2"

Nota :

O nome do arquivo pode ser absoluto (como /usr/conf/1.5/Make.yml) ou relativo (como ../../cfg/img.yml).

Incluir arquivos por curingas

O nome do arquivo pode conter curingas no estilo de shell. Os dados carregados dos arquivos encontrados pelos curingas serão definidos em uma sequência.

Se 0.yamlfoi:

files: !include include.d/*.yaml

Nós conseguiremos:

files:
  - name: "1"
  - name: "2"

Nota :

  • Pois Python>=3.5, se o recursiveargumento da tag !include YAML for true, o padrão “**”corresponderá a qualquer arquivo e zero ou mais diretórios e subdiretórios.
  • O uso do “**”padrão em grandes árvores de diretório pode consumir uma quantidade excessiva de tempo devido à pesquisa recursiva.

Para habilitar o recursiveargumento, devemos escrever a !includetag no modo Mappingou Sequence:

  • Argumentos no Sequencemodo:
!include [tests/data/include.d/**/*.yaml, true]
  • Argumentos no Mappingmodo:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
xqliang
fonte
Na verdade, isso não responde à pergunta. Pertence a uma solução Python, não a uma que usa o formato YAML padronizado.
precisa
@oligofren Manipuladores de tags personalizados são um recurso do YAML, permitindo que os analisadores estendam o YAML para especificar tipos e implementar comportamentos personalizados como esses. Seria uma longa extensão para a especificação YAML-se a ir tão longe quanto a prescrever como a inclusão arquivo deve trabalhar com todas as especificações diferentes caminhos OS, sistemas de arquivos, etc.
Anton Strogonoff
@AntonStrogonoff Obrigado por trazer isso à minha atenção. Você poderia me indicar um lugar na RFC? Não menciona a palavra "costume". Ref yaml.org/spec/1.2/spec.html
oligofren
1
@oligofren De nada. Procure por tags "específicas do aplicativo" .
Anton Strogonoff
8

Expandindo a resposta de @ Josh_Bode, aqui está minha própria solução PyYAML, que tem a vantagem de ser uma subclasse independente de yaml.Loader. Não depende de nenhum global no nível do yamlmódulo ou da modificação do estado global do módulo.

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
fonte
2
Finalmente, comecei a adicionar a abordagem baseada em classe à minha resposta, mas você me venceu :) Nota: Se você usar yaml.load(f, IncludeLoader)dentro, _includepoderá evitar a substituição da raiz. Além disso, a menos que você faça isso, a solução não funcionará mais do que um nível de profundidade, pois os dados incluídos usam a yaml.Loaderclasse regular .
Josh Bode
Eu tinha que remover a palavra-chave rootde kwargsdepois de definir self.roota fazê-lo funcionar com cordas. Movi o bloco if-else acima da superchamada. Talvez alguém possa confirmar minha descoberta ou me mostrar como usar a classe com strings e o rootparâmetro
Woltan 15/08
1
Infelizmente, isso não funciona com referências como `` included: & INCLUDED! Include inner.yaml merge: <<: * INCLUDED ``
antony
2

Eu faço alguns exemplos para sua referência.

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()

resultado

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

Atualização 2

e você pode combiná-lo assim

# 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
fonte
1

Infelizmente, o YAML não fornece isso em seu padrão.

Mas se você estiver usando Ruby, há uma gema fornecendo a funcionalidade que você está solicitando, estendendo a biblioteca YAML do ruby: https://github.com/entwanderer/yaml_extend

user8419486
fonte
1

Acho que a solução usada pelo @ maxy-B parece ótima. No entanto, não foi bem-sucedido para mim com inclusões aninhadas. Por exemplo, se config_1.yaml incluir config_2.yaml, que inclui config_3.yaml, houve um problema com o carregador. No entanto, se você simplesmente apontar a nova classe loader para si mesma no carregamento, ela funcionará! Especificamente, se substituirmos a função _include antiga pela versão ligeiramente modificada:

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

Após refletir, concordo com os outros comentários, que o carregamento aninhado não é apropriado para o yaml em geral, pois o fluxo de entrada pode não ser um arquivo, mas é muito útil!

PaddyM
fonte
1

O padrão YML não especifica uma maneira de fazer isso. E esse problema não se limita ao YML. JSON tem as mesmas limitações.

Muitos aplicativos que usam configurações baseadas em YML ou JSON acabam enfrentando esse problema. E quando isso acontece, eles formam sua própria convenção .

por exemplo, para definições de API do swagger:

$ref: 'file.yml'

por exemplo, para configurações de composição do docker:

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

Como alternativa, se você deseja dividir o conteúdo de um arquivo yml em vários arquivos, como uma árvore de conteúdo, pode definir sua própria convenção de estrutura de pastas e usar um script de mesclagem (existente).

bvdb
fonte
0

O YAML 1.2 padrão não inclui nativamente esse recurso. No entanto, muitas implementações fornecem alguma extensão para isso.

Apresento uma maneira de alcançá-lo com Java e snakeyaml:1.24(biblioteca Java para analisar / emitir arquivos YAML) que permite criar uma tag YAML personalizada para atingir o objetivo a seguir (você verá que eu estou usando-a para carregar conjuntos de testes definidos em vários arquivos YAML e que eu fiz funcionar como uma lista de inclusões para um test:nó de destino ):

# ... yaml prev stuff

tests: !include
  - '1.hello-test-suite.yaml'
  - '3.foo-test-suite.yaml'
  - '2.bar-test-suite.yaml'

# ... more yaml document

Aqui está o Java de uma classe que permite processar a !includetag. Os arquivos são carregados do caminho de classe (diretório de recursos do 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
fonte
0

Com o Yglu , você pode importar outros arquivos como este:

A.yaml

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

B.yaml

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

Como $importé uma função, você também pode passar uma expressão como argumento:

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

Isso daria a mesma saída que acima.

Disclaimer: Eu sou o autor de Yglu.

lbovet
fonte
-1

Com o Symfony , sua manipulação do yaml indiretamente permitirá aninhar arquivos yaml. O truque é fazer uso da parametersopção. por exemplo:

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

O resultado será o mesmo que:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
jxmallett
fonte
-6

Provavelmente não foi suportado quando a pergunta foi feita, mas você pode importar outro arquivo YAML para um:

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

Embora eu não tenha nenhuma referência on-line, mas isso funciona para mim.

Sankalp
fonte
4
Isso não faz nenhuma inclusão. Ele cria um mapeamento com uma sequência que consiste em uma única sequência "/seu_localização_para_yaml_file/Util.area.yaml", como valor para a chave imports.
Anthon