blob: a79dfec1e89764e641c88d57d024c6304281d5b5 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""YAML parser utils, parser yaml string to ``ruamel.yaml`` object and nested key dict."""
from __future__ import annotations
import copy
import io
from typing import Any
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
class YamlParser:
"""A parser to parse Yaml file and provider easier way to access or change value.
This parser provider delimiter string key to get or set :class:`ruamel.yaml.YAML` object
For example, yaml config named ``test.yaml`` and its content as below:
.. code-block:: yaml
one:
two1:
three: value1
two2: value2
you could get ``value1`` and ``value2`` by nested path
.. code-block:: python
yaml_parser = YamlParser("test.yaml")
# Use function ``get`` to get value
value1 = yaml_parser.get("one.two1.three")
# Or use build-in ``__getitem__`` to get value
value2 = yaml_parser["one.two2"]
or you could change ``value1`` to ``value3``, also change ``value2`` to ``value4`` by nested path assigned
.. code-block:: python
yaml_parser["one.two1.three"] = "value3"
yaml_parser["one.two2"] = "value4"
"""
def __init__(self, content: str, delimiter: str | None = "."):
self._content = content
self.src_parser = content
self._delimiter = delimiter
@property
def src_parser(self) -> CommentedMap:
"""Get src_parser property."""
return self._src_parser
@src_parser.setter
def src_parser(self, content: str) -> None:
"""Set src_parser property."""
self._yaml = YAML()
self._src_parser = self._yaml.load(content)
def parse_nested_dict(
self, result: dict, commented_map: CommentedMap, key: str
) -> None:
"""Parse :class:`ruamel.yaml.comments.CommentedMap` to nested dict using :param:`delimiter`."""
if not isinstance(commented_map, CommentedMap):
return
for sub_key in set(commented_map.keys()):
next_key = f"{key}{self._delimiter}{sub_key}"
result[next_key] = commented_map[sub_key]
self.parse_nested_dict(result, commented_map[sub_key], next_key)
@property
def dict_parser(self) -> dict:
"""Get :class:`CommentedMap` to nested dict using :param:`delimiter` as key delimiter.
Use Depth-First-Search get all nested key and value, and all key connect by :param:`delimiter`.
It make users could easier access or change :class:`CommentedMap` object.
For example, yaml config named ``test.yaml`` and its content as below:
.. code-block:: yaml
one:
two1:
three: value1
two2: value2
It could parser to nested dict as
.. code-block:: python
{
"one": ordereddict([('two1', ordereddict([('three', 'value1')])), ('two2', 'value2')]),
"one.two1": ordereddict([('three', 'value1')]),
"one.two1.three": "value1",
"one.two2": "value2",
}
"""
res = dict()
src_parser_copy = copy.deepcopy(self.src_parser)
base_keys = set(src_parser_copy.keys())
if not base_keys:
return res
else:
for key in base_keys:
res[key] = src_parser_copy[key]
self.parse_nested_dict(res, src_parser_copy[key], key)
return res
def __contains__(self, key) -> bool:
return key in self.dict_parser
def __getitem__(self, key: str) -> Any:
return self.dict_parser[key]
def __setitem__(self, key: str, val: Any) -> None:
if key not in self.dict_parser:
raise KeyError("Key %s do not exists.", key)
mid = None
keys = key.split(self._delimiter)
for idx, k in enumerate(keys, 1):
if idx == len(keys):
mid[k] = val
else:
mid = mid[k] if mid else self.src_parser[k]
def get(self, key: str) -> Any:
"""Get value by key, is call ``__getitem__``."""
return self[key]
def __str__(self) -> str:
"""Transfer :class:`YamlParser` to string object.
It is useful when users want to output the :class:`YamlParser` object they change just now.
"""
buf = io.StringIO()
self._yaml.dump(self.src_parser, buf)
return buf.getvalue()
def __repr__(self) -> str:
return f"YamlParser({str(self)})"