Browse Source
* [python] refactor yaml file parser * Change yaml parser package to ruamel.yaml * Refactor configuration.py module * And file.py to write file locally * Add more tests on it close: #8593 * Fix UT error * Remove pypyaml from tests * Fix file error when param create is False * Fix error logic * And tests to avoid regression3.0.0/version-upgrade
Jiajie Zhong
3 years ago
committed by
GitHub
15 changed files with 925 additions and 432 deletions
@ -0,0 +1,27 @@
|
||||
<!-- |
||||
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. |
||||
--> |
||||
|
||||
# UPDATING |
||||
|
||||
Updating is try to document non-backward compatible updates which notice users the detail changes about pydolphinscheduler. |
||||
It started after version 2.0.5 released |
||||
|
||||
## dev |
||||
|
||||
* Use package ``ruamel.yaml`` replace ``pyyaml`` for write yaml file with comment. |
@ -0,0 +1,57 @@
|
||||
# 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. |
||||
|
||||
"""File util for pydolphinscheduler.""" |
||||
|
||||
from pathlib import Path |
||||
from typing import Optional |
||||
|
||||
|
||||
def write( |
||||
content: str, |
||||
to_path: str, |
||||
create: Optional[bool] = True, |
||||
overwrite: Optional[bool] = False, |
||||
) -> None: |
||||
"""Write configs dict to configuration file. |
||||
|
||||
:param content: The source string want to write to :param:`to_path`. |
||||
:param to_path: The path want to write content. |
||||
:param create: Whether create the file parent directory or not if it does not exist. |
||||
If set ``True`` will create file with :param:`to_path` if path not exists, otherwise |
||||
``False`` will not create. Default ``True``. |
||||
:param overwrite: Whether overwrite the file or not if it exists. If set ``True`` |
||||
will overwrite the exists content, otherwise ``False`` will not overwrite it. Default ``True``. |
||||
""" |
||||
path = Path(to_path) |
||||
if not path.parent.exists(): |
||||
if create: |
||||
path.parent.mkdir(parents=True) |
||||
else: |
||||
raise ValueError( |
||||
"Parent directory do not exists and set param `create` to `False`." |
||||
) |
||||
if not path.exists(): |
||||
with path.open(mode="w") as f: |
||||
f.write(content) |
||||
elif overwrite: |
||||
with path.open(mode="w") as f: |
||||
f.write(content) |
||||
else: |
||||
raise FileExistsError( |
||||
"File %s already exists and you choose not overwrite mode.", to_path |
||||
) |
@ -1,85 +0,0 @@
|
||||
# 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. |
||||
|
||||
"""Path dict allow users access value by key chain, like `var.key1.key2.key3`.""" |
||||
|
||||
|
||||
# TODO maybe we should rewrite it by `collections.abc.MutableMapping` later, |
||||
# according to https://stackoverflow.com/q/3387691/7152658 |
||||
class PathDict(dict): |
||||
"""Path dict allow users access value by key chain, like `var.key1.key2.key3`.""" |
||||
|
||||
def __init__(self, original=None): |
||||
super().__init__() |
||||
if original is None: |
||||
pass |
||||
elif isinstance(original, dict): |
||||
for key in original: |
||||
self.__setitem__(key, original[key]) |
||||
else: |
||||
raise TypeError( |
||||
"Parameter original expected dict type but get %s", type(original) |
||||
) |
||||
|
||||
def __getitem__(self, key): |
||||
if "." not in key: |
||||
# try: |
||||
return dict.__getitem__(self, key) |
||||
# except KeyError: |
||||
# # cPickle would get error when key without value pairs, in this case we just skip it |
||||
# return |
||||
my_key, rest_of_key = key.split(".", 1) |
||||
target = dict.__getitem__(self, my_key) |
||||
if not isinstance(target, PathDict): |
||||
raise KeyError( |
||||
'Cannot get "%s" to (%s) as sub-key of "%s".' |
||||
% (rest_of_key, repr(target), my_key) |
||||
) |
||||
return target[rest_of_key] |
||||
|
||||
def __setitem__(self, key, value): |
||||
if "." in key: |
||||
my_key, rest_of_key = key.split(".", 1) |
||||
target = self.setdefault(my_key, PathDict()) |
||||
if not isinstance(target, PathDict): |
||||
raise KeyError( |
||||
'Cannot set "%s" from (%s) as sub-key of "%s"' |
||||
% (rest_of_key, repr(target), my_key) |
||||
) |
||||
target[rest_of_key] = value |
||||
else: |
||||
if isinstance(value, dict) and not isinstance(value, PathDict): |
||||
value = PathDict(value) |
||||
dict.__setitem__(self, key, value) |
||||
|
||||
def __contains__(self, key): |
||||
if "." not in key: |
||||
return dict.__contains__(self, key) |
||||
my_key, rest_of_key = key.split(".", 1) |
||||
target = dict.__getitem__(self, my_key) |
||||
if not isinstance(target, PathDict): |
||||
return False |
||||
return rest_of_key in target |
||||
|
||||
def setdefault(self, key, default): |
||||
"""Overwrite method dict.setdefault.""" |
||||
if key not in self: |
||||
self[key] = default |
||||
return self[key] |
||||
|
||||
__setattr__ = __setitem__ |
||||
__getattr__ = __getitem__ |
@ -0,0 +1,169 @@
|
||||
# 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.""" |
||||
|
||||
import copy |
||||
import io |
||||
from typing import Any, Dict, Optional |
||||
|
||||
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: Optional[str] = "."): |
||||
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 get_int(self, key: str) -> int: |
||||
"""Get value and covert it to int.""" |
||||
return int(self.get(key)) |
||||
|
||||
def get_bool(self, key: str) -> bool: |
||||
"""Get value and covert it to boolean.""" |
||||
val = self.get(key) |
||||
if isinstance(val, str): |
||||
return val.lower() in {"true", "t"} |
||||
elif isinstance(val, int): |
||||
return val != 0 |
||||
else: |
||||
return val |
||||
|
||||
def to_string(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() |
@ -0,0 +1,34 @@
|
||||
# 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. |
||||
|
||||
"""Testing util about file operating.""" |
||||
|
||||
from pathlib import Path |
||||
from typing import Union |
||||
|
||||
|
||||
def get_file_content(path: Union[str, Path]) -> str: |
||||
"""Get file content in given path.""" |
||||
with open(path, mode="r") as f: |
||||
return f.read() |
||||
|
||||
|
||||
def delete_file(path: Union[str, Path]) -> None: |
||||
"""Delete file in given path.""" |
||||
path = Path(path).expanduser() if isinstance(path, str) else path.expanduser() |
||||
if path.exists(): |
||||
path.unlink() |
@ -0,0 +1,85 @@
|
||||
# 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. |
||||
|
||||
"""Test file utils.""" |
||||
|
||||
import shutil |
||||
from pathlib import Path |
||||
|
||||
import pytest |
||||
|
||||
from pydolphinscheduler.utils import file |
||||
from tests.testing.file import delete_file, get_file_content |
||||
|
||||
content = "test_content" |
||||
file_path = "/tmp/test/file/test_file_write.txt" |
||||
|
||||
|
||||
@pytest.fixture |
||||
def teardown_del_file(): |
||||
"""Teardown about delete file.""" |
||||
yield |
||||
delete_file(file_path) |
||||
|
||||
|
||||
@pytest.fixture |
||||
def setup_crt_first(): |
||||
"""Set up and teardown about create file first and then delete it.""" |
||||
file.write(content=content, to_path=file_path) |
||||
yield |
||||
delete_file(file_path) |
||||
|
||||
|
||||
def test_write_content(teardown_del_file): |
||||
"""Test function :func:`write` on write behavior with correct content.""" |
||||
assert not Path(file_path).exists() |
||||
file.write(content=content, to_path=file_path) |
||||
assert Path(file_path).exists() |
||||
assert content == get_file_content(file_path) |
||||
|
||||
|
||||
def test_write_not_create_parent(teardown_del_file): |
||||
"""Test function :func:`write` with parent not exists and do not create path.""" |
||||
file_test_dir = Path(file_path).parent |
||||
if file_test_dir.exists(): |
||||
shutil.rmtree(str(file_test_dir)) |
||||
assert not file_test_dir.exists() |
||||
with pytest.raises( |
||||
ValueError, |
||||
match="Parent directory do not exists and set param `create` to `False`", |
||||
): |
||||
file.write(content=content, to_path=file_path, create=False) |
||||
|
||||
|
||||
def test_write_overwrite(setup_crt_first): |
||||
"""Test success with file exists but set ``True`` to overwrite.""" |
||||
assert Path(file_path).exists() |
||||
|
||||
new_content = f"new_{content}" |
||||
file.write(content=new_content, to_path=file_path, overwrite=True) |
||||
assert new_content == get_file_content(file_path) |
||||
|
||||
|
||||
def test_write_overwrite_error(setup_crt_first): |
||||
"""Test error with file exists but set ``False`` to overwrite.""" |
||||
assert Path(file_path).exists() |
||||
|
||||
new_content = f"new_{content}" |
||||
with pytest.raises( |
||||
FileExistsError, match=".*already exists and you choose not overwrite mode\\." |
||||
): |
||||
file.write(content=new_content, to_path=file_path) |
@ -1,201 +0,0 @@
|
||||
# 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. |
||||
|
||||
"""Test utils.path_dict module.""" |
||||
|
||||
import copy |
||||
from typing import Dict, Tuple |
||||
|
||||
import pytest |
||||
|
||||
from pydolphinscheduler.utils.path_dict import PathDict |
||||
|
||||
src_dict_list = [ |
||||
# dict with one single level |
||||
{"a": 1}, |
||||
# dict with two levels, with same nested keys 'b' |
||||
{"a": 1, "b": 2, "c": {"d": 3}, "e": {"b": 4}}, |
||||
# dict with three levels, with same nested keys 'b' |
||||
{"a": 1, "b": 2, "c": {"d": 3}, "e": {"b": {"b": 4}, "f": 5}}, |
||||
# dict with specific key container |
||||
{ |
||||
"a": 1, |
||||
"a-b": 2, |
||||
}, |
||||
] |
||||
|
||||
|
||||
@pytest.mark.parametrize("org", src_dict_list) |
||||
def test_val_between_dict_and_path_dict(org: Dict): |
||||
"""Test path dict equal to original dict.""" |
||||
path_dict = PathDict(org) |
||||
assert org == dict(path_dict) |
||||
|
||||
|
||||
def test_path_dict_basic_attr_access(): |
||||
"""Test basic behavior of path dict. |
||||
|
||||
Including add by attribute, with simple, nested dict, and specific key dict. |
||||
""" |
||||
expect = copy.deepcopy(src_dict_list[2]) |
||||
path_dict = PathDict(expect) |
||||
|
||||
# Add node with one level |
||||
val = 3 |
||||
path_dict.f = val |
||||
expect.update({"f": val}) |
||||
assert expect == path_dict |
||||
|
||||
# Add node with multiple level |
||||
val = {"abc": 123} |
||||
path_dict.e.g = val |
||||
expect.update({"e": {"b": {"b": 4}, "f": 5, "g": val}}) |
||||
assert expect == path_dict |
||||
|
||||
# Specific key |
||||
expect = copy.deepcopy(src_dict_list[3]) |
||||
path_dict = PathDict(expect) |
||||
assert 1 == path_dict.a |
||||
assert 2 == getattr(path_dict, "a-b") |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"org, exists, not_exists", |
||||
[ |
||||
( |
||||
src_dict_list[0], |
||||
("a"), |
||||
("b", "a.b"), |
||||
), |
||||
( |
||||
src_dict_list[1], |
||||
("a", "b", "c", "e", "c.d", "e.b"), |
||||
("a.b", "c.e", "b.c", "b.e"), |
||||
), |
||||
( |
||||
src_dict_list[2], |
||||
("a", "b", "c", "e", "c.d", "e.b", "e.b.b", "e.b.b", "e.f"), |
||||
("a.b", "c.e", "b.c", "b.e", "b.b.f", "b.f"), |
||||
), |
||||
], |
||||
) |
||||
def test_path_dict_attr(org: Dict, exists: Tuple, not_exists: Tuple): |
||||
"""Test properties' integrity of path dict.""" |
||||
path_dict = PathDict(org) |
||||
assert all([hasattr(path_dict, path) for path in exists]) |
||||
# assert not any([hasattr(path_dict, path) for path in not_exists]) |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"org, path_get", |
||||
[ |
||||
( |
||||
src_dict_list[0], |
||||
{"a": 1}, |
||||
), |
||||
( |
||||
src_dict_list[1], |
||||
{ |
||||
"a": 1, |
||||
"b": 2, |
||||
"c": {"d": 3}, |
||||
"c.d": 3, |
||||
"e": {"b": 4}, |
||||
"e.b": 4, |
||||
}, |
||||
), |
||||
( |
||||
src_dict_list[2], |
||||
{ |
||||
"a": 1, |
||||
"b": 2, |
||||
"c": {"d": 3}, |
||||
"c.d": 3, |
||||
"e": {"b": {"b": 4}, "f": 5}, |
||||
"e.b": {"b": 4}, |
||||
"e.b.b": 4, |
||||
"e.f": 5, |
||||
}, |
||||
), |
||||
], |
||||
) |
||||
def test_path_dict_get(org: Dict, path_get: Dict): |
||||
"""Test path dict getter function.""" |
||||
path_dict = PathDict(org) |
||||
assert all([path_get[path] == path_dict.__getattr__(path) for path in path_get]) |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"org, path_set, expect", |
||||
[ |
||||
# Add not exists node |
||||
( |
||||
src_dict_list[0], |
||||
{"b": 2}, |
||||
{ |
||||
"a": 1, |
||||
"b": 2, |
||||
}, |
||||
), |
||||
# Overwrite exists node with different type of value |
||||
( |
||||
src_dict_list[0], |
||||
{"a": "b"}, |
||||
{"a": "b"}, |
||||
), |
||||
# Add multiple not exists node with variable types of value |
||||
( |
||||
src_dict_list[0], |
||||
{ |
||||
"b.c.d": 123, |
||||
"b.c.e": "a", |
||||
"b.f": {"g": 23, "h": "bc", "i": {"j": "k"}}, |
||||
}, |
||||
{ |
||||
"a": 1, |
||||
"b": { |
||||
"c": { |
||||
"d": 123, |
||||
"e": "a", |
||||
}, |
||||
"f": {"g": 23, "h": "bc", "i": {"j": "k"}}, |
||||
}, |
||||
}, |
||||
), |
||||
# Test complex original data |
||||
( |
||||
src_dict_list[2], |
||||
{ |
||||
"g": 12, |
||||
"c.h": 34, |
||||
}, |
||||
{ |
||||
"a": 1, |
||||
"b": 2, |
||||
"g": 12, |
||||
"c": {"d": 3, "h": 34}, |
||||
"e": {"b": {"b": 4}, "f": 5}, |
||||
}, |
||||
), |
||||
], |
||||
) |
||||
def test_path_dict_set(org: Dict, path_set: Dict, expect: Dict): |
||||
"""Test path dict setter function.""" |
||||
path_dict = PathDict(org) |
||||
for path in path_set: |
||||
path_dict.__setattr__(path, path_set[path]) |
||||
assert expect == path_dict |
@ -0,0 +1,272 @@
|
||||
# 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. |
||||
|
||||
"""Test utils.path_dict module.""" |
||||
|
||||
from typing import Dict |
||||
|
||||
import pytest |
||||
from ruamel.yaml import YAML |
||||
|
||||
from pydolphinscheduler.utils.yaml_parser import YamlParser |
||||
from tests.testing.path import path_default_config_yaml |
||||
|
||||
yaml = YAML() |
||||
|
||||
expects = [ |
||||
{ |
||||
# yaml.load("no need test") is a flag about skipping it because it to different to maintainer |
||||
"name": yaml.load("no need test"), |
||||
"name.family": ("Smith", "SmithEdit"), |
||||
"name.given": ("Alice", "AliceEdit"), |
||||
"name.mark": yaml.load("no need test"), |
||||
"name.mark.name_mark": yaml.load("no need test"), |
||||
"name.mark.name_mark.key": ("value", "valueEdit"), |
||||
}, |
||||
{ |
||||
# yaml.load("no need test") is a flag about skipping it because it to different to maintainer |
||||
"java_gateway": yaml.load("no need test"), |
||||
"java_gateway.address": ("127.0.0.1", "127.1.1.1"), |
||||
"java_gateway.port": (25333, 25555), |
||||
"java_gateway.auto_convert": (True, False), |
||||
"default": yaml.load("no need test"), |
||||
"default.user": yaml.load("no need test"), |
||||
"default.user.name": ("userPythonGateway", "userPythonGatewayEdit"), |
||||
"default.user.password": ("userPythonGateway", "userPythonGatewayEdit"), |
||||
"default.user.email": ( |
||||
"userPythonGateway@dolphinscheduler.com", |
||||
"userEdit@dolphinscheduler.com", |
||||
), |
||||
"default.user.tenant": ("tenant_pydolphin", "tenant_pydolphinEdit"), |
||||
"default.user.phone": (11111111111, 22222222222), |
||||
"default.user.state": (1, 0), |
||||
"default.workflow": yaml.load("no need test"), |
||||
"default.workflow.project": ("project-pydolphin", "project-pydolphinEdit"), |
||||
"default.workflow.tenant": ("tenant_pydolphin", "SmithEdit"), |
||||
"default.workflow.user": ("userPythonGateway", "SmithEdit"), |
||||
"default.workflow.queue": ("queuePythonGateway", "SmithEdit"), |
||||
"default.workflow.worker_group": ("default", "SmithEdit"), |
||||
"default.workflow.time_zone": ("Asia/Shanghai", "SmithEdit"), |
||||
}, |
||||
] |
||||
|
||||
param = [ |
||||
"""#example |
||||
name: |
||||
# details |
||||
family: Smith # very common |
||||
given: Alice # one of the siblings |
||||
mark: |
||||
name_mark: |
||||
key: value |
||||
""" |
||||
] |
||||
|
||||
with open(path_default_config_yaml, "r") as f: |
||||
param.append(f.read()) |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, delimiter, expect", |
||||
[ |
||||
( |
||||
param[0], |
||||
"|", |
||||
expects[0], |
||||
), |
||||
( |
||||
param[1], |
||||
"/", |
||||
expects[1], |
||||
), |
||||
], |
||||
) |
||||
def test_yaml_parser_specific_delimiter(src: str, delimiter: str, expect: Dict): |
||||
"""Test specific delimiter for :class:`YamlParser`.""" |
||||
|
||||
def ch_dl(key): |
||||
return key.replace(".", delimiter) |
||||
|
||||
yaml_parser = YamlParser(src, delimiter=delimiter) |
||||
assert all( |
||||
[ |
||||
expect[key][0] == yaml_parser[ch_dl(key)] |
||||
for key in expect |
||||
if expect[key] != "no need test" |
||||
] |
||||
) |
||||
assert all( |
||||
[ |
||||
expect[key][0] == yaml_parser.get(ch_dl(key)) |
||||
for key in expect |
||||
if expect[key] != "no need test" |
||||
] |
||||
) |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, expect", |
||||
[ |
||||
( |
||||
param[0], |
||||
expects[0], |
||||
), |
||||
( |
||||
param[1], |
||||
expects[1], |
||||
), |
||||
], |
||||
) |
||||
def test_yaml_parser_contains(src: str, expect: Dict): |
||||
"""Test magic function :func:`YamlParser.__contain__` also with `key in obj` syntax.""" |
||||
yaml_parser = YamlParser(src) |
||||
assert len(expect.keys()) == len( |
||||
yaml_parser.dict_parser.keys() |
||||
), "Parser keys length not equal to expect keys length" |
||||
assert all( |
||||
[key in yaml_parser for key in expect] |
||||
), "Parser keys not equal to expect keys" |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, expect", |
||||
[ |
||||
( |
||||
param[0], |
||||
expects[0], |
||||
), |
||||
( |
||||
param[1], |
||||
expects[1], |
||||
), |
||||
], |
||||
) |
||||
def test_yaml_parser_get(src: str, expect: Dict): |
||||
"""Test magic function :func:`YamlParser.__getitem__` also with `obj[key]` syntax.""" |
||||
yaml_parser = YamlParser(src) |
||||
assert all( |
||||
[ |
||||
expect[key][0] == yaml_parser[key] |
||||
for key in expect |
||||
if expect[key] != "no need test" |
||||
] |
||||
) |
||||
assert all( |
||||
[ |
||||
expect[key][0] == yaml_parser.get(key) |
||||
for key in expect |
||||
if expect[key] != "no need test" |
||||
] |
||||
) |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, expect", |
||||
[ |
||||
( |
||||
param[0], |
||||
expects[0], |
||||
), |
||||
( |
||||
param[1], |
||||
expects[1], |
||||
), |
||||
], |
||||
) |
||||
def test_yaml_parser_set(src: str, expect: Dict): |
||||
"""Test magic function :func:`YamlParser.__setitem__` also with `obj[key] = val` syntax.""" |
||||
yaml_parser = YamlParser(src) |
||||
for key in expect: |
||||
assert key in yaml_parser.dict_parser.keys() |
||||
if expect[key] == "no need test": |
||||
continue |
||||
assert expect[key][0] == yaml_parser.dict_parser[key] |
||||
assert expect[key][1] != yaml_parser.dict_parser[key] |
||||
|
||||
yaml_parser[key] = expect[key][1] |
||||
assert expect[key][0] != yaml_parser.dict_parser[key] |
||||
assert expect[key][1] == yaml_parser.dict_parser[key] |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, setter, expect", |
||||
[ |
||||
( |
||||
param[0], |
||||
{"name.mark.name_mark.key": "edit"}, |
||||
"""#example |
||||
name: |
||||
# details |
||||
family: Smith # very common |
||||
given: Alice # one of the siblings |
||||
mark: |
||||
name_mark: |
||||
key: edit |
||||
""", |
||||
), |
||||
( |
||||
param[0], |
||||
{ |
||||
"name.family": "SmithEdit", |
||||
"name.given": "AliceEdit", |
||||
"name.mark.name_mark.key": "edit", |
||||
}, |
||||
"""#example |
||||
name: |
||||
# details |
||||
family: SmithEdit # very common |
||||
given: AliceEdit # one of the siblings |
||||
mark: |
||||
name_mark: |
||||
key: edit |
||||
""", |
||||
), |
||||
], |
||||
) |
||||
def test_yaml_parser_to_string(src: str, setter: Dict, expect: str): |
||||
"""Test function :func:`YamlParser.to_string`.""" |
||||
yaml_parser = YamlParser(src) |
||||
for key, val in setter.items(): |
||||
yaml_parser[key] = val |
||||
|
||||
assert expect == yaml_parser.to_string() |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, key, expect", |
||||
[ |
||||
(param[1], "java_gateway.port", 25333), |
||||
(param[1], "default.user.phone", 11111111111), |
||||
(param[1], "default.user.state", 1), |
||||
], |
||||
) |
||||
def test_yaml_parser_get_int(src: str, key: str, expect: int): |
||||
"""Test function :func:`YamlParser.get_int`.""" |
||||
yaml_parser = YamlParser(src) |
||||
assert expect == yaml_parser.get_int(key) |
||||
|
||||
|
||||
@pytest.mark.parametrize( |
||||
"src, key, expect", |
||||
[ |
||||
(param[1], "java_gateway.auto_convert", True), |
||||
], |
||||
) |
||||
def test_yaml_parser_get_bool(src: str, key: str, expect: bool): |
||||
"""Test function :func:`YamlParser.get_bool`.""" |
||||
yaml_parser = YamlParser(src) |
||||
assert expect == yaml_parser.get_bool(key) |
Loading…
Reference in new issue