This is an automated email from the ASF dual-hosted git repository.

caishunfeng pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 5c64078  [python] refactor yaml file parser (#8701)
5c64078 is described below

commit 5c640789c3dacac3fee3555ad601ac09d6bee099
Author: Jiajie Zhong <[email protected]>
AuthorDate: Mon Mar 7 11:42:28 2022 +0800

    [python] refactor yaml file parser (#8701)
    
    * [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 regression
---
 .../pydolphinscheduler/UPDATING.md                 |  27 ++
 .../pydolphinscheduler/setup.py                    |   2 +-
 .../src/pydolphinscheduler/core/configuration.py   | 166 ++++++-------
 .../pydolphinscheduler/core/default_config.yaml    |  12 +-
 .../src/pydolphinscheduler/utils/file.py           |  57 +++++
 .../src/pydolphinscheduler/utils/path_dict.py      |  85 -------
 .../src/pydolphinscheduler/utils/yaml_parser.py    | 169 +++++++++++++
 .../pydolphinscheduler/tests/cli/test_config.py    |  67 +++--
 .../tests/core/test_configuration.py               | 161 ++++++++++--
 .../tests/core/test_default_config_yaml.py         |  16 +-
 .../pydolphinscheduler/tests/testing/constants.py  |   3 +
 .../tests/testing/{constants.py => file.py}        |  30 ++-
 .../pydolphinscheduler/tests/utils/test_file.py    |  85 +++++++
 .../tests/utils/test_path_dict.py                  | 201 ---------------
 .../tests/utils/test_yaml_parser.py                | 272 +++++++++++++++++++++
 15 files changed, 905 insertions(+), 448 deletions(-)

diff --git a/dolphinscheduler-python/pydolphinscheduler/UPDATING.md 
b/dolphinscheduler-python/pydolphinscheduler/UPDATING.md
new file mode 100644
index 0000000..9c5cc42
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/UPDATING.md
@@ -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.
diff --git a/dolphinscheduler-python/pydolphinscheduler/setup.py 
b/dolphinscheduler-python/pydolphinscheduler/setup.py
index fa46787..7b5cda8 100644
--- a/dolphinscheduler-python/pydolphinscheduler/setup.py
+++ b/dolphinscheduler-python/pydolphinscheduler/setup.py
@@ -38,7 +38,7 @@ version = "2.0.4"
 prod = [
     "click>=8.0.0",
     "py4j~=0.10",
-    "pyyaml",
+    "ruamel.yaml",
 ]
 
 build = [
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
index ec45876..e8d6605 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
@@ -16,137 +16,133 @@
 # under the License.
 
 """Configuration module for pydolphinscheduler."""
-
-import copy
 import os
 from pathlib import Path
-from typing import Any, Dict
-
-import yaml
+from typing import Any
 
-from pydolphinscheduler.exceptions import PyDSConfException, PyDSParamException
-from pydolphinscheduler.utils.path_dict import PathDict
+from pydolphinscheduler.exceptions import PyDSConfException
+from pydolphinscheduler.utils import file
+from pydolphinscheduler.utils.yaml_parser import YamlParser
 
-DEFAULT_CONFIG_PATH = 
Path(__file__).resolve().parent.joinpath("default_config.yaml")
+BUILD_IN_CONFIG_PATH = 
Path(__file__).resolve().parent.joinpath("default_config.yaml")
 
 
-def get_config_file_path() -> Path:
+def config_path() -> Path:
     """Get the path of pydolphinscheduler configuration file."""
     pyds_home = os.environ.get("PYDOLPHINSCHEDULER_HOME", 
"~/pydolphinscheduler")
     config_file_path = Path(pyds_home).joinpath("config.yaml").expanduser()
     return config_file_path
 
 
-def read_yaml(path: str) -> Dict:
-    """Read configs dict from configuration file.
+def get_configs() -> YamlParser:
+    """Get all configuration settings from configuration file.
 
-    :param path: The path of configuration file.
+    Will use custom configuration file first if it exists, otherwise default 
configuration file in
+    default path.
     """
-    with open(path, "r") as f:
-        return yaml.safe_load(f)
-
-
-def write_yaml(context: Dict, path: str) -> None:
-    """Write configs dict to configuration file.
-
-    :param context: The configs dict write to configuration file.
-    :param path: The path of configuration file.
-    """
-    parent = Path(path).parent
-    if not parent.exists():
-        parent.mkdir(parents=True)
-    with open(path, mode="w") as f:
-        f.write(yaml.dump(context))
-
-
-def default_yaml_config() -> Dict:
-    """Get default configs in ``DEFAULT_CONFIG_PATH``."""
-    with open(DEFAULT_CONFIG_PATH, "r") as f:
-        return yaml.safe_load(f)
-
-
-def _whether_exists_config() -> bool:
-    """Check whether config file already exists in 
:func:`get_config_file_path`."""
-    return True if get_config_file_path().exists() else False
-
-
-def get_all_configs():
-    """Get all configs from configuration file."""
-    exists = _whether_exists_config()
-    if exists:
-        return read_yaml(str(get_config_file_path()))
-    else:
-        return default_yaml_config()
-
-
-# Add configs as module variables to avoid read configuration multiple times 
when
-#  Get common configuration setting
-#  Set or get multiple configs in single time
-configs = get_all_configs()
+    path = str(config_path()) if config_path().exists() else 
BUILD_IN_CONFIG_PATH
+    with open(path, mode="r") as f:
+        return YamlParser(f.read())
 
 
 def init_config_file() -> None:
-    """Initialize configuration file to :func:`get_config_file_path`."""
-    if _whether_exists_config():
+    """Initialize configuration file by default configs."""
+    if config_path().exists():
         raise PyDSConfException(
             "Initialize configuration false to avoid overwrite configure by 
accident, file already exists "
             "in %s, if you wan to overwrite the exists configure please remove 
the exists file manually.",
-            str(get_config_file_path()),
+            str(config_path()),
         )
-    write_yaml(context=default_yaml_config(), path=str(get_config_file_path()))
+    file.write(content=get_configs().to_string(), to_path=str(config_path()))
 
 
 def get_single_config(key: str) -> Any:
     """Get single config to configuration file.
 
-    :param key: The config path want get.
+    Support get from nested keys by delimiter ``.``.
+
+    For example, yaml config as below:
+
+    .. code-block:: yaml
+
+        one:
+          two1:
+            three: value1
+          two2: value2
+
+    you could get ``value1`` and ``value2`` by nested path
+
+    .. code-block:: python
+
+        value1 = get_single_config("one.two1.three")
+        value2 = get_single_config("one.two2")
+
+    :param key: The config key want to get it value.
     """
-    global configs
-    config_path_dict = PathDict(configs)
-    if key not in config_path_dict:
-        raise PyDSParamException(
+    config = get_configs()
+    if key not in config:
+        raise PyDSConfException(
             "Configuration path %s do not exists. Can not get configuration.", 
key
         )
-    return config_path_dict.__getattr__(key)
+    return config[key]
 
 
 def set_single_config(key: str, value: Any) -> None:
     """Change single config to configuration file.
 
-    :param key: The config path want change.
+    For example, yaml config as below:
+
+    .. code-block:: yaml
+
+        one:
+          two1:
+            three: value1
+          two2: value2
+
+    you could change ``value1`` to ``value3``, also change ``value2`` to 
``value4`` by nested path assigned
+
+    .. code-block:: python
+
+        set_single_config["one.two1.three"] = "value3"
+        set_single_config["one.two2"] = "value4"
+
+    :param key: The config key want change.
     :param value: The new value want to set.
     """
-    global configs
-    config_path_dict = PathDict(configs)
-    if key not in config_path_dict:
-        raise PyDSParamException(
+    config = get_configs()
+    if key not in config:
+        raise PyDSConfException(
             "Configuration path %s do not exists. Can not set configuration.", 
key
         )
-    config_path_dict.__setattr__(key, value)
-    write_yaml(context=dict(config_path_dict), 
path=str(get_config_file_path()))
+    config[key] = value
+    file.write(content=config.to_string(), to_path=str(config_path()), 
overwrite=True)
 
 
 # Start Common Configuration Settings
-path_configs = PathDict(copy.deepcopy(configs))
+
+# Add configs as module variables to avoid read configuration multiple times 
when
+#  Get common configuration setting
+#  Set or get multiple configs in single time
+configs: YamlParser = get_configs()
 
 # Java Gateway Settings
-JAVA_GATEWAY_ADDRESS = str(getattr(path_configs, "java_gateway.address"))
-JAVA_GATEWAY_PORT = str(getattr(path_configs, "java_gateway.port"))
-JAVA_GATEWAY_AUTO_CONVERT = str(getattr(path_configs, 
"java_gateway.auto_convert"))
+JAVA_GATEWAY_ADDRESS = configs.get("java_gateway.address")
+JAVA_GATEWAY_PORT = configs.get_int("java_gateway.port")
+JAVA_GATEWAY_AUTO_CONVERT = configs.get_bool("java_gateway.auto_convert")
 
 # User Settings
-USER_NAME = str(getattr(path_configs, "default.user.name"))
-USER_PASSWORD = str(getattr(path_configs, "default.user.password"))
-USER_EMAIL = str(getattr(path_configs, "default.user.email"))
-USER_PHONE = str(getattr(path_configs, "default.user.phone"))
-USER_STATE = str(getattr(path_configs, "default.user.state"))
+USER_NAME = configs.get("default.user.name")
+USER_PASSWORD = configs.get("default.user.password")
+USER_EMAIL = configs.get("default.user.email")
+USER_PHONE = configs.get("default.user.phone")
+USER_STATE = configs.get("default.user.state")
 
 # Workflow Settings
-WORKFLOW_PROJECT = str(getattr(path_configs, "default.workflow.project"))
-WORKFLOW_TENANT = str(getattr(path_configs, "default.workflow.tenant"))
-WORKFLOW_USER = str(getattr(path_configs, "default.workflow.user"))
-WORKFLOW_QUEUE = str(getattr(path_configs, "default.workflow.queue"))
-WORKFLOW_WORKER_GROUP = str(getattr(path_configs, 
"default.workflow.worker_group"))
-WORKFLOW_TIME_ZONE = str(getattr(path_configs, "default.workflow.time_zone"))
+WORKFLOW_PROJECT = configs.get("default.workflow.project")
+WORKFLOW_TENANT = configs.get("default.workflow.tenant")
+WORKFLOW_USER = configs.get("default.workflow.user")
+WORKFLOW_QUEUE = configs.get("default.workflow.queue")
+WORKFLOW_WORKER_GROUP = configs.get("default.workflow.worker_group")
+WORKFLOW_TIME_ZONE = configs.get("default.workflow.time_zone")
 
 # End Common Configuration Setting
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
index 45b1346..410f64d 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
@@ -15,25 +15,33 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# Setting about Java gateway server
 java_gateway:
   # The address of Python gateway server start. Set its value to `0.0.0.0` if 
your Python API run in different
   # between Python gateway server. It could be be specific to other address 
like `127.0.0.1` or `localhost`
   address: 127.0.0.1
+
   # The port of Python gateway server start. Define which port you could 
connect to Python gateway server from
   # Python API side.
   port: 25333
-  
+
+  # Whether automatically convert Python objects to Java Objects. Default 
value is ``True``. There is some
+  # performance lost when set to ``True`` but for now pydolphinscheduler do 
not handle the convert issue between
+  # java and Python, mark it as TODO item in the future.
   auto_convert: true
 
+# Setting about dolphinscheduler default value, will use the value set below 
if property do not set, which
+# including ``user``, ``workflow`` 
 default:
+  # Default value for dolphinscheduler's user object
   user:
     name: userPythonGateway
-    # TODO simple set password same as username
     password: userPythonGateway
     email: [email protected]
     tenant: tenant_pydolphin
     phone: 11111111111
     state: 1
+  # Default value for dolphinscheduler's workflow object
   workflow:
     project: project-pydolphin
     tenant: tenant_pydolphin
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/file.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/file.py
new file mode 100644
index 0000000..075b902
--- /dev/null
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/file.py
@@ -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
+        )
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/path_dict.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/path_dict.py
deleted file mode 100644
index cf836c9..0000000
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/path_dict.py
+++ /dev/null
@@ -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__
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/yaml_parser.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/yaml_parser.py
new file mode 100644
index 0000000..6d1e67e
--- /dev/null
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/yaml_parser.py
@@ -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()
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py
index 7d5f88b..f7c489a 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py
@@ -23,26 +23,24 @@ from pathlib import Path
 import pytest
 
 from pydolphinscheduler.cli.commands import cli
-from pydolphinscheduler.core.configuration import get_config_file_path
+from pydolphinscheduler.core.configuration import BUILD_IN_CONFIG_PATH, 
config_path
 from tests.testing.cli import CliTestWrapper
-from tests.testing.constants import DEV_MODE
+from tests.testing.constants import DEV_MODE, ENV_PYDS_HOME
+from tests.testing.file import get_file_content
 
-default_config_path = "~/pydolphinscheduler"
 config_file = "config.yaml"
 
 
 @pytest.fixture
-def delete_tmp_config_file():
-    """Util for deleting temp configuration file after test finish."""
+def teardown_file_env():
+    """Util for deleting temp configuration file and pop env var after test 
finish."""
     yield
-    config_file_path = get_config_file_path()
-    config_file_path.unlink()
+    config_file_path = config_path()
+    if config_file_path.exists():
+        config_file_path.unlink()
+    os.environ.pop(ENV_PYDS_HOME, None)
 
 
[email protected](
-    DEV_MODE,
-    reason="Avoid delete ~/pydolphinscheduler/config.yaml by accident when 
test locally.",
-)
 @pytest.mark.parametrize(
     "home",
     [
@@ -51,24 +49,23 @@ def delete_tmp_config_file():
         "/tmp/test_abc",
     ],
 )
-def test_config_init(delete_tmp_config_file, home):
+def test_config_init(teardown_file_env, home):
     """Test command line interface `config --init`."""
     if home:
-        os.environ["PYDOLPHINSCHEDULER_HOME"] = home
-        config_path = home
-    else:
-        config_path = default_config_path
+        os.environ[ENV_PYDS_HOME] = home
+    elif DEV_MODE:
+        pytest.skip(
+            "Avoid delete ~/pydolphinscheduler/config.yaml by accident when 
test locally."
+        )
 
-    path = Path(config_path).joinpath(config_file).expanduser()
-    assert not path.exists()
+    config_file_path = config_path()
+    assert not config_file_path.exists()
 
     cli_test = CliTestWrapper(cli, ["config", "--init"])
     cli_test.assert_success()
 
-    assert path.exists()
-    # TODO We have a bug here, yaml dump do not support comment
-    # with path.open(mode="r") as cli_crt, open(path_default_config_yaml, "r") 
as src:
-    #     assert src.read() == cli_crt.read()
+    assert config_file_path.exists()
+    assert get_file_content(config_file_path) == 
get_file_content(BUILD_IN_CONFIG_PATH)
 
 
 @pytest.mark.parametrize(
@@ -80,9 +77,9 @@ def test_config_init(delete_tmp_config_file, home):
         ("default.workflow.project", "project-pydolphin"),
     ],
 )
-def test_config_get(delete_tmp_config_file, key: str, expect: str):
+def test_config_get(teardown_file_env, key: str, expect: str):
     """Test command line interface `config --get XXX`."""
-    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    os.environ[ENV_PYDS_HOME] = "/tmp/pydolphinscheduler"
     cli_test = CliTestWrapper(cli, ["config", "--init"])
     cli_test.assert_success()
 
@@ -109,9 +106,9 @@ def test_config_get(delete_tmp_config_file, key: str, 
expect: str):
         ),
     ],
 )
-def test_config_get_multiple(delete_tmp_config_file, keys: str, expects: str):
+def test_config_get_multiple(teardown_file_env, keys: str, expects: str):
     """Test command line interface `config --get KEY1 --get KEY2 ...`."""
-    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    os.environ[ENV_PYDS_HOME] = "/tmp/pydolphinscheduler"
     cli_test = CliTestWrapper(cli, ["config", "--init"])
     cli_test.assert_success()
 
@@ -125,8 +122,6 @@ def test_config_get_multiple(delete_tmp_config_file, keys: 
str, expects: str):
         cli_test.assert_success(output=f"{keys[idx]} = {expect}", fuzzy=True)
 
 
-# TODO fix command `config --set KEY VAL`
[email protected](reason="We still have bug in command `config --set KEY VAL`")
 @pytest.mark.parametrize(
     "key, value",
     [
@@ -136,9 +131,11 @@ def test_config_get_multiple(delete_tmp_config_file, keys: 
str, expects: str):
         ("default.workflow.project", "edit-project-pydolphin"),
     ],
 )
-def test_config_set(delete_tmp_config_file, key: str, value: str):
+def test_config_set(teardown_file_env, key: str, value: str):
     """Test command line interface `config --set KEY VALUE`."""
-    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    path = "/tmp/pydolphinscheduler"
+    assert not Path(path).joinpath(config_file).exists()
+    os.environ[ENV_PYDS_HOME] = path
     cli_test = CliTestWrapper(cli, ["config", "--init"])
     cli_test.assert_success()
 
@@ -153,10 +150,6 @@ def test_config_set(delete_tmp_config_file, key: str, 
value: str):
     assert f"{key} = {value}" in cli_test.result.output
 
 
-# TODO do not skip `config --set KEY1 VAL1 --set KEY2 VAL2`
[email protected](
-    reason="We still have bug in command `config --set KEY1 VAL1 --set KEY2 
VAL2`"
-)
 @pytest.mark.parametrize(
     "keys, values",
     [
@@ -176,9 +169,11 @@ def test_config_set(delete_tmp_config_file, key: str, 
value: str):
         ),
     ],
 )
-def test_config_set_multiple(delete_tmp_config_file, keys: str, values: str):
+def test_config_set_multiple(teardown_file_env, keys: str, values: str):
     """Test command line interface `config --set KEY1 VAL1 --set KEY2 VAL2`."""
-    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    path = "/tmp/pydolphinscheduler"
+    assert not Path(path).joinpath(config_file).exists()
+    os.environ[ENV_PYDS_HOME] = path
     cli_test = CliTestWrapper(cli, ["config", "--init"])
     cli_test.assert_success()
 
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py
index b055cd1..a0704c9 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py
@@ -19,27 +19,160 @@
 
 import os
 from pathlib import Path
+from typing import Any
 
 import pytest
 
 from pydolphinscheduler.core import configuration
+from pydolphinscheduler.core.configuration import (
+    BUILD_IN_CONFIG_PATH,
+    config_path,
+    get_single_config,
+    set_single_config,
+)
+from pydolphinscheduler.exceptions import PyDSConfException
+from pydolphinscheduler.utils.yaml_parser import YamlParser
+from tests.testing.constants import DEV_MODE, ENV_PYDS_HOME
+from tests.testing.file import get_file_content
+
+
[email protected]
+def teardown_file_env():
+    """Util for deleting temp configuration file and pop env var after test 
finish."""
+    yield
+    config_file_path = config_path()
+    if config_file_path.exists():
+        config_file_path.unlink()
+    os.environ.pop(ENV_PYDS_HOME, None)
+
+
[email protected](
+    "home, expect",
+    [
+        (None, "~/pydolphinscheduler/config.yaml"),
+        ("/tmp/pydolphinscheduler", "/tmp/pydolphinscheduler/config.yaml"),
+        ("/tmp/test_abc", "/tmp/test_abc/config.yaml"),
+    ],
+)
+def test_config_path(home: Any, expect: str):
+    """Test function :func:`config_path`."""
+    if home:
+        os.environ[ENV_PYDS_HOME] = home
+    assert Path(expect).expanduser() == configuration.config_path()
+
+
[email protected](
+    "home",
+    [
+        None,
+        "/tmp/pydolphinscheduler",
+        "/tmp/test_abc",
+    ],
+)
+def test_init_config_file(teardown_file_env, home: Any):
+    """Test init config file."""
+    if home:
+        os.environ[ENV_PYDS_HOME] = home
+    elif DEV_MODE:
+        pytest.skip(
+            "Avoid delete ~/pydolphinscheduler/config.yaml by accident when 
test locally."
+        )
+    assert not config_path().exists()
+    configuration.init_config_file()
+    assert config_path().exists()
+
+    assert get_file_content(config_path()) == 
get_file_content(BUILD_IN_CONFIG_PATH)
+
+
[email protected](
+    "home",
+    [
+        None,
+        "/tmp/pydolphinscheduler",
+        "/tmp/test_abc",
+    ],
+)
+def test_init_config_file_duplicate(teardown_file_env, home: Any):
+    """Test raise error with init config file which already exists."""
+    if home:
+        os.environ[ENV_PYDS_HOME] = home
+    elif DEV_MODE:
+        pytest.skip(
+            "Avoid delete ~/pydolphinscheduler/config.yaml by accident when 
test locally."
+        )
+    assert not config_path().exists()
+    configuration.init_config_file()
+    assert config_path().exists()
+
+    with pytest.raises(PyDSConfException, match=".*file already exists.*"):
+        configuration.init_config_file()
+
+
+def test_get_configs_build_in():
+    """Test function :func:`get_configs` with build-in config file."""
+    content = get_file_content(BUILD_IN_CONFIG_PATH)
+    assert YamlParser(content).src_parser == 
configuration.get_configs().src_parser
+    assert YamlParser(content).dict_parser == 
configuration.get_configs().dict_parser
+
+
[email protected](
+    "key, val, new_val",
+    [
+        ("java_gateway.address", "127.0.0.1", "127.1.1.1"),
+        ("java_gateway.port", 25333, 25555),
+        ("java_gateway.auto_convert", True, False),
+        ("default.user.name", "userPythonGateway", "editUserPythonGateway"),
+        ("default.user.password", "userPythonGateway", 
"editUserPythonGateway"),
+        (
+            "default.user.email",
+            "[email protected]",
+            "[email protected]",
+        ),
+        ("default.user.phone", 11111111111, 22222222222),
+        ("default.user.state", 1, 0),
+        ("default.workflow.project", "project-pydolphin", 
"eidt-project-pydolphin"),
+        ("default.workflow.tenant", "tenant_pydolphin", 
"edit_tenant_pydolphin"),
+        ("default.workflow.user", "userPythonGateway", 
"editUserPythonGateway"),
+        ("default.workflow.queue", "queuePythonGateway", 
"editQueuePythonGateway"),
+        ("default.workflow.worker_group", "default", "specific"),
+        ("default.workflow.time_zone", "Asia/Shanghai", "Asia/Beijing"),
+    ],
+)
+def test_single_config_get_set(teardown_file_env, key: str, val: Any, new_val: 
Any):
+    """Test function :func:`get_single_config` and 
:func:`set_single_config`."""
+    assert val == get_single_config(key)
+    set_single_config(key, new_val)
+    assert new_val == get_single_config(key)
+
+
+def test_single_config_get_set_not_exists_key():
+    """Test function :func:`get_single_config` and :func:`set_single_config` 
error while key not exists."""
+    not_exists_key = "i_am_not_exists_key"
+    with pytest.raises(PyDSConfException, match=".*do not exists.*"):
+        get_single_config(not_exists_key)
+    with pytest.raises(PyDSConfException, match=".*do not exists.*"):
+        set_single_config(not_exists_key, not_exists_key)
 
 
 @pytest.mark.parametrize(
-    "env, expect",
+    "config_name, expect",
     [
-        (None, "~/pydolphinscheduler"),
-        ("/tmp/pydolphinscheduler", "/tmp/pydolphinscheduler"),
-        ("/tmp/test_abc", "/tmp/test_abc"),
+        ("JAVA_GATEWAY_ADDRESS", "127.0.0.1"),
+        ("JAVA_GATEWAY_PORT", 25333),
+        ("JAVA_GATEWAY_AUTO_CONVERT", True),
+        ("USER_NAME", "userPythonGateway"),
+        ("USER_PASSWORD", "userPythonGateway"),
+        ("USER_EMAIL", "[email protected]"),
+        ("USER_PHONE", 11111111111),
+        ("USER_STATE", 1),
+        ("WORKFLOW_PROJECT", "project-pydolphin"),
+        ("WORKFLOW_TENANT", "tenant_pydolphin"),
+        ("WORKFLOW_USER", "userPythonGateway"),
+        ("WORKFLOW_QUEUE", "queuePythonGateway"),
+        ("WORKFLOW_WORKER_GROUP", "default"),
+        ("WORKFLOW_TIME_ZONE", "Asia/Shanghai"),
     ],
 )
-def test_get_config_file_path(env, expect):
-    """Test get config file path method."""
-    # Avoid env setting by other tests
-    os.environ.pop("PYDOLPHINSCHEDULER_HOME", None)
-    if env:
-        os.environ["PYDOLPHINSCHEDULER_HOME"] = env
-    assert (
-        Path(expect).joinpath("config.yaml").expanduser()
-        == configuration.get_config_file_path()
-    )
+def test_get_configuration(config_name: str, expect: Any):
+    """Test get exists attribute in :mod:`configuration`."""
+    assert expect == getattr(configuration, config_name)
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
index 050cc52..b4d5e07 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
@@ -17,23 +17,23 @@
 
 """Test default config file."""
 
-from typing import Dict
-
-import yaml
+from ruamel.yaml import YAML
+from ruamel.yaml.comments import CommentedMap
 
 from tests.testing.path import path_default_config_yaml
 
 
-def nested_key_check(test_dict: Dict) -> None:
+def nested_key_check(comment_map: CommentedMap) -> None:
     """Test whether default configuration file exists specific character."""
-    for key, val in test_dict.items():
+    for key, val in comment_map.items():
         assert "." not in key, f"There is not allowed special character in key 
`{key}`."
-        if isinstance(val, dict):
+        if isinstance(val, CommentedMap):
             nested_key_check(val)
 
 
 def test_key_without_dot_delimiter():
     """Test wrapper of whether default configuration file exists specific 
character."""
+    yaml = YAML()
     with open(path_default_config_yaml, "r") as f:
-        default_config = yaml.safe_load(f)
-        nested_key_check(default_config)
+        comment_map = yaml.load(f.read())
+        nested_key_check(comment_map)
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
index dcc32a6..7e214ff 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
@@ -29,6 +29,9 @@ task_without_example = {
     "procedure",
 }
 
+# pydolphinscheduler environment home
+ENV_PYDS_HOME = "PYDOLPHINSCHEDULER_HOME"
+
 # whether in dev mode, if true we will add or remove some tests. Or make be 
and more detail infos when
 # test failed.
 DEV_MODE = str(
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/testing/file.py
similarity index 58%
copy from dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
copy to dolphinscheduler-python/pydolphinscheduler/tests/testing/file.py
index dcc32a6..82e0837 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/file.py
@@ -15,22 +15,20 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""Constants variables for test module."""
+"""Testing util about file operating."""
 
-import os
+from pathlib import Path
+from typing import Union
 
-# Record some task without example in directory `example`. Some of them maybe 
can not write example,
-# but most of them just without adding by mistake, and we should add it later.
-task_without_example = {
-    "sql",
-    "http",
-    "sub_process",
-    "python",
-    "procedure",
-}
 
-# whether in dev mode, if true we will add or remove some tests. Or make be 
and more detail infos when
-# test failed.
-DEV_MODE = str(
-    os.environ.get("PY_DOLPHINSCHEDULER_DEV_MODE", False)
-).strip().lower() in {"true", "t", "1"}
+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()
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_file.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_file.py
new file mode 100644
index 0000000..4cc6df4
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_file.py
@@ -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"
+
+
[email protected]
+def teardown_del_file():
+    """Teardown about delete file."""
+    yield
+    delete_file(file_path)
+
+
[email protected]
+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)
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_path_dict.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_path_dict.py
deleted file mode 100644
index 92e4b2f..0000000
--- a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_path_dict.py
+++ /dev/null
@@ -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,
-    },
-]
-
-
[email protected]("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")
-
-
[email protected](
-    "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])
-
-
[email protected](
-    "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])
-
-
[email protected](
-    "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
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_yaml_parser.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_yaml_parser.py
new file mode 100644
index 0000000..2e3006c
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_yaml_parser.py
@@ -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": (
+            "[email protected]",
+            "[email protected]",
+        ),
+        "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())
+
+
[email protected](
+    "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"
+        ]
+    )
+
+
[email protected](
+    "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"
+
+
[email protected](
+    "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"
+        ]
+    )
+
+
[email protected](
+    "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]
+
+
[email protected](
+    "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()
+
+
[email protected](
+    "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)
+
+
[email protected](
+    "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)

Reply via email to