Jak mogę dołączyć plik YAML do innego?

288

Mam więc dwa pliki YAML, „A” i „B”, i chcę, aby zawartość A została wstawiona do B, albo połączona z istniejącą strukturą danych, jak tablica, lub jako element podrzędny elementu, jak wartość dla określonego klucza skrótu.

Czy to w ogóle jest możliwe? W jaki sposób? Jeśli nie, jakieś wskazówki do odniesienia normatywnego?

kch
źródło
1
Niedawno wpadłem na HiYaPyCo dla Pythona, który robi dokładnie to. Możesz łączyć różne pliki YAML razem. Jest to bardzo ładny moduł Pythona, który warto poznać.
nowox

Odpowiedzi:

326

Nie, YAML nie zawiera żadnych instrukcji „import” lub „include”.

jameshfisher
źródło
8
Możesz utworzyć moduł obsługi! Include <nazwa pliku>.
clarkevans
5
@ clarkevans na pewno, ale ta konstrukcja byłaby „poza” językiem YAML.
jameshfisher
2
To jest teraz możliwe. Poniżej dodałem odpowiedź ... mam nadzieję, że to pomoże.
daveaspinall,
1
Jeśli używasz Railsów, możesz wstawić składnię ERB <% = 'fdsa fdsa'%> i zadziała
dniu
9
Myślę, że ta odpowiedź powinna zostać sformułowana w następujący sposób: „Nie, standardowy YAML nie zawiera tej funkcji. Niemniej jednak wiele implementacji zapewnia pewne rozszerzenie w tym celu”.
Franklin Yu,
112

Twoje pytanie nie wymaga rozwiązania w języku Python, ale oto jedno z nich używa PyYAML .

PyYAML umożliwia dołączanie niestandardowych konstruktorów (np. !include) Do modułu ładującego YAML. Dołączyłem katalog główny, który można ustawić, aby to rozwiązanie obsługiwało względne i bezwzględne odwołania do plików.

Rozwiązanie klasy

Oto rozwiązanie oparte na klasach, które pozwala uniknąć globalnej zmiennej root mojej oryginalnej odpowiedzi.

Zobacz tę istotę podobnego, bardziej niezawodnego rozwiązania Python 3, które używa metaklasy do rejestrowania niestandardowego konstruktora.

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)

Przykład:

foo.yaml

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

bar.yaml

- 3.6
- [1, 2, 3]

Teraz pliki można załadować za pomocą:

>>> 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
źródło
To interesująca funkcja, dzięki. Ale jaki jest cel wszystkich tych manipulacji przy użyciu root / old_root? Podejrzewam, że kod includefunkcji można uprościć: `def include (loader, node):" "" Dołącz inny plik YAML. "" "Nazwa_pliku = loader.construct_scalar (węzeł) dane = yaml.load (otwórz (nazwa pliku))`
Aliaksei Ramanau
Globalny katalog główny istnieje, więc krewny obejmuje pracę na dowolnej głębokości, np. Gdy dołączone pliki znajdujące się w innym katalogu zawierają plik względem tego katalogu. Absolute obejmuje również powinno działać. Prawdopodobnie istnieje lepszy sposób na zrobienie tego bez zmiennej globalnej, być może przy użyciu niestandardowej klasy yaml.Loader.
Josh Bode
2
Czy można również mieć coś takiego: foo.yaml: a: bla bar.yaml: `! Include foo.yaml b: blubb` Aby wynik był następujący:` {{a a: bla, 'b': blubb}
Martin
3
To powinna być zaakceptowana odpowiedź. Również nitpick bezpieczeństwa, powinieneś użyć yaml.safeload zamiast yaml.load, aby uniknąć specjalnie spreparowanego yaml z posiadania twojej usługi.
danielpops
1
@JoshBode powinno to działać dla ciebie: gist.github.com/danielpops/5a0726f2fb6288da749c4cd604276be8
danielpops
32

Jeśli używasz wersji YAML Symfony , jest to możliwe, jak poniżej:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }
daveaspinall
źródło
34
Jest to specyficzne dla tego, jak Symfony interpretuje YAML, a nie jako część samego YAML.
jameshfisher,
9
Tak, dlatego opublikowałem link do dokumentacji Symfony. Pytanie brzmi: „Czy to w ogóle możliwe? Jak?”… Tak jest. Nie widzę powodu, aby głosować negatywnie.
daveaspinall,
4
Nie głosowałem cię; Zaznaczam tylko, że jest to specyficzne dla Symfony YAML.
jameshfisher,
9
Nie ma „wersji YAML Symfony” ... jest to po prostu biblioteka kompatybilna z YAML producenta, która zawiera dodatkowe rzeczy, które nie są częścią YAML.
dreftymac,
3
Nie ma powodu, aby głosować za odrzuceniem tej odpowiedzi, jeśli odpowiedź „oparta na klasie” jest pozytywnie oceniona.
Michaił
13

Uwzględnienia nie są bezpośrednio obsługiwane w YAML, o ile mi wiadomo, będziesz musiał sam zapewnić mechanizm, jednak generalnie jest to łatwe.

Używałem YAML jako języka konfiguracji w moich aplikacjach w Pythonie iw tym przypadku często definiuję taką konwencję:

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

Następnie w moim (pythonowym) kodzie:

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

Jedynym minusem jest to, że zmienne w dołączeniach zawsze nadpisują zmienne w main, i nie ma sposobu, aby zmienić ten priorytet, zmieniając miejsce, w którym instrukcja „include: pojawia się w pliku main.yml.

W nieco innej kwestii, YAML nie obsługuje włączania, ponieważ nie jest tak zaprojektowany, jak wyłączny znacznik oparty na plikach. Co oznaczałoby włączenie, jeśli otrzymałeś je w odpowiedzi na żądanie AJAX?

clh
źródło
3
działa to tylko wtedy, gdy plik yaml nie zawiera zagnieżdżonej konfiguracji.
Freedom
10

W przypadku użytkowników Pythona możesz wypróbować pyyaml-include .

zainstalować

pip install pyyaml-include

Stosowanie

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)

Rozważmy, że mamy takie pliki YAML :

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml
  • 1.yaml treść:
name: "1"
  • 2.yaml treść:
name: "2"

Dołącz pliki według nazwy

  • Na najwyższym poziomie:

    Jeśli 0.yamlbył:

!include include.d/1.yaml

Dostaniemy:

{"name": "1"}
  • W mapowaniu:

    Jeśli 0.yamlbył:

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

Dostaniemy:

  file1:
    name: "1"
  file2:
    name: "2"
  • Kolejno:

    Jeśli 0.yamlbył:

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

Dostaniemy:

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

Uwaga :

Nazwa pliku może być bezwzględna (jak /usr/conf/1.5/Make.yml) lub względna (jak ../../cfg/img.yml).

Dołącz pliki według symboli wieloznacznych

Nazwa pliku może zawierać symbole wieloznaczne w stylu powłoki. Dane ładowane z pliku (ów) znalezionych przez symbole wieloznaczne zostaną ustawione w sekwencji.

Jeśli 0.yamlbył:

files: !include include.d/*.yaml

Dostaniemy:

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

Uwaga :

  • Na przykład Python>=3.5, jeśli recursiveargumentem tagu !include YAML jest true, wzorzec “**”będzie pasował do dowolnych plików i zero lub więcej katalogów i podkatalogów.
  • Używanie “**”wzorca w dużych drzewach katalogów może pochłaniać nadmiernie dużo czasu ze względu na wyszukiwanie rekurencyjne.

Aby włączyć recursiveargument, napiszemy !includetag w trybie Mappinglub Sequence:

  • Argumenty w Sequencetrybie:
!include [tests/data/include.d/**/*.yaml, true]
  • Argumenty w Mappingtrybie:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}
xqliang
źródło
To nie odpowiada na pytanie. Dotyczy rozwiązania w języku Python, a nie w standaryzowanym formacie YAML.
oligofren
@oligofren Niestandardowe programy obsługi znaczników to funkcja YAML, która pozwala analizatorom rozszerzyć YAML w celu określenia typów i zaimplementowania takich niestandardowych zachowań. Sama specyfikacja YAML byłaby długa, aby określić, w jaki sposób włączenie plików powinno działać ze wszystkimi różnymi specyfikacjami ścieżek systemu operacyjnego, systemami plików itp.
Anton Strogonoff
@AntonStrogonoff Dziękuję za zwrócenie mojej uwagi. Czy możesz wskazać mi takie miejsce w RFC? Nie ma w nim słowa „niestandardowy”. Ref. Yaml.org/spec/1.2/spec.html
oligofren
1
@oligofren Nie ma za co. Poszukaj tagów „specyficznych dla aplikacji” .
Anton Strogonoff,
8

Rozwijając odpowiedź @ Josh_Bode, oto moje własne rozwiązanie PyYAML, które ma tę zaletę, że jest samodzielną podklasą yaml.Loader. Nie zależy to od globalizacji na poziomie yamlmodułu ani od modyfikacji globalnego stanu modułu.

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
źródło
2
W końcu doszedłem do dodania opartego na klasach podejścia do mojej odpowiedzi, ale pobiłeś mnie do sedna :) Uwaga: Jeśli użyjesz tego yaml.load(f, IncludeLoader)w _includesobie, możesz uniknąć konieczności wymiany roota. Ponadto, chyba że to zrobisz, rozwiązanie nie będzie działało głębiej niż na jednym poziomie, ponieważ zawarte dane korzystają ze zwykłej yaml.Loaderklasy.
Josh Bode
Musiałem usunąć słowa kluczowego rootz kwargspo ustawieniu self.rootje zdobyć pracę z tekstem. Przeniosłem blok if-else nad superrozmowę. Może ktoś inny może potwierdzić moje odkrycie lub pokazać, jak używać klasy z ciągami znaków i rootparametrem.
Woltan
1
Niestety nie działa to z odniesieniami takimi jak `` zawarte: i ZAWARTE! Obejmują scalenie inner.yaml: <<: * ZAWARTE ''
antony
2

Podaję kilka przykładów w celach informacyjnych.

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

wynik

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

Aktualizacja 2

i możesz to połączyć w ten sposób

# 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
źródło
1

Niestety YAML nie zapewnia tego w swoim standardzie.

Ale jeśli używasz Ruby, istnieje klejnot zapewniający funkcjonalność, o którą prosisz, rozszerzając bibliotekę Ruby YAML: https://github.com/entwanderer/yaml_extend

użytkownik8419486
źródło
1

Myślę, że rozwiązanie zastosowane przez @ maxy-B wygląda świetnie. Jednak nie udało mi się to dzięki zagnieżdżonym inkluzjom. Na przykład jeśli config_1.yaml zawiera config_2.yaml, który obejmuje config_3.yaml, to wystąpił problem z modułem ładującym. Jeśli jednak po prostu wskażesz nową klasę modułu ładującego na ładunek, zadziała! W szczególności, jeśli zastąpimy starą funkcję _include bardzo nieznacznie zmodyfikowaną wersją:

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

Po zastanowieniu zgadzam się z innymi komentarzami, że ładowanie zagnieżdżone nie jest ogólnie odpowiednie dla yaml, ponieważ strumień wejściowy może nie być plikiem, ale jest bardzo przydatny!

PaddyM
źródło
1

Standard YML nie określa sposobu wykonania tej czynności. Ten problem nie ogranicza się do YML. JSON ma te same ograniczenia.

Wiele aplikacji korzystających z konfiguracji opartych na YML lub JSON może w końcu napotkać ten problem. A kiedy to się dzieje, tworzą własną konwencję .

np. dla definicji API swagger:

$ref: 'file.yml'

np. dla konfiguracji tworzenia dokera:

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

Alternatywnie, jeśli chcesz podzielić zawartość pliku yml na wiele plików, takich jak drzewo zawartości, możesz zdefiniować własną konwencję dotyczącą struktury folderów i użyć (istniejącego) skryptu scalania.

bvdb
źródło
0

Standardowy YAML 1.2 nie obejmuje natywnie tej funkcji. Niemniej jednak wiele implementacji zapewnia pewne rozszerzenie w tym celu.

Przedstawiam sposób osiągnięcia tego za pomocą Java i snakeyaml:1.24(biblioteki Java do parsowania / emitowania plików YAML), który pozwala na utworzenie niestandardowego znacznika YAML w celu osiągnięcia następującego celu (zobaczysz, że używam go do ładowania pakietów testowych zdefiniowanych w kilku plikach YAML i sprawiłem, że działa jako lista dołączeń dla test:węzła docelowego ):

# ... yaml prev stuff

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

# ... more yaml document

Oto jedna klasa Java, która umożliwia przetwarzanie !includeznacznika. Pliki są ładowane ze ścieżki klasy (katalog zasobów 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
źródło
0

Za pomocą Yglu możesz importować inne pliki takie jak to:

A.yaml

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

B.yaml

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

Jako $importfunkcja możesz również przekazać wyrażenie jako argument:

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

To dałoby taką samą wydajność jak powyżej.

Oświadczenie: Jestem autorem Yglu.

lbovet
źródło
-1

Dzięki Symfony jego obsługa yaml pozwoli pośrednio na zagnieżdżanie plików yaml. Sztuką jest skorzystanie z tej parametersopcji. na przykład:

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

Wynik będzie taki sam jak:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
jxmallett
źródło
-6

Prawdopodobnie nie było obsługiwane po zadaniu pytania, ale możesz zaimportować inny plik YAML do jednego:

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

Chociaż nie mam żadnych odnośników online, ale to działa dla mnie.

Sankalp
źródło
4
To wcale nie obejmuje. Tworzy mapowanie z sekwencją składającą się z pojedynczego ciągu „/ twój_lokalizacja_do_pliku_amaml/Util.area.yaml”, jako wartość klucza imports.
Anthon