Skip to content

Commit

Permalink
Feature/Avaiga#27 Implement Config comparator (Avaiga#30)
Browse files Browse the repository at this point in the history
* feat: add _ConfigComparator

* feat: add deepdiff as required package
  • Loading branch information
trgiangdo authored Dec 30, 2022
1 parent 10b45ba commit 4453ba4
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 2 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name = "pypi"

[packages]
toml = "==0.10"
deepdiff = "==6.2.2"

[dev-packages]
black = "*"
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

requirements = [
"toml>=0.10,<0.11",
"deepdiff>=6.2,<6.3"
]

test_requirements = ["pytest>=3.8"]
Expand Down
112 changes: 112 additions & 0 deletions src/taipy/config/_config_comparator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Copyright 2022 Avaiga Private Limited
#
# Licensed 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.

import json
import re
from typing import Dict, List, Tuple

from deepdiff import DeepDiff

from ._base_serializer import _BaseSerializer
from ._config import _Config
from .config import Config


class _ConfigComparator:
@classmethod
def _compare(cls, old_conf: _Config, new_conf: _Config) -> Dict[str, List[Tuple[Tuple[str]]]]:
"""Compare between 2 _Config object to check for compatibility.
Return a dictionary with the following format:
{
"item_added": [
((entity_name_1, entity_id_1, attribute_1), added_object_1),
((entity_name_2, entity_id_2, attribute_2), added_object_2),
],
"item_changed": [
((entity_name_1, entity_id_1, attribute_1), (old_value_1, new_value_1)),
((entity_name_2, entity_id_2, attribute_2), (old_value_2, new_value_2)),
],
"item_removed": [
((entity_name_1, entity_id_1, attribute_1), removed_object_1),
((entity_name_2, entity_id_2, attribute_2), removed_object_2),
],
}
"""
old_conf_json = json.loads(Config._to_json(old_conf)) # type: ignore
new_conf_json = json.loads(Config._to_json(new_conf)) # type: ignore

deepdiff_result = DeepDiff(old_conf_json, new_conf_json)

config_diff: Dict[str, List] = {
"added_items": [],
"removed_items": [],
"modified_items": [],
}

if dictionary_item_added := deepdiff_result.get("dictionary_item_added"):
for item_added in dictionary_item_added:
entity_name, entity_id, attribute = cls.__get_changed_entity_attribute(item_added)

if attribute:
value_added = new_conf_json[entity_name][entity_id][attribute]
else:
value_added = new_conf_json[entity_name][entity_id]

entity_name = cls.__rename_global_node_name(entity_name)

config_diff["added_items"].append(((entity_name, entity_id, attribute), (value_added)))

if dictionary_item_removed := deepdiff_result.get("dictionary_item_removed"):
for item_removed in dictionary_item_removed:
entity_name, entity_id, attribute = cls.__get_changed_entity_attribute(item_removed)

if attribute:
value_removed = old_conf_json[entity_name][entity_id][attribute]
else:
value_removed = old_conf_json[entity_name][entity_id]

entity_name = cls.__rename_global_node_name(entity_name)

config_diff["removed_items"].append(((entity_name, entity_id, attribute), (value_removed)))

if values_changed := deepdiff_result.get("values_changed"):
for item_changed, value_changed in values_changed.items():
entity_name, entity_id, attribute = cls.__get_changed_entity_attribute(item_changed)
entity_name = cls.__rename_global_node_name(entity_name)

config_diff["modified_items"].append(
((entity_name, entity_id, attribute), (value_changed["old_value"], value_changed["new_value"]))
)

# Sort by entity name
config_diff["added_items"].sort(key=lambda x: x[0][0])
config_diff["removed_items"].sort(key=lambda x: x[0][0])
config_diff["modified_items"].sort(key=lambda x: x[0][0])

return config_diff

@classmethod
def __get_changed_entity_attribute(cls, attribute_bracket_notation):
"""Split the entity name, entity id (if exists), and the attribute name from JSON bracket notation."""
try:
entity_name, entity_id, attribute = re.findall(r"\[\'(.*?)\'\]", attribute_bracket_notation)
except ValueError:
entity_name, entity_id = re.findall(r"\[\'(.*?)\'\]", attribute_bracket_notation)
attribute = None

return entity_name, entity_id, attribute

@classmethod
def __rename_global_node_name(cls, node_name):
if node_name == _BaseSerializer._GLOBAL_NODE_NAME:
return "Global Configuration"
return node_name
4 changes: 2 additions & 2 deletions src/taipy/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,11 @@ def __log_message(cls, config):
raise ConfigurationIssueError("Configuration issues found.")

@classmethod
def _to_json(cls, _config):
def _to_json(cls, _config: _Config) -> str:
return cls.__json_serializer._serialize(_config)

@classmethod
def _from_json(cls, config_as_str: str):
def _from_json(cls, config_as_str: str) -> _Config:
return cls.__json_serializer._deserialize(config_as_str)


Expand Down
74 changes: 74 additions & 0 deletions tests/config/test_config_comparator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2022 Avaiga Private Limited
#
# Licensed 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.

from src.taipy.config._config import _Config
from src.taipy.config._config_comparator import _ConfigComparator
from src.taipy.config.global_app.global_app_config import GlobalAppConfig
from tests.config.utils.section_for_tests import SectionForTest
from tests.config.utils.unique_section_for_tests import UniqueSectionForTest


def test_config_comparator():
unique_section_1 = UniqueSectionForTest(attribute="unique_attribute_1", prop="unique_prop_1")
section_1 = SectionForTest("section_1", attribute="attribute_1", prop="prop_1")
section_2 = SectionForTest("section_2", attribute=2, prop="prop_2")
section_3 = SectionForTest("section_3", attribute=3, prop="prop_3")
section_4 = SectionForTest("section_4", attribute=4, prop="prop_4")

_config_1 = _Config._default_config()
_config_1._sections[SectionForTest.name] = {"section_1": section_1, "section_2": section_2, "section_4": section_4}
_config_1._unique_sections[UniqueSectionForTest.name] = unique_section_1

_config_2 = _Config._default_config()

# Update some global config
_config_2._global_config = GlobalAppConfig(
root_folder="foo",
storage_folder="bar",
repository_properties={"foo": "bar"},
repository_type="baz",
clean_entities_enabled=True,
)
# Update section_2, add section_3, and remove section 4
_config_2._sections[SectionForTest.name] = {"section_1": section_1, "section_2": section_3, "section_3": section_2}
_config_2._unique_sections[UniqueSectionForTest.name] = unique_section_1

config_diff = _ConfigComparator._compare(_config_1, _config_2)

# The result was sorted so test by indexing is fine.
assert len(config_diff["added_items"]) == 2
assert config_diff["added_items"][1] == (
("section_name", "section_3", None),
{"attribute": "2:int", "prop": "prop_2"},
)

# TODO: This should not in ["added_items"] since it is a default value that was changed. This should be fixed soon.
assert config_diff["added_items"][0] == (("Global Configuration", "repository_properties", None), {"foo": "bar"})

assert len(config_diff["modified_items"]) == 6
assert config_diff["modified_items"][0] == (("Global Configuration", "root_folder", None), ("./taipy/", "foo"))
assert config_diff["modified_items"][1] == (("Global Configuration", "storage_folder", None), (".data/", "bar"))
assert config_diff["modified_items"][2] == (
("Global Configuration", "clean_entities_enabled", None),
("ENV[TAIPY_CLEAN_ENTITIES_ENABLED]", "True:bool"),
)
assert config_diff["modified_items"][3] == (
("Global Configuration", "repository_type", None),
("filesystem", "baz"),
)
assert config_diff["modified_items"][4] == (("section_name", "section_2", "attribute"), ("2:int", "3:int"))
assert config_diff["modified_items"][5] == (("section_name", "section_2", "prop"), ("prop_2", "prop_3"))

assert len(config_diff["removed_items"]) == 1
assert config_diff["removed_items"][0] == (
("section_name", "section_4", None),
{"attribute": "4:int", "prop": "prop_4"},
)

0 comments on commit 4453ba4

Please sign in to comment.