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
 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
 75    @staticmethod
 76    def load(*args, **kw):
 77        """
 78        Execute `yaml.load()`.
 79        Handles the breaking change at `v6.0` of `PyYAML`
 80        (added `yaml.Loader` as a positional argument).
 81        """
 82        packaging_version = attempt_import('packaging.version')
 83        _args = list(args)
 84        if (
 85            _import_name == 'yaml'
 86            and packaging_version.parse(_yaml.__version__) >= packaging_version.parse('6.0')
 87        ):
 88            _args += [_yaml.Loader]
 89        
 90        return _yaml.load(*_args, **filter_keywords(_yaml.load, **kw))
 91
 92
 93    @staticmethod
 94    def dump(data, stream=None, **kw):
 95        """
 96        Dump to a stream. If no stream is provided, return a string instead.
 97        For `ruamel.yaml`, it dumps into a `StringIO` stream and returns `getvalue()`.
 98        """
 99        get_string = False
100        if stream is None and _import_name == 'ruamel.yaml':
101            stream = _lib.compat.StringIO()
102            get_string = True
103
104        if _import_name == 'yaml' and 'Dumper' not in kw:
105            kw['Dumper'] = get_dumper_class()
106
107        result = _yaml.dump(data, stream, **filter_keywords(_yaml.dump, **kw))
108        if get_string:
109            return stream.getvalue()
110        return result
111
112
113def get_dumper_class():
114    """
115    Return the dumper class to use when writing.
116    Only supports `yaml`.
117    """
118    global _dumper
119    if _dumper is not None:
120        return _dumper
121
122    if _import_name != 'yaml':
123        return None
124
125    class CustomDumper(_yaml.Dumper):
126        """
127        Add an extra line break when writing.
128        """
129        def write_line_break(self, data=None):
130            if len(self.indents) == 1:
131                super(CustomDumper, self).write_line_break(data)
132            super(CustomDumper, self).write_line_break(data)
133
134    _dumper = CustomDumper
135    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
 66    @staticmethod
 67    def safe_load(*args, **kw):
 68        """
 69        Execute `safe_load` for `PyYAML` and `load` for `ruamel.yaml`.
 70        """
 71        if _import_name == 'ruamel.yaml':
 72            return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
 73        return _yaml.safe_load(*args, **filter_keywords(_yaml.safe_load, **kw))
 74
 75
 76    @staticmethod
 77    def load(*args, **kw):
 78        """
 79        Execute `yaml.load()`.
 80        Handles the breaking change at `v6.0` of `PyYAML`
 81        (added `yaml.Loader` as a positional argument).
 82        """
 83        packaging_version = attempt_import('packaging.version')
 84        _args = list(args)
 85        if (
 86            _import_name == 'yaml'
 87            and packaging_version.parse(_yaml.__version__) >= packaging_version.parse('6.0')
 88        ):
 89            _args += [_yaml.Loader]
 90        
 91        return _yaml.load(*_args, **filter_keywords(_yaml.load, **kw))
 92
 93
 94    @staticmethod
 95    def dump(data, stream=None, **kw):
 96        """
 97        Dump to a stream. If no stream is provided, return a string instead.
 98        For `ruamel.yaml`, it dumps into a `StringIO` stream and returns `getvalue()`.
 99        """
100        get_string = False
101        if stream is None and _import_name == 'ruamel.yaml':
102            stream = _lib.compat.StringIO()
103            get_string = True
104
105        if _import_name == 'yaml' and 'Dumper' not in kw:
106            kw['Dumper'] = get_dumper_class()
107
108        result = _yaml.dump(data, stream, **filter_keywords(_yaml.dump, **kw))
109        if get_string:
110            return stream.getvalue()
111        return result

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

@staticmethod
def safe_load(*args, **kw):
66    @staticmethod
67    def safe_load(*args, **kw):
68        """
69        Execute `safe_load` for `PyYAML` and `load` for `ruamel.yaml`.
70        """
71        if _import_name == 'ruamel.yaml':
72            return _yaml.load(*args, **filter_keywords(_yaml.load, **kw))
73        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):
76    @staticmethod
77    def load(*args, **kw):
78        """
79        Execute `yaml.load()`.
80        Handles the breaking change at `v6.0` of `PyYAML`
81        (added `yaml.Loader` as a positional argument).
82        """
83        packaging_version = attempt_import('packaging.version')
84        _args = list(args)
85        if (
86            _import_name == 'yaml'
87            and packaging_version.parse(_yaml.__version__) >= packaging_version.parse('6.0')
88        ):
89            _args += [_yaml.Loader]
90        
91        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):
 94    @staticmethod
 95    def dump(data, stream=None, **kw):
 96        """
 97        Dump to a stream. If no stream is provided, return a string instead.
 98        For `ruamel.yaml`, it dumps into a `StringIO` stream and returns `getvalue()`.
 99        """
100        get_string = False
101        if stream is None and _import_name == 'ruamel.yaml':
102            stream = _lib.compat.StringIO()
103            get_string = True
104
105        if _import_name == 'yaml' and 'Dumper' not in kw:
106            kw['Dumper'] = get_dumper_class()
107
108        result = _yaml.dump(data, stream, **filter_keywords(_yaml.dump, **kw))
109        if get_string:
110            return stream.getvalue()
111        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():
114def get_dumper_class():
115    """
116    Return the dumper class to use when writing.
117    Only supports `yaml`.
118    """
119    global _dumper
120    if _dumper is not None:
121        return _dumper
122
123    if _import_name != 'yaml':
124        return None
125
126    class CustomDumper(_yaml.Dumper):
127        """
128        Add an extra line break when writing.
129        """
130        def write_line_break(self, data=None):
131            if len(self.indents) == 1:
132                super(CustomDumper, self).write_line_break(data)
133            super(CustomDumper, self).write_line_break(data)
134
135    _dumper = CustomDumper
136    return _dumper

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