meerschaum.utils.yaml

Meerschaum wrapper around YAML libraries.

This is so switching between PyYAML and ruamel.yaml is smoother.

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6Meerschaum wrapper around YAML libraries.
  7
  8This is so switching between PyYAML and ruamel.yaml is smoother.
  9"""
 10
 11from meerschaum.utils.misc import filter_keywords
 12from meerschaum.utils.packages import attempt_import, all_packages, _import_module
 13from meerschaum.utils.warnings import error
 14from meerschaum.utils.threading import Lock
 15
 16_lib = None
 17### Also supports 'ruamel.yaml'.
 18_import_name = 'yaml'
 19
 20_yaml = None
 21_dumper = None
 22_locks = {
 23    '_lib': Lock(),
 24    '_yaml': Lock(),
 25}
 26
 27__pdoc__ = {
 28    'attempt_import': False,
 29    'error': False,
 30    'all_packages': False,
 31}
 32
 33
 34def _string_presenter(dumper, data: str):
 35    """
 36    Format strings with newlines as blocks.
 37    https://stackoverflow.com/a/33300001/9699829
 38    """
 39    tag_str = 'tag:yaml.org,2002:str'
 40    kw = {}
 41    if len(data.splitlines()) > 1:
 42        kw['style'] = '|'
 43    return dumper.represent_scalar(tag_str, data, **kw)
 44
 45
 46class yaml:
 47    """
 48    Wrapper around `PyYAML` and `ruamel.yaml` so that we may switch between implementations.
 49    """
 50    global _yaml, _lib, _dumper
 51    if _import_name is None:
 52        error("No YAML library declared.")
 53    with _locks['_lib']:
 54        try:
 55            _lib = _import_module(_import_name)
 56        except (ImportError, ModuleNotFoundError):
 57            _lib = attempt_import(_import_name, split=False, lazy=False, install=True)
 58    with _locks['_yaml']:
 59        _yaml = _lib if _import_name != 'ruamel.yaml' else _lib.YAML()
 60        if _import_name != 'ruamel.yaml':
 61            _yaml.add_representer(str, _string_presenter)
 62            _yaml.representer.SafeRepresenter.add_representer(str, _string_presenter)
 63
 64    @staticmethod
 65    def safe_load(*args, **kw):
 66        """
 67        Execute `safe_load` for `PyYAML` and `load` for `ruamel.yaml`.
 68        """
 69        if _import_name == 'ruamel.yaml':
 70            return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
 71        return _yaml.safe_load(*args, **filter_keywords(_yaml.safe_load, **kw))
 72
 73    @staticmethod
 74    def load(*args, **kw):
 75        """
 76        Execute `yaml.load()`.
 77        Handles the breaking change at `v6.0` of `PyYAML`
 78        (added `yaml.Loader` as a positional argument).
 79        """
 80        packaging_version = attempt_import('packaging.version')
 81        if (
 82            _import_name == 'yaml'
 83            and packaging_version.parse(_yaml.__version__) >= packaging_version.parse('6.0')
 84            and 'Loader' not in kw
 85        ):
 86            kw['Loader'] = _yaml.Loader
 87
 88        return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
 89
 90    @staticmethod
 91    def dump(data, stream=None, **kw):
 92        """
 93        Dump to a stream. If no stream is provided, return a string instead.
 94        For `ruamel.yaml`, it dumps into a `StringIO` stream and returns `getvalue()`.
 95        """
 96        get_string = False
 97        if stream is None and _import_name == 'ruamel.yaml':
 98            stream = _lib.compat.StringIO()
 99            get_string = True
100
101        if _import_name == 'yaml' and 'Dumper' not in kw:
102            kw['Dumper'] = get_dumper_class()
103
104        result = _yaml.dump(data, stream, **filter_keywords(_yaml.dump, **kw))
105        if get_string:
106            return stream.getvalue()
107        return result
108
109
110def get_dumper_class():
111    """
112    Return the dumper class to use when writing.
113    Only supports `yaml`.
114    """
115    global _dumper
116    if _dumper is not None:
117        return _dumper
118
119    if _import_name != 'yaml':
120        return None
121
122    class CustomDumper(_yaml.Dumper):
123        """
124        Add an extra line break when writing.
125        """
126        def write_line_break(self, data=None):
127            if len(self.indents) == 1:
128                super(CustomDumper, self).write_line_break(data)
129            super(CustomDumper, self).write_line_break(data)
130
131    _dumper = CustomDumper
132    return _dumper
class yaml:
 47class yaml:
 48    """
 49    Wrapper around `PyYAML` and `ruamel.yaml` so that we may switch between implementations.
 50    """
 51    global _yaml, _lib, _dumper
 52    if _import_name is None:
 53        error("No YAML library declared.")
 54    with _locks['_lib']:
 55        try:
 56            _lib = _import_module(_import_name)
 57        except (ImportError, ModuleNotFoundError):
 58            _lib = attempt_import(_import_name, split=False, lazy=False, install=True)
 59    with _locks['_yaml']:
 60        _yaml = _lib if _import_name != 'ruamel.yaml' else _lib.YAML()
 61        if _import_name != 'ruamel.yaml':
 62            _yaml.add_representer(str, _string_presenter)
 63            _yaml.representer.SafeRepresenter.add_representer(str, _string_presenter)
 64
 65    @staticmethod
 66    def safe_load(*args, **kw):
 67        """
 68        Execute `safe_load` for `PyYAML` and `load` for `ruamel.yaml`.
 69        """
 70        if _import_name == 'ruamel.yaml':
 71            return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
 72        return _yaml.safe_load(*args, **filter_keywords(_yaml.safe_load, **kw))
 73
 74    @staticmethod
 75    def load(*args, **kw):
 76        """
 77        Execute `yaml.load()`.
 78        Handles the breaking change at `v6.0` of `PyYAML`
 79        (added `yaml.Loader` as a positional argument).
 80        """
 81        packaging_version = attempt_import('packaging.version')
 82        if (
 83            _import_name == 'yaml'
 84            and packaging_version.parse(_yaml.__version__) >= packaging_version.parse('6.0')
 85            and 'Loader' not in kw
 86        ):
 87            kw['Loader'] = _yaml.Loader
 88
 89        return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
 90
 91    @staticmethod
 92    def dump(data, stream=None, **kw):
 93        """
 94        Dump to a stream. If no stream is provided, return a string instead.
 95        For `ruamel.yaml`, it dumps into a `StringIO` stream and returns `getvalue()`.
 96        """
 97        get_string = False
 98        if stream is None and _import_name == 'ruamel.yaml':
 99            stream = _lib.compat.StringIO()
100            get_string = True
101
102        if _import_name == 'yaml' and 'Dumper' not in kw:
103            kw['Dumper'] = get_dumper_class()
104
105        result = _yaml.dump(data, stream, **filter_keywords(_yaml.dump, **kw))
106        if get_string:
107            return stream.getvalue()
108        return result

Wrapper around PyYAML and ruamel.yaml so that we may switch between implementations.

@staticmethod
def safe_load(*args, **kw):
65    @staticmethod
66    def safe_load(*args, **kw):
67        """
68        Execute `safe_load` for `PyYAML` and `load` for `ruamel.yaml`.
69        """
70        if _import_name == 'ruamel.yaml':
71            return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
72        return _yaml.safe_load(*args, **filter_keywords(_yaml.safe_load, **kw))

Execute safe_load for PyYAML and load for ruamel.yaml.

@staticmethod
def load(*args, **kw):
74    @staticmethod
75    def load(*args, **kw):
76        """
77        Execute `yaml.load()`.
78        Handles the breaking change at `v6.0` of `PyYAML`
79        (added `yaml.Loader` as a positional argument).
80        """
81        packaging_version = attempt_import('packaging.version')
82        if (
83            _import_name == 'yaml'
84            and packaging_version.parse(_yaml.__version__) >= packaging_version.parse('6.0')
85            and 'Loader' not in kw
86        ):
87            kw['Loader'] = _yaml.Loader
88
89        return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))

Execute yaml.load(). Handles the breaking change at v6.0 of PyYAML (added yaml.Loader as a positional argument).

@staticmethod
def dump(data, stream=None, **kw):
 91    @staticmethod
 92    def dump(data, stream=None, **kw):
 93        """
 94        Dump to a stream. If no stream is provided, return a string instead.
 95        For `ruamel.yaml`, it dumps into a `StringIO` stream and returns `getvalue()`.
 96        """
 97        get_string = False
 98        if stream is None and _import_name == 'ruamel.yaml':
 99            stream = _lib.compat.StringIO()
100            get_string = True
101
102        if _import_name == 'yaml' and 'Dumper' not in kw:
103            kw['Dumper'] = get_dumper_class()
104
105        result = _yaml.dump(data, stream, **filter_keywords(_yaml.dump, **kw))
106        if get_string:
107            return stream.getvalue()
108        return result

Dump to a stream. If no stream is provided, return a string instead. For ruamel.yaml, it dumps into a StringIO stream and returns getvalue().

def get_dumper_class():
111def get_dumper_class():
112    """
113    Return the dumper class to use when writing.
114    Only supports `yaml`.
115    """
116    global _dumper
117    if _dumper is not None:
118        return _dumper
119
120    if _import_name != 'yaml':
121        return None
122
123    class CustomDumper(_yaml.Dumper):
124        """
125        Add an extra line break when writing.
126        """
127        def write_line_break(self, data=None):
128            if len(self.indents) == 1:
129                super(CustomDumper, self).write_line_break(data)
130            super(CustomDumper, self).write_line_break(data)
131
132    _dumper = CustomDumper
133    return _dumper

Return the dumper class to use when writing. Only supports yaml.