¿Cómo puedo incluir un archivo YAML dentro de otro?

288

Así que tengo dos archivos YAML, "A" y "B" y quiero que el contenido de A se inserte dentro de B, ya sea empalmado en la estructura de datos existente, como una matriz, o como un elemento secundario de un elemento, como el valor para una determinada clave hash.

¿Es esto posible en absoluto? ¿Cómo? Si no, ¿algún puntero a una referencia normativa?

kch
fuente
1
Recientemente me topé con HiYaPyCo para Python que hace exactamente esto. Puede fusionar diferentes archivos YAML juntos. Es un módulo Python muy bueno que vale la pena conocer.
nowox

Respuestas:

326

No, YAML no incluye ningún tipo de declaración de "importación" o "inclusión".

jameshfisher
fuente
8
Puede crear un controlador! Include <filename>.
clarkevans
55
@clarkevans seguro, pero esa construcción estaría "fuera" del lenguaje YAML.
jameshfisher
2
Esto ahora es posible. He agregado una respuesta a continuación ... espero que ayude.
daveaspinall
1
Si está utilizando Rails, puede insertar la sintaxis ERB <% = 'fdsa fdsa'%> y funcionará
gleenn
9
Creo que esta respuesta debería reformularse como "No, el YAML estándar no incluye esta función. Sin embargo, muchas implementaciones proporcionan alguna extensión para hacerlo".
Franklin Yu
112

Su pregunta no solicita una solución de Python, pero aquí hay una que usa PyYAML .

PyYAML le permite adjuntar constructores personalizados (como !include) al cargador YAML. He incluido un directorio raíz que se puede configurar para que esta solución admita referencias de archivo relativas y absolutas.

Solución basada en clase

Aquí hay una solución basada en clases, que evita la variable raíz global de mi respuesta original.

Vea este resumen para una solución Python 3 similar y más robusta que utiliza una metaclase para registrar el constructor 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)

Un ejemplo:

foo.yaml

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

bar.yaml

- 3.6
- [1, 2, 3]

Ahora los archivos se pueden cargar 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
fuente
Esta es una característica interesante, gracias. Pero, ¿cuál es el propósito de todas estas manipulaciones con root / old_root? Supongo que el código de includefunción puede simplificarse: `def include (loader, node):" "" Incluir otro archivo YAML. "" "Filename = loader.construct_scalar (node) data = yaml.load (open (filename))`
Aliaksei Ramanau
La raíz global está ahí, de modo que relativo incluye el trabajo a cualquier profundidad, por ejemplo, cuando los archivos incluidos que se encuentran en un directorio diferente incluyen un archivo relativo a ese directorio. Absolute incluye también debería funcionar. Probablemente haya una forma más limpia de hacer esto sin una variable global, tal vez usando una clase personalizada yaml.Loader.
Josh Bode
2
¿También es posible tener algo como esto: foo.yaml: a: bla bar.yaml: `! Include foo.yaml b: blubb` Para que el resultado sea:` {'a': bla, 'b': blubb}
Martin
3
Esta debería ser la respuesta aceptada. Además, un truco de seguridad, debe usar yaml.safeload en lugar de yaml.load, para evitar que yaml especialmente diseñado sea el propietario de su servicio.
danielpops
1
@JoshBode esto debería funcionar para usted: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops
32

Si está utilizando la versión de Symfony de YAML , esto es posible, así:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }
daveaspinall
fuente
34
Esto es específico de cómo Symfony interpreta YAML, en lugar de ser parte de YAML.
jameshfisher
9
Sí, por eso publiqué el enlace a los documentos de Symfony. La pregunta es "¿Es esto posible? ¿Cómo?" ... así es como. No veo ninguna razón para un voto negativo.
daveaspinall
44
No te menosprecié; Solo estoy señalando que esto es específico de Symfony YAML.
jameshfisher
9
No existe una "versión Symfony de YAML" ... esta es simplemente una biblioteca compatible con YAML específica del proveedor que tiene cosas adicionales que no forman parte de YAML.
dreftymac
3
No hay razón para rechazar esta respuesta si la respuesta "basada en la clase" es votada.
Mikhail
13

Por lo que yo sé, las inclusiones no son directamente compatibles con YAML, tendrá que proporcionar un mecanismo usted mismo, sin embargo, esto generalmente es fácil de hacer.

He usado YAML como lenguaje de configuración en mis aplicaciones de Python, y en este caso a menudo defino una convención como esta:

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

Luego en mi código (python) hago:

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

El único inconveniente es que las variables en las inclusiones siempre anularán las variables en main, y no hay forma de cambiar esa precedencia cambiando dónde aparece la instrucción "include:" en el archivo main.yml.

En un punto ligeramente diferente, YAML no es compatible, ya que no está realmente diseñado tan exclusivamente como un marcado basado en archivos. ¿Qué significaría un include si lo recibiste en respuesta a una solicitud de AJAX?

clh
fuente
3
esto solo funciona cuando el archivo yaml no contiene una configuración anidada.
Libertad
10

Para usuarios de Python, puedes probar pyyaml-include .

Instalar en pc

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 tenemos tales archivos YAML :

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

Incluir archivos por nombre

  • En el nivel superior:

    Si 0.yamlfue:

!include include.d/1.yaml

Nosotros recibiremos:

{"name": "1"}
  • En mapeo:

    Si 0.yamlfue:

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

Nosotros recibiremos:

  file1:
    name: "1"
  file2:
    name: "2"
  • En secuencia:

    Si 0.yamlfue:

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

Nosotros recibiremos:

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

Nota :

El nombre del archivo puede ser absoluto (como /usr/conf/1.5/Make.yml) o relativo (como ../../cfg/img.yml).

Incluir archivos por comodines

El nombre del archivo puede contener comodines de estilo shell. Los datos cargados de los archivos encontrados por comodines se establecerán en una secuencia.

Si 0.yamlfue:

files: !include include.d/*.yaml

Nosotros recibiremos:

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

Nota :

  • Por ejemploPython>=3.5 , si el recursiveargumento de la etiqueta !include YAML es true, el patrón “**”coincidirá con cualquier archivo y cero o más directorios y subdirectorios.
  • El uso del “**”patrón en grandes árboles de directorios puede consumir una cantidad excesiva de tiempo debido a la búsqueda recursiva.

Para habilitar el recursiveargumento, escribiremos la !includeetiqueta en Mappingo Sequencemodo:

  • Argumentos en Sequencemodo:
!include [tests/data/include.d/**/*.yaml, true]
  • Argumentos en Mappingmodo:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
xqliang
fuente
Esto en realidad no responde la pregunta. Se trata de una solución de Python, no una que use el formato estandarizado YAML.
oligofren
@oligofren Los manejadores de etiquetas personalizadas son una característica de YAML, que permite a los analizadores extender YAML para especificar tipos e implementar comportamientos personalizados como estos. Sería un largo tramo por sí misma especificación YAML a ir tan lejos como para prescribir cómo la inclusión de archivos debe trabajar con todas las especificaciones de ruta dispares OS, sistemas de archivos, etc.
Anton STROGONOFF
@AntonStrogonoff Gracias por llamar mi atención. ¿Podría señalarme un lugar así en la RFC? No menciona la palabra "costumbre". Ref. Yaml.org/spec/1.2/spec.html
oligofren
1
@oligofren De nada. Busque etiquetas "específicas de la aplicación" .
Anton Strogonoff
8

Ampliando la respuesta de @ Josh_Bode, aquí está mi propia solución PyYAML, que tiene la ventaja de ser una subclase autónoma de yaml.Loader. No depende de ningún nivel global de yamlmódulo o de modificar el estado global del 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
fuente
2
Finalmente pude agregar el enfoque basado en la clase a mi respuesta, pero me ganaste el golpe :) Nota: Si usas yaml.load(f, IncludeLoader)dentro _include, puedes evitar tener que reemplazar la raíz. Además, a menos que haga esto, la solución no funcionará a más de un nivel de profundidad ya que los datos incluidos utilizan la yaml.Loaderclase regular .
Josh Bode, el
Tuve que quitar la palabra clave rootde kwargsdespués de ajustar self.rootpara que funcione con cuerdas. Moví el bloque if-else encima de la superllamada. Quizás alguien más pueda confirmar mi hallazgo o mostrarme cómo usar la clase con cadenas y el rootparámetro.
Woltan
1
Desafortunadamente, esto no funciona con referencias como `` `` incluido: & INCLUIDOS! Incluir inner.yaml merge: <<: * INCLUIDOS ``
antony
2

Hago algunos ejemplos para su referencia.

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

salida

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

Actualización 2

y puedes combinarlo así

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

Desafortunadamente, YAML no proporciona esto en su estándar.

Pero si está utilizando Ruby, hay una gema que proporciona la funcionalidad que está solicitando al extender la biblioteca ruby ​​YAML: https://github.com/entwanderer/yaml_extend

user8419486
fuente
1

Creo que la solución utilizada por @ maxy-B se ve muy bien. Sin embargo, no tuvo éxito para mí con inclusiones anidadas. Por ejemplo, si config_1.yaml incluye config_2.yaml, que incluye config_3.yaml, hubo un problema con el cargador. Sin embargo, si simplemente apunta la nueva clase de cargador a sí misma en la carga, ¡funciona! Específicamente, si reemplazamos la antigua función _include con la versión ligeramente 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

Después de reflexionar, estoy de acuerdo con los otros comentarios, que la carga anidada no es apropiada para yaml en general, ya que la secuencia de entrada puede no ser un archivo, ¡pero es muy útil!

PaddyM
fuente
1

El estándar YML no especifica una forma de hacer esto. Y este problema no se limita a YML. JSON tiene las mismas limitaciones.

Muchas aplicaciones que usan configuraciones basadas en YML o JSON se topan con este problema eventualmente. Y cuando eso sucede, inventan su propia convención. .

por ejemplo, para definiciones de API swagger:

$ref: 'file.yml'

Por ejemplo, para las configuraciones de composición de Docker:

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

Alternativamente, si desea dividir el contenido de un archivo yml en varios archivos, como un árbol de contenido, puede definir su propia convención de estructura de carpetas y usar un script de fusión (existente).

bvdb
fuente
0

YAML 1.2 estándar no incluye de forma nativa esta función. Sin embargo, muchas implementaciones proporcionan alguna extensión para hacerlo.

Presento una forma de lograrlo con Java y snakeyaml:1.24(biblioteca Java para analizar / emitir archivos YAML) que permite crear una etiqueta YAML personalizada para lograr el siguiente objetivo (verá que lo estoy usando para cargar conjuntos de pruebas definidos en varios archivos YAML y que lo hice funcionar como una lista de incluye para un test:nodo 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

Aquí está el Java de una clase que permite procesar la !includeetiqueta. Los archivos se cargan desde classpath (directorio de recursos de 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
fuente
0

Con Yglu , puede importar otros archivos como este:

A.yaml

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

B.yaml

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

Como $importes una función, también puede pasar una expresión como argumento:

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

Esto daría el mismo resultado que el anterior.

Descargo de responsabilidad: soy el autor de Yglu.

lbovet
fuente
-1

Con Symfony , su manejo de yaml le permitirá indirectamente anidar archivos yaml. El truco es hacer uso de la parametersopción. p.ej:

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

El resultado será el mismo que:

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

Probablemente no fue compatible cuando se hizo la pregunta, pero puede importar otro archivo YAML en uno:

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

Aunque no tengo ninguna referencia en línea, pero esto funciona para mí.

Sankalp
fuente
44
Esto no incluye nada en absoluto. Crea una asignación con una secuencia que consiste en una sola cadena "/your_location_to_yaml_file/Util.area.yaml", como valor para la clave imports.
Anthon