Skip to content

Commit

Permalink
Feat: Add comparator for INI files (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien-berchet authored Mar 13, 2023
1 parent 04520d7 commit 273d8b3
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 14 deletions.
4 changes: 4 additions & 0 deletions dir_content_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path

from dir_content_diff.base_comparators import DefaultComparator
from dir_content_diff.base_comparators import IniComparator
from dir_content_diff.base_comparators import JsonComparator
from dir_content_diff.base_comparators import PdfComparator
from dir_content_diff.base_comparators import XmlComparator
Expand All @@ -22,6 +23,9 @@

_DEFAULT_COMPARATORS = {
None: DefaultComparator(),
".cfg": IniComparator(), # luigi config files
".conf": IniComparator(), # logging config files
".ini": IniComparator(),
".json": JsonComparator(),
".pdf": PdfComparator(),
".yaml": YamlComparator(),
Expand Down
34 changes: 34 additions & 0 deletions dir_content_diff/base_comparators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Module containing the base comparators."""
import configparser
import filecmp
import json
from abc import ABC
Expand Down Expand Up @@ -460,6 +461,39 @@ def xmltodict(obj):
return {root.tag: output}


class IniComparator(DictComparator):
"""Comparator for INI files.
This comparator is based on the :class:`DictComparator` and uses the same parameters.
.. note::
The ``load_kwargs`` are passed to the ``configparser.ConfigParser``.
"""

def load(self, path, **kwargs): # pylint: disable=arguments-differ
"""Open a XML file."""
data = configparser.ConfigParser(**kwargs)
data.read(path)
return self.configparser_to_dict(data)

@staticmethod
def configparser_to_dict(config):
"""Transform a ConfigParser object into a dict."""
dict_config = {}
for section in config.sections():
dict_config[section] = {}
for option in config.options(section):
val = config.get(section, option)
try:
# Try to load JSON strings if possible
val = json.loads(val)
except json.JSONDecodeError:
pass
dict_config[section][option] = val
return dict_config


class PdfComparator(BaseComparator):
"""Comparator for PDF files."""

Expand Down
18 changes: 18 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def ref_tree(empty_ref_tree):
generate_test_files.create_json(empty_ref_tree / "file.json")
generate_test_files.create_yaml(empty_ref_tree / "file.yaml")
generate_test_files.create_xml(empty_ref_tree / "file.xml")
generate_test_files.create_ini(empty_ref_tree / "file.ini")
return empty_ref_tree


Expand All @@ -53,6 +54,7 @@ def res_tree_equal(empty_res_tree):
generate_test_files.create_json(empty_res_tree / "file.json")
generate_test_files.create_yaml(empty_res_tree / "file.yaml")
generate_test_files.create_xml(empty_res_tree / "file.xml")
generate_test_files.create_ini(empty_res_tree / "file.ini")
return empty_res_tree


Expand All @@ -63,6 +65,7 @@ def res_tree_diff(empty_res_tree):
generate_test_files.create_json(empty_res_tree / "file.json", diff=True)
generate_test_files.create_yaml(empty_res_tree / "file.yaml", diff=True)
generate_test_files.create_xml(empty_res_tree / "file.xml", diff=True)
generate_test_files.create_ini(empty_res_tree / "file.ini", diff=True)
return empty_res_tree


Expand Down Expand Up @@ -129,6 +132,21 @@ def xml_diff(dict_diff):
return diff


@pytest.fixture
def ini_diff():
"""The diff that should be reported for the INI files."""
diff = (
r"The files '\S*/file\.ini' and '\S*/file\.ini' are different:\n"
r"Changed the value of '\[section1\]\[attr1\]' from 'val1' to 'val2'\.\n"
r"Changed the value of '\[section1\]\[attr2\]' from 1 to 2.\n"
r"Changed the value of '\[section2\]\[attr3\]\[1\]' from 2 to 3.\n"
r"Changed the value of '\[section2\]\[attr3\]\[3\]' from 'b' to 'c'.\n"
r"Changed the value of '\[section2\]\[attr4\]\[a\]' from 1 to 4.\n"
r"Changed the value of '\[section2\]\[attr4\]\[b\]\[1\]' from 2 to 3."
)
return diff


@pytest.fixture
def ref_csv(ref_tree):
"""The reference CSV file."""
Expand Down
31 changes: 31 additions & 0 deletions tests/generate_test_files.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Function to create base files used for tests."""
import configparser
import copy
import json
import tempfile
Expand Down Expand Up @@ -137,6 +138,36 @@ def create_xml(filename, diff=False):
f.write(xml_data)


REF_INI = {
"section1": {
"attr1": "val1",
"attr2": 1,
},
"section2": {"attr3": [1, 2, "a", "b"], "attr4": {"a": 1, "b": [1, 2]}},
}
DIFF_INI = {
"section1": {
"attr1": "val2",
"attr2": 2,
},
"section2": {"attr3": [1, 3, "a", "c"], "attr4": {"a": 4, "b": [1, 3]}},
}


def create_ini(filename, diff=False):
"""Create a INI file."""
ini_data = configparser.ConfigParser()
if diff:
data = copy.deepcopy(DIFF_INI)
else:
data = copy.deepcopy(REF_INI)
data["section2"]["attr3"] = json.dumps(data["section2"]["attr3"])
data["section2"]["attr4"] = json.dumps(data["section2"]["attr4"])
ini_data.read_dict(data)
with open(filename, "w", encoding="utf-8") as f:
ini_data.write(f)


def create_pdf(filename, diff=False):
"""Create a PDF file."""
if diff:
Expand Down
64 changes: 53 additions & 11 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
# pylint: disable=use-implicit-booleaness-not-comparison
import configparser
import json
import re

Expand Down Expand Up @@ -492,6 +493,21 @@ def test_add_to_output_with_none(self):
comparator = dir_content_diff.XmlComparator()
comparator.add_to_output(None, None)

class TestIniComparator:
"""Test the INI comparator."""

def test_initodict(self, ref_tree):
"""Test conversion of INI files into dict."""
data = configparser.ConfigParser()
data.read(ref_tree / "file.ini")

comparator = dir_content_diff.IniComparator()
res = comparator.configparser_to_dict(data)
assert res == {
"section1": {"attr1": "val1", "attr2": 1},
"section2": {"attr3": [1, 2, "a", "b"], "attr4": {"a": 1, "b": [1, 2]}},
}


class TestRegistry:
"""Test the internal registry."""
Expand All @@ -500,6 +516,9 @@ def test_init_register(self, registry_reseter):
"""Test the initial registry with the get_comparators() function."""
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".yaml": dir_content_diff.YamlComparator(),
Expand All @@ -512,6 +531,9 @@ def test_update_register(self, registry_reseter):
dir_content_diff.register_comparator(".test_ext", dir_content_diff.JsonComparator())
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".test_ext": dir_content_diff.JsonComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
Expand All @@ -524,6 +546,9 @@ def test_update_register(self, registry_reseter):
dir_content_diff.unregister_comparator("json") # Test suffix without dot
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".test_ext": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".yml": dir_content_diff.YamlComparator(),
Expand All @@ -533,6 +558,9 @@ def test_update_register(self, registry_reseter):
dir_content_diff.reset_comparators()
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".yaml": dir_content_diff.YamlComparator(),
Expand All @@ -556,6 +584,9 @@ def test_update_register(self, registry_reseter):
dir_content_diff.register_comparator(".new_ext", dir_content_diff.JsonComparator())
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".yaml": dir_content_diff.YamlComparator(),
Expand All @@ -568,6 +599,9 @@ def test_update_register(self, registry_reseter):
)
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".yaml": dir_content_diff.YamlComparator(),
Expand Down Expand Up @@ -676,17 +710,18 @@ def test_specific_args(self, ref_tree, res_tree_equal):
class TestDiffTrees:
"""Tests that should return differences."""

def test_diff_tree(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff):
def test_diff_tree(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff, ini_diff):
"""Test that the returned differences are correct."""
res = compare_trees(ref_tree, res_tree_diff)

assert len(res) == 4
assert len(res) == 5
match_res_0 = re.match(pdf_diff, res["file.pdf"])
match_res_1 = re.match(dict_diff, res["file.json"])
match_res_2 = re.match(dict_diff, res["file.yaml"])
match_res_3 = re.match(xml_diff, res["file.xml"])
match_res_4 = re.match(ini_diff, res["file.ini"])

for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3, match_res_4]:
assert match_i is not None

def test_assert_equal_trees(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff):
Expand All @@ -704,7 +739,7 @@ def test_diff_ref_not_empty_res_empty(self, ref_tree, empty_res_tree):
"""Test with empty compared tree."""
res = compare_trees(ref_tree, empty_res_tree)

assert len(res) == 4
assert len(res) == 5
match_res_0 = re.match(
r"The file 'file.pdf' does not exist in '\S*/res'\.", res["file.pdf"]
)
Expand All @@ -717,8 +752,11 @@ def test_diff_ref_not_empty_res_empty(self, ref_tree, empty_res_tree):
match_res_3 = re.match(
r"The file 'file.xml' does not exist in '\S*/res'\.", res["file.xml"]
)
match_res_4 = re.match(
r"The file 'file.ini' does not exist in '\S*/res'\.", res["file.ini"]
)

for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3, match_res_4]:
assert match_i is not None

def test_exception_in_comparator(self, ref_tree, res_tree_equal, registry_reseter):
Expand All @@ -740,7 +778,7 @@ def bad_comparator(ref_path, test_path, *args, **kwargs):
)
assert match is not None

def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff):
def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff, ini_diff):
"""Test specific args."""
specific_args = {
"file.pdf": {"threshold": 50},
Expand All @@ -749,7 +787,7 @@ def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff):
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)

# This time the PDF files are considered as equal
assert len(res) == 3
assert len(res) == 4
match_res_0 = re.match(dict_diff, res["file.yaml"])
match_res_1 = re.match(
dict_diff.replace(
Expand All @@ -759,8 +797,9 @@ def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff):
res["file.json"],
)
match_res_2 = re.match(xml_diff, res["file.xml"])
match_res_3 = re.match(ini_diff, res["file.ini"])

for match_i in [match_res_0, match_res_1, match_res_2]:
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
assert match_i is not None

def test_unknown_comparator(self, ref_tree, res_tree_diff, registry_reseter):
Expand All @@ -783,12 +822,14 @@ def test_nested_files(self, ref_with_nested_file, res_diff_with_nested_file):
)
assert match is not None

def test_fix_dot_notation(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff):
def test_fix_dot_notation(
self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff, ini_diff
):
"""Test that the dot notation is properly fixed."""
specific_args = {"file.yaml": {"args": [None, None, None, False, 0, True]}}
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)

assert len(res) == 4
assert len(res) == 5
match_res_0 = re.match(pdf_diff, res["file.pdf"])
match_res_1 = re.match(
dict_diff.replace(
Expand All @@ -800,8 +841,9 @@ def test_fix_dot_notation(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xm
)
match_res_2 = re.match(dict_diff, res["file.json"])
match_res_3 = re.match(xml_diff, res["file.xml"])
match_res_4 = re.match(ini_diff, res["file.ini"])

for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3, match_res_4]:
assert match_i is not None

def test_format_inside_diff(self, ref_tree, res_tree_diff, dict_diff):
Expand Down
12 changes: 9 additions & 3 deletions tests/test_pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ def test_pandas_register(self, registry_reseter):
"""Test registering the pandas plugin."""
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".xml": dir_content_diff.XmlComparator(),
Expand All @@ -31,6 +34,9 @@ def test_pandas_register(self, registry_reseter):
dir_content_diff.pandas.register()
assert dir_content_diff.get_comparators() == {
None: dir_content_diff.DefaultComparator(),
".cfg": dir_content_diff.IniComparator(),
".conf": dir_content_diff.IniComparator(),
".ini": dir_content_diff.IniComparator(),
".json": dir_content_diff.JsonComparator(),
".pdf": dir_content_diff.PdfComparator(),
".xml": dir_content_diff.XmlComparator(),
Expand Down Expand Up @@ -109,7 +115,7 @@ def test_specific_args(
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)

# The CSV file is considered as equal thanks to the given kwargs
assert len(res) == 4
assert len(res) == 5
assert re.match(".*/file.csv.*", str(res)) is None

def test_replace_pattern(
Expand Down Expand Up @@ -237,7 +243,7 @@ def test_diff_tree(
"""Test that the returned differences are correct."""
res = compare_trees(ref_tree, res_tree_diff)

assert len(res) == 5
assert len(res) == 6
res_csv = res["file.csv"]
match_res = re.match(csv_diff, res_csv)
assert match_res is not None
Expand All @@ -251,7 +257,7 @@ def test_read_csv_kwargs(
}
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)

assert len(res) == 5
assert len(res) == 6
res_csv = res["file.csv"]
kwargs_msg = (
"Kwargs used for loading data: {'header': None, 'skiprows': 1, 'prefix': 'col_'}\n"
Expand Down

0 comments on commit 273d8b3

Please sign in to comment.