Come posso includere un file YAML all'interno di un altro?


288

Quindi ho due file YAML, "A" e "B" e voglio che il contenuto di A sia inserito all'interno di B, o impiombato nella struttura di dati esistente, come un array, o come figlio di un elemento, come il valore per una certa chiave hash.

Ciò è effettivamente possibile? Come? In caso contrario, qualche puntatore a un riferimento normativo?



1
Di recente ho incontrato HiYaPyCo per Python che fa esattamente questo. È possibile unire insieme diversi file YAML. Is è un modulo Python molto bello che vale la pena conoscere.
nowox,

Risposte:


327

No, YAML non include alcun tipo di istruzione "import" o "include".


8
È possibile creare un gestore! Include <nomefile>.
Clarkevans,

5
@clarkevans certo, ma quel costrutto sarebbe "fuori" dal linguaggio YAML.
jameshfisher,

2
Questo è ora possibile. Ho aggiunto una risposta qui sotto ... spero che sia d'aiuto.
daveaspinall,

1
Se stai usando Rails, puoi inserire la sintassi <% = 'fdsa fdsa'%> ERB e funzionerà
gleenn,

9
Penso che questa risposta debba essere riformulata come "No, YAML standard non include questa funzione. Tuttavia molte implementazioni forniscono alcune estensioni per farlo."
Franklin Yu,

113

La tua domanda non richiede una soluzione Python, ma eccone una PyYAML .

PyYAML ti consente di collegare costruttori personalizzati (come !include ) al caricatore YAML. Ho incluso una directory principale che può essere impostata in modo che questa soluzione supporti riferimenti di file relativi e assoluti.

Soluzione basata sulla classe

Ecco una soluzione di classe, che evita la variabile radice globale della mia risposta originale.

Guarda questo riassunto per una soluzione Python 3 simile e più robusta che utilizza una metaclasse per registrare il costruttore personalizzato.

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 esempio:

foo.yaml

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

bar.yaml

- 3.6
- [1, 2, 3]

Ora i file possono essere caricati 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]]}

Questa è una caratteristica interessante, grazie. Ma qual è lo scopo di tutte queste manipolazioni con root / old_root? Suppongo che il codice della includefunzione possa essere semplificato: `def include (caricatore, nodo):" "" Includi un altro file YAML. "" "Nome file = loader.construct_scalar (nodo) dati = yaml.load (aperto (nome file))`
Aliaksei Ramanau,

Il root globale è lì in modo che relativo includa il lavoro a qualsiasi profondità, ad esempio quando i file inclusi che si trovano in una directory diversa includono un file relativo a quella directory. Anche le inclusioni assolute dovrebbero funzionare. C'è probabilmente un modo più pulito per farlo senza una variabile globale, magari usando una classe yaml.Loader personalizzata.
Josh Bode,

2
È anche possibile avere qualcosa del genere: foo.yaml: a: bla bar.yaml: `! Include foo.yaml b: blubb` In modo che il risultato sia:` {'a': bla, 'b': blubb}
Martin

3
Questa dovrebbe essere la risposta accettata. Inoltre, un nitpick di sicurezza, dovresti usare yaml.safeload invece di yaml.load, per evitare che yaml appositamente predisposto possieda il tuo servizio.
danielpops

1
@JoshBode questo dovrebbe funzionare per te: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops

32

Se stai usando la versione di YAML di Symfony , questo è possibile, in questo modo:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }

34
Questo è specifico di come Symfony interpreta YAML, piuttosto che parte di YAML stesso.
jameshfisher,

9
Sì, è per questo che ho pubblicato il link ai documenti di Symfony. La domanda si pone "È possibile tutto questo? Come?" ... ecco come. Non vedi alcun motivo per un downvote.
daveaspinall,

4
Non ti ho votato a fondo; Sto solo sottolineando che questo è specifico di Symfony YAML.
jameshfisher,

9
Non esiste una "versione di YAML di Symfony" ... questa è semplicemente una libreria compatibile con YAML specifica del fornitore che contiene elementi extra che non fanno parte di YAML.
dreftymac

3
Nessun motivo per sottovalutare questa risposta se la risposta "basata sulla classe" è votata.
Mikhail,

13

Le inclusioni non sono supportate direttamente in YAML, per quanto ne so, dovrete fornire un meccanismo da soli, tuttavia questo è generalmente facile da fare.

Ho usato YAML come linguaggio di configurazione nelle mie app Python e in questo caso spesso definisco una convenzione come questa:

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

Quindi nel mio codice (python) faccio:

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

L'unico lato negativo è che le variabili in include avranno sempre la precedenza sulle variabili in main, e non c'è modo di cambiare quella precedenza cambiando la posizione dell'istruzione "include: nel file main.yml.

Su un punto leggermente diverso, YAML non supporta le inclusioni poiché non è progettato in modo esclusivo come un mark-up basato su file. Cosa significherebbe un'inclusione se la ottenessi in risposta a una richiesta AJAX?


3
funziona solo quando il file yaml non contiene una configurazione nidificata.
Libertà,

10

Per gli utenti di Python, puoi provare pyyaml-include .

Installare

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)

Considera che abbiamo tali file YAML :

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

Includi i file per nome

  • Al livello superiore:

    Se 0.yamlfosse:

!include include.d/1.yaml

Otterremo:

{"name": "1"}
  • Nella mappatura:

    Se 0.yamlfosse:

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

Otterremo:

  file1:
    name: "1"
  file2:
    name: "2"
  • In sequenza:

    Se 0.yamlfosse:

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

Otterremo:

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

Nota :

Il nome del file può essere assoluto (come /usr/conf/1.5/Make.yml) o relativo (come ../../cfg/img.yml).

Includi file con caratteri jolly

Il nome del file può contenere caratteri jolly in stile shell. I dati caricati dai file trovati dai caratteri jolly verranno impostati in sequenza.

Se 0.yamlfosse:

files: !include include.d/*.yaml

Otterremo:

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

Nota :

  • Perché Python>=3.5, se l' recursiveargomento del tag !include YAML è true, il modello “**”corrisponderà a qualsiasi file e zero o più directory e sottodirectory.
  • L'uso del “**”modello in alberi di directory di grandi dimensioni può richiedere un tempo eccessivo a causa della ricerca ricorsiva.

Per abilitare l' recursiveargomento, dovremo scrivere il !includetag in Mappingo Sequencemode:

  • Argomenti in Sequencemodalità:
!include [tests/data/include.d/**/*.yaml, true]
  • Argomenti in Mappingmodalità:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}

Questo in realtà non risponde alla domanda. Riguarda una soluzione Python, non una che utilizza il formato YAML standardizzato.
Oligofren,

@oligofren I gestori di tag personalizzati sono una funzionalità di YAML, che consente ai parser di estendere YAML per specificare i tipi e implementare comportamenti personalizzati come questi. Sarebbe molto lungo per la specifica YAML arrivare fino a prescrivere come l'inclusione dei file dovrebbe funzionare con tutte le specifiche del percorso del sistema operativo, i filesystem, ecc.
Anton Strogonoff,

@AntonStrogonoff Grazie per averlo portato alla mia attenzione. Potresti indicarmi un posto simile nella RFC? Non ha menzione della parola "personalizzato". Rif yaml.org/spec/1.2/spec.html
oligofren

1
@oligofren Prego. Cerca tag "specifici dell'applicazione" .
Anton Strogonoff,

8

Espandendo la risposta di @ Josh_Bode, ecco la mia soluzione PyYAML, che ha il vantaggio di essere una sottoclasse autonoma di yaml.Loader. Non dipende da alcun globale a livello di yamlmodulo o dalla modifica dello stato globale del modulo.

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                                                      

2
Finalmente sono riuscito ad aggiungere l'approccio basato sulla classe alla mia risposta, ma mi hai battuto sul pugno :) Nota: se usi yaml.load(f, IncludeLoader)dentro _includepuoi evitare di sostituire il root. Inoltre, a meno che non lo facciate, la soluzione non funzionerà a più di un livello di profondità poiché i dati inclusi utilizzano la yaml.Loaderclasse normale .
Josh Bode,

Ho dovuto rimuovere la parola chiave rootdi kwargsdopo l'impostazione self.rootper farlo funzionare con le stringhe. Ho spostato il blocco if-else sopra la superchiamata. Forse qualcun altro può confermare la mia scoperta o mostrarmi come usare la classe con le stringhe e il rootparametro.
Woltan,

1
Sfortunatamente, questo non funziona con riferimenti come `` incluso: & INCLUSO! Includi inner.yaml merge: <<: * INCLUSED `` `
antony

2

Faccio alcuni esempi per il tuo riferimento.

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

produzione

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

Aggiornamento 2

e puoi combinarlo, in questo modo

# 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.


1

Penso che la soluzione utilizzata da @ maxy-B sia fantastica. Tuttavia, non è riuscito per me con inclusioni nidificate. Ad esempio, se config_1.yaml include config_2.yaml, che include config_3.yaml, si è verificato un problema con il caricatore. Tuttavia, se si punta semplicemente la nuova classe del caricatore su se stessa al caricamento, funziona! In particolare, se sostituiamo la vecchia funzione _include con la versione leggermente modificata:

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

Riflettendomi concordo con gli altri commenti, che il caricamento nidificato non è appropriato per yaml in generale poiché il flusso di input potrebbe non essere un file, ma è molto utile!


1

Lo standard YML no specifica un modo per farlo. E questo problema non si limita a YML. JSON ha gli stessi limiti.

Molte applicazioni che utilizzano configurazioni basate su YML o JSON alla fine incontrano questo problema. E quando ciò accade, formano la propria convenzione .

ad es. per definizioni API swagger:

$ref: 'file.yml'

ad es. per le configurazioni di composizione docker:

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

In alternativa, se si desidera dividere il contenuto di un file yml in più file, come un albero di contenuti, è possibile definire la propria convenzione di struttura delle cartelle e utilizzare uno script di unione (esistente).



0

Lo standard YAML 1.2 non include nativamente questa funzione. Tuttavia molte implementazioni forniscono alcune estensioni per farlo.

Vi presento un modo per raggiungerlo con Java e snakeyaml:1.24(libreria Java per analizzare / emettere file YAML) che consente di creare un tag YAML personalizzato per raggiungere il seguente obiettivo (vedrete che lo sto usando per caricare le suite di test definite in diversi file YAML e che l'ho fatto funzionare come un elenco di include per un test:nodo di destinazione ):

# ... yaml prev stuff

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

# ... more yaml document

Ecco il Java di una classe che consente l'elaborazione del !includetag. I file vengono caricati da classpath (directory delle risorse di 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));
                }
            }
        }

    }

}

0

Con Yglu , puoi importare altri file come questo:

A.yaml

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

B.yaml

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

Come $importè una funzione, puoi anche passare un'espressione come argomento:

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

Ciò darebbe lo stesso risultato di cui sopra.

Disclaimer: sono l'autore di Yglu.


-1

Con Symfony , la sua gestione di yaml consentirà indirettamente di nidificare i file yaml. Il trucco è utilizzare l' parametersopzione. per esempio:

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

Il risultato sarà lo stesso di:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"

-6

Probabilmente non era supportato quando è stata posta la domanda, ma è possibile importare altri file YAML in uno:

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

Anche se non ho alcun riferimento online ma questo funziona per me.


4
Questo non include affatto. Crea una mappatura con una sequenza costituita da una singola stringa "/your_location_to_yaml_file/Util.area.yaml", come valore per la chiave imports.
Anthon,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.