Skip to content

Commit 273d8b3

Browse files
Feat: Add comparator for INI files (#28)
1 parent 04520d7 commit 273d8b3

File tree

6 files changed

+149
-14
lines changed

6 files changed

+149
-14
lines changed

dir_content_diff/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib import Path
99

1010
from dir_content_diff.base_comparators import DefaultComparator
11+
from dir_content_diff.base_comparators import IniComparator
1112
from dir_content_diff.base_comparators import JsonComparator
1213
from dir_content_diff.base_comparators import PdfComparator
1314
from dir_content_diff.base_comparators import XmlComparator
@@ -22,6 +23,9 @@
2223

2324
_DEFAULT_COMPARATORS = {
2425
None: DefaultComparator(),
26+
".cfg": IniComparator(), # luigi config files
27+
".conf": IniComparator(), # logging config files
28+
".ini": IniComparator(),
2529
".json": JsonComparator(),
2630
".pdf": PdfComparator(),
2731
".yaml": YamlComparator(),

dir_content_diff/base_comparators.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Module containing the base comparators."""
2+
import configparser
23
import filecmp
34
import json
45
from abc import ABC
@@ -460,6 +461,39 @@ def xmltodict(obj):
460461
return {root.tag: output}
461462

462463

464+
class IniComparator(DictComparator):
465+
"""Comparator for INI files.
466+
467+
This comparator is based on the :class:`DictComparator` and uses the same parameters.
468+
469+
.. note::
470+
471+
The ``load_kwargs`` are passed to the ``configparser.ConfigParser``.
472+
"""
473+
474+
def load(self, path, **kwargs): # pylint: disable=arguments-differ
475+
"""Open a XML file."""
476+
data = configparser.ConfigParser(**kwargs)
477+
data.read(path)
478+
return self.configparser_to_dict(data)
479+
480+
@staticmethod
481+
def configparser_to_dict(config):
482+
"""Transform a ConfigParser object into a dict."""
483+
dict_config = {}
484+
for section in config.sections():
485+
dict_config[section] = {}
486+
for option in config.options(section):
487+
val = config.get(section, option)
488+
try:
489+
# Try to load JSON strings if possible
490+
val = json.loads(val)
491+
except json.JSONDecodeError:
492+
pass
493+
dict_config[section][option] = val
494+
return dict_config
495+
496+
463497
class PdfComparator(BaseComparator):
464498
"""Comparator for PDF files."""
465499

tests/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def ref_tree(empty_ref_tree):
4343
generate_test_files.create_json(empty_ref_tree / "file.json")
4444
generate_test_files.create_yaml(empty_ref_tree / "file.yaml")
4545
generate_test_files.create_xml(empty_ref_tree / "file.xml")
46+
generate_test_files.create_ini(empty_ref_tree / "file.ini")
4647
return empty_ref_tree
4748

4849

@@ -53,6 +54,7 @@ def res_tree_equal(empty_res_tree):
5354
generate_test_files.create_json(empty_res_tree / "file.json")
5455
generate_test_files.create_yaml(empty_res_tree / "file.yaml")
5556
generate_test_files.create_xml(empty_res_tree / "file.xml")
57+
generate_test_files.create_ini(empty_res_tree / "file.ini")
5658
return empty_res_tree
5759

5860

@@ -63,6 +65,7 @@ def res_tree_diff(empty_res_tree):
6365
generate_test_files.create_json(empty_res_tree / "file.json", diff=True)
6466
generate_test_files.create_yaml(empty_res_tree / "file.yaml", diff=True)
6567
generate_test_files.create_xml(empty_res_tree / "file.xml", diff=True)
68+
generate_test_files.create_ini(empty_res_tree / "file.ini", diff=True)
6669
return empty_res_tree
6770

6871

@@ -129,6 +132,21 @@ def xml_diff(dict_diff):
129132
return diff
130133

131134

135+
@pytest.fixture
136+
def ini_diff():
137+
"""The diff that should be reported for the INI files."""
138+
diff = (
139+
r"The files '\S*/file\.ini' and '\S*/file\.ini' are different:\n"
140+
r"Changed the value of '\[section1\]\[attr1\]' from 'val1' to 'val2'\.\n"
141+
r"Changed the value of '\[section1\]\[attr2\]' from 1 to 2.\n"
142+
r"Changed the value of '\[section2\]\[attr3\]\[1\]' from 2 to 3.\n"
143+
r"Changed the value of '\[section2\]\[attr3\]\[3\]' from 'b' to 'c'.\n"
144+
r"Changed the value of '\[section2\]\[attr4\]\[a\]' from 1 to 4.\n"
145+
r"Changed the value of '\[section2\]\[attr4\]\[b\]\[1\]' from 2 to 3."
146+
)
147+
return diff
148+
149+
132150
@pytest.fixture
133151
def ref_csv(ref_tree):
134152
"""The reference CSV file."""

tests/generate_test_files.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Function to create base files used for tests."""
2+
import configparser
23
import copy
34
import json
45
import tempfile
@@ -137,6 +138,36 @@ def create_xml(filename, diff=False):
137138
f.write(xml_data)
138139

139140

141+
REF_INI = {
142+
"section1": {
143+
"attr1": "val1",
144+
"attr2": 1,
145+
},
146+
"section2": {"attr3": [1, 2, "a", "b"], "attr4": {"a": 1, "b": [1, 2]}},
147+
}
148+
DIFF_INI = {
149+
"section1": {
150+
"attr1": "val2",
151+
"attr2": 2,
152+
},
153+
"section2": {"attr3": [1, 3, "a", "c"], "attr4": {"a": 4, "b": [1, 3]}},
154+
}
155+
156+
157+
def create_ini(filename, diff=False):
158+
"""Create a INI file."""
159+
ini_data = configparser.ConfigParser()
160+
if diff:
161+
data = copy.deepcopy(DIFF_INI)
162+
else:
163+
data = copy.deepcopy(REF_INI)
164+
data["section2"]["attr3"] = json.dumps(data["section2"]["attr3"])
165+
data["section2"]["attr4"] = json.dumps(data["section2"]["attr4"])
166+
ini_data.read_dict(data)
167+
with open(filename, "w", encoding="utf-8") as f:
168+
ini_data.write(f)
169+
170+
140171
def create_pdf(filename, diff=False):
141172
"""Create a PDF file."""
142173
if diff:

tests/test_base.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# pylint: disable=redefined-outer-name
44
# pylint: disable=unused-argument
55
# pylint: disable=use-implicit-booleaness-not-comparison
6+
import configparser
67
import json
78
import re
89

@@ -492,6 +493,21 @@ def test_add_to_output_with_none(self):
492493
comparator = dir_content_diff.XmlComparator()
493494
comparator.add_to_output(None, None)
494495

496+
class TestIniComparator:
497+
"""Test the INI comparator."""
498+
499+
def test_initodict(self, ref_tree):
500+
"""Test conversion of INI files into dict."""
501+
data = configparser.ConfigParser()
502+
data.read(ref_tree / "file.ini")
503+
504+
comparator = dir_content_diff.IniComparator()
505+
res = comparator.configparser_to_dict(data)
506+
assert res == {
507+
"section1": {"attr1": "val1", "attr2": 1},
508+
"section2": {"attr3": [1, 2, "a", "b"], "attr4": {"a": 1, "b": [1, 2]}},
509+
}
510+
495511

496512
class TestRegistry:
497513
"""Test the internal registry."""
@@ -500,6 +516,9 @@ def test_init_register(self, registry_reseter):
500516
"""Test the initial registry with the get_comparators() function."""
501517
assert dir_content_diff.get_comparators() == {
502518
None: dir_content_diff.DefaultComparator(),
519+
".cfg": dir_content_diff.IniComparator(),
520+
".conf": dir_content_diff.IniComparator(),
521+
".ini": dir_content_diff.IniComparator(),
503522
".json": dir_content_diff.JsonComparator(),
504523
".pdf": dir_content_diff.PdfComparator(),
505524
".yaml": dir_content_diff.YamlComparator(),
@@ -512,6 +531,9 @@ def test_update_register(self, registry_reseter):
512531
dir_content_diff.register_comparator(".test_ext", dir_content_diff.JsonComparator())
513532
assert dir_content_diff.get_comparators() == {
514533
None: dir_content_diff.DefaultComparator(),
534+
".cfg": dir_content_diff.IniComparator(),
535+
".conf": dir_content_diff.IniComparator(),
536+
".ini": dir_content_diff.IniComparator(),
515537
".test_ext": dir_content_diff.JsonComparator(),
516538
".json": dir_content_diff.JsonComparator(),
517539
".pdf": dir_content_diff.PdfComparator(),
@@ -524,6 +546,9 @@ def test_update_register(self, registry_reseter):
524546
dir_content_diff.unregister_comparator("json") # Test suffix without dot
525547
assert dir_content_diff.get_comparators() == {
526548
None: dir_content_diff.DefaultComparator(),
549+
".cfg": dir_content_diff.IniComparator(),
550+
".conf": dir_content_diff.IniComparator(),
551+
".ini": dir_content_diff.IniComparator(),
527552
".test_ext": dir_content_diff.JsonComparator(),
528553
".pdf": dir_content_diff.PdfComparator(),
529554
".yml": dir_content_diff.YamlComparator(),
@@ -533,6 +558,9 @@ def test_update_register(self, registry_reseter):
533558
dir_content_diff.reset_comparators()
534559
assert dir_content_diff.get_comparators() == {
535560
None: dir_content_diff.DefaultComparator(),
561+
".cfg": dir_content_diff.IniComparator(),
562+
".conf": dir_content_diff.IniComparator(),
563+
".ini": dir_content_diff.IniComparator(),
536564
".json": dir_content_diff.JsonComparator(),
537565
".pdf": dir_content_diff.PdfComparator(),
538566
".yaml": dir_content_diff.YamlComparator(),
@@ -556,6 +584,9 @@ def test_update_register(self, registry_reseter):
556584
dir_content_diff.register_comparator(".new_ext", dir_content_diff.JsonComparator())
557585
assert dir_content_diff.get_comparators() == {
558586
None: dir_content_diff.DefaultComparator(),
587+
".cfg": dir_content_diff.IniComparator(),
588+
".conf": dir_content_diff.IniComparator(),
589+
".ini": dir_content_diff.IniComparator(),
559590
".json": dir_content_diff.JsonComparator(),
560591
".pdf": dir_content_diff.PdfComparator(),
561592
".yaml": dir_content_diff.YamlComparator(),
@@ -568,6 +599,9 @@ def test_update_register(self, registry_reseter):
568599
)
569600
assert dir_content_diff.get_comparators() == {
570601
None: dir_content_diff.DefaultComparator(),
602+
".cfg": dir_content_diff.IniComparator(),
603+
".conf": dir_content_diff.IniComparator(),
604+
".ini": dir_content_diff.IniComparator(),
571605
".json": dir_content_diff.JsonComparator(),
572606
".pdf": dir_content_diff.PdfComparator(),
573607
".yaml": dir_content_diff.YamlComparator(),
@@ -676,17 +710,18 @@ def test_specific_args(self, ref_tree, res_tree_equal):
676710
class TestDiffTrees:
677711
"""Tests that should return differences."""
678712

679-
def test_diff_tree(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff):
713+
def test_diff_tree(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff, ini_diff):
680714
"""Test that the returned differences are correct."""
681715
res = compare_trees(ref_tree, res_tree_diff)
682716

683-
assert len(res) == 4
717+
assert len(res) == 5
684718
match_res_0 = re.match(pdf_diff, res["file.pdf"])
685719
match_res_1 = re.match(dict_diff, res["file.json"])
686720
match_res_2 = re.match(dict_diff, res["file.yaml"])
687721
match_res_3 = re.match(xml_diff, res["file.xml"])
722+
match_res_4 = re.match(ini_diff, res["file.ini"])
688723

689-
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
724+
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3, match_res_4]:
690725
assert match_i is not None
691726

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

707-
assert len(res) == 4
742+
assert len(res) == 5
708743
match_res_0 = re.match(
709744
r"The file 'file.pdf' does not exist in '\S*/res'\.", res["file.pdf"]
710745
)
@@ -717,8 +752,11 @@ def test_diff_ref_not_empty_res_empty(self, ref_tree, empty_res_tree):
717752
match_res_3 = re.match(
718753
r"The file 'file.xml' does not exist in '\S*/res'\.", res["file.xml"]
719754
)
755+
match_res_4 = re.match(
756+
r"The file 'file.ini' does not exist in '\S*/res'\.", res["file.ini"]
757+
)
720758

721-
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
759+
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3, match_res_4]:
722760
assert match_i is not None
723761

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

743-
def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff):
781+
def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff, ini_diff):
744782
"""Test specific args."""
745783
specific_args = {
746784
"file.pdf": {"threshold": 50},
@@ -749,7 +787,7 @@ def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff):
749787
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)
750788

751789
# This time the PDF files are considered as equal
752-
assert len(res) == 3
790+
assert len(res) == 4
753791
match_res_0 = re.match(dict_diff, res["file.yaml"])
754792
match_res_1 = re.match(
755793
dict_diff.replace(
@@ -759,8 +797,9 @@ def test_specific_args(self, ref_tree, res_tree_diff, dict_diff, xml_diff):
759797
res["file.json"],
760798
)
761799
match_res_2 = re.match(xml_diff, res["file.xml"])
800+
match_res_3 = re.match(ini_diff, res["file.ini"])
762801

763-
for match_i in [match_res_0, match_res_1, match_res_2]:
802+
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
764803
assert match_i is not None
765804

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

786-
def test_fix_dot_notation(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff):
825+
def test_fix_dot_notation(
826+
self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xml_diff, ini_diff
827+
):
787828
"""Test that the dot notation is properly fixed."""
788829
specific_args = {"file.yaml": {"args": [None, None, None, False, 0, True]}}
789830
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)
790831

791-
assert len(res) == 4
832+
assert len(res) == 5
792833
match_res_0 = re.match(pdf_diff, res["file.pdf"])
793834
match_res_1 = re.match(
794835
dict_diff.replace(
@@ -800,8 +841,9 @@ def test_fix_dot_notation(self, ref_tree, res_tree_diff, pdf_diff, dict_diff, xm
800841
)
801842
match_res_2 = re.match(dict_diff, res["file.json"])
802843
match_res_3 = re.match(xml_diff, res["file.xml"])
844+
match_res_4 = re.match(ini_diff, res["file.ini"])
803845

804-
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3]:
846+
for match_i in [match_res_0, match_res_1, match_res_2, match_res_3, match_res_4]:
805847
assert match_i is not None
806848

807849
def test_format_inside_diff(self, ref_tree, res_tree_diff, dict_diff):

tests/test_pandas.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ def test_pandas_register(self, registry_reseter):
2121
"""Test registering the pandas plugin."""
2222
assert dir_content_diff.get_comparators() == {
2323
None: dir_content_diff.DefaultComparator(),
24+
".cfg": dir_content_diff.IniComparator(),
25+
".conf": dir_content_diff.IniComparator(),
26+
".ini": dir_content_diff.IniComparator(),
2427
".json": dir_content_diff.JsonComparator(),
2528
".pdf": dir_content_diff.PdfComparator(),
2629
".xml": dir_content_diff.XmlComparator(),
@@ -31,6 +34,9 @@ def test_pandas_register(self, registry_reseter):
3134
dir_content_diff.pandas.register()
3235
assert dir_content_diff.get_comparators() == {
3336
None: dir_content_diff.DefaultComparator(),
37+
".cfg": dir_content_diff.IniComparator(),
38+
".conf": dir_content_diff.IniComparator(),
39+
".ini": dir_content_diff.IniComparator(),
3440
".json": dir_content_diff.JsonComparator(),
3541
".pdf": dir_content_diff.PdfComparator(),
3642
".xml": dir_content_diff.XmlComparator(),
@@ -109,7 +115,7 @@ def test_specific_args(
109115
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)
110116

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

115121
def test_replace_pattern(
@@ -237,7 +243,7 @@ def test_diff_tree(
237243
"""Test that the returned differences are correct."""
238244
res = compare_trees(ref_tree, res_tree_diff)
239245

240-
assert len(res) == 5
246+
assert len(res) == 6
241247
res_csv = res["file.csv"]
242248
match_res = re.match(csv_diff, res_csv)
243249
assert match_res is not None
@@ -251,7 +257,7 @@ def test_read_csv_kwargs(
251257
}
252258
res = compare_trees(ref_tree, res_tree_diff, specific_args=specific_args)
253259

254-
assert len(res) == 5
260+
assert len(res) == 6
255261
res_csv = res["file.csv"]
256262
kwargs_msg = (
257263
"Kwargs used for loading data: {'header': None, 'skiprows': 1, 'prefix': 'col_'}\n"

0 commit comments

Comments
 (0)