Repository: gbowne1/json-maestro (language: python)

Issue:
jsonmaestro.py def remove_comments has unknown types
```py
def remove_comments(obj: Union[Dict[str, Any], List[Any], Any]) -> Union[Dict[str, Any], List[Any], Any]:
	"""Remove comments from the object."""
	if isinstance(obj, dict):
		return {k: remove_comments(v) for k, v in obj.items()}
	elif isinstance(obj, list):
		return [remove_comments(item) for item in obj]
	else:
		return obj
```

has partially unknown types for k, v and item in the return, at least according to my Pylance.
json.load automatically removes duplicate keys in valid json
I have notice that when loading json from string 

example:
```json
{
    data: "foo",
    key: "bar",
    data: "baz",
}
```

json.load will automatically put data=baz into the dictionary


Discussion / hints:

Fix patch:
diff --git a/data/commented.json b/data/commented.json
new file mode 100644
index 0000000..3c17d2b
--- /dev/null
+++ b/data/commented.json
@@ -0,0 +1,65 @@
+{
+	// User information section
+	"user": {
+		"id": 12345, // Unique user identifier
+		"name": "John Doe", // Full name of the user
+		"age": 30, // User's age
+		"email": "john.doe@example.com", // User's email address
+		"isActive": true, // Is the user currently active?
+
+		/* This section contains the
+		 user's social media profiles */
+		"socialProfiles": {
+			"twitter": "@johndoe", // Twitter handle
+			"linkedin": "/in/johndoe", // LinkedIn profile URL
+			// Facebook is currently not provided
+			"facebook": null
+		}
+	},
+
+	// List of user's favorite books
+	"favorites": {
+		"books": [
+			{
+				"title": "1984", // Book title
+				"author": "George Orwell", // Author of the book
+				"rating": 4.9 // User's rating for the book
+			},
+			{
+				"title": "To Kill a Mockingbird",
+				"author": "Harper Lee",
+				"rating": 4.8
+			}
+		],
+
+		/* User's favorite movies
+		 Data includes only top 2 movies */
+		"movies": [
+			{
+				"title": "Inception",
+				"year": 2010, // Release year of the movie
+				"rating": 9.0 // Rating out of 10
+			},
+			{
+				"title": "The Matrix",
+				"year": 1999,
+				"rating": 8.7
+			}
+		]
+	},
+
+	// Application settings
+	"settings": {
+		"theme": "dark", // UI theme preference: light or dark
+		"notifications": {
+			"email": true, // Email notifications enabled?
+			"sms": false // SMS notifications enabled?
+		}
+	},
+
+	// Metadata about the JSON file
+	"metadata": {
+		"version": 1.0, // Version of the file structure
+		"lastUpdated": "2024-10-10T08:00:00Z" // Last update timestamp
+	}
+}
diff --git a/data/commented_with_duplicate.json b/data/commented_with_duplicate.json
new file mode 100644
index 0000000..5caa3aa
--- /dev/null
+++ b/data/commented_with_duplicate.json
@@ -0,0 +1,88 @@
+{
+	// User information section
+	"user": {
+		"id": 12345, // Unique user identifier
+		"name": "John Doe", // Full name of the user
+		"age": 30, // User's age
+
+		/* Duplicate key example - "age" is defined again,
+		 this could override the previous one */
+		"age": 31, // Updated age of the user (overrides the previous "age" key)
+
+		"email": "john.doe@example.com", // User's email address
+		"isActive": true, // Is the user currently active?
+
+		// Duplicate keys in nested object
+		"socialProfiles": {
+			"twitter": "@johndoe", // Twitter handle
+			"linkedin": "/in/johndoe", // LinkedIn profile URL
+
+			// Duplicate key example - "twitter" is defined again
+			"twitter": "@realjohndoe", // Updated Twitter handle (overrides the previous one)
+
+			// Facebook is currently not provided
+			"facebook": null
+		}
+	},
+
+	// List of user's favorite books
+	"favorites": {
+		"books": [
+			{
+				"title": "1984", // Book title
+				"author": "George Orwell", // Author of the book
+				"rating": 4.9 // User's rating for the book
+			},
+			{
+				"title": "To Kill a Mockingbird",
+				"author": "Harper Lee",
+				"rating": 4.8
+			}
+		],
+
+		// Duplicate key example in arrays
+		"movies": [
+			{
+				"title": "Inception",
+				"year": 2010, // Release year of the movie
+				"rating": 9.0 // Rating out of 10
+			},
+			{
+				"title": "The Matrix",
+				"year": 1999,
+				"rating": 8.7
+			}
+		],
+
+		// Duplicate key - "movies" is defined again
+		"movies": [
+			{
+				"title": "Interstellar", // New movie added (this array will overwrite the previous one)
+				"year": 2014,
+				"rating": 8.6
+			}
+		]
+	},
+
+	// Application settings with duplicates
+	"settings": {
+		"theme": "dark", // UI theme preference: light or dark
+		"notifications": {
+			"email": true, // Email notifications enabled?
+			"sms": false // SMS notifications enabled?
+		},
+
+		// Duplicate key "theme"
+		"theme": "light" // This will override the previous theme (dark)
+	},
+
+	// Metadata about the JSON file
+	"metadata": {
+		"version": 1.0, // Version of the file structure
+		"lastUpdated": "2024-10-10T08:00:00Z", // Last update timestamp
+
+		// Duplicate key example - "version" is defined again
+		// This will overwrite the original "version" above
+		"version": 2.0 // Updated version number
+	}
+}
diff --git a/data/json.json b/data/json.json
new file mode 100644
index 0000000..212b771
--- /dev/null
+++ b/data/json.json
@@ -0,0 +1,4 @@
+{
+        "editor.fontSize": 14,
+        "files.autoSave": "onFocusChange"
+}
\ No newline at end of file
diff --git a/data/jsonc.json b/data/jsonc.json
new file mode 100644
index 0000000..6431056
--- /dev/null
+++ b/data/jsonc.json
@@ -0,0 +1,7 @@
+{
+        "key1": "value1",
+        "key2": 42,
+        // this is a comment
+        "key3": true
+        // "key4": false
+}
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..6e494fd
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,23 @@
+name: test
+
+on:
+    push:
+        branches: [ "master" ]
+    pull_request:
+        branches: [ "master" ]
+
+jobs:
+    test:
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v4
+            - name: Set up Python 3.10
+              uses: actions/setup-python@v3
+              with:
+                  python-version: "3.10"
+            - name: Install dependencies
+              run: |
+                  python -m pip install --upgrade pip
+                  pip install -r requirements.txt
+            - name: Run tests
+              run: pytest
\ No newline at end of file
diff --git a/pytest.ini b/pytest.ini
index be59a25..e053eb4 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -38,7 +38,7 @@ log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno
 log_cli_date_format = %Y-%m-%d %H:%M:%S
 
 # Disable cacheprovider to ensure tests always run
-cache_dir = /dev/null
+; cache_dir = /dev/null
 
 # Parallel execution (if you have pytest-xdist installed)
 # addopts = -n auto
@@ -48,6 +48,6 @@ minversion = 6.0
 required_plugins = pytest-cov pytest-mock
 
 # Environment variables
-env =
-    DATABASE_URL=sqlite:///test_db.sqlite
-    DEBUG=True
\ No newline at end of file
+; env =
+;     DATABASE_URL=sqlite:///test_db.sqlite
+;     DEBUG=True
\ No newline at end of file
diff --git a/tests/test_cleaners.py b/tests/test_cleaners.py
new file mode 100644
index 0000000..e135df2
--- /dev/null
+++ b/tests/test_cleaners.py
@@ -0,0 +1,25 @@
+import os
+import pytest
+
+from jsonmaestro import helpers, load_json, load_jsonc, save_json
+
+
+@pytest.mark.parametrize(
+    "file_name",
+    ["jsonc.json", "commented.json", "commented_with_duplicate.json"])
+def test_remove_comments(file_name):
+	if helpers.is_json(f"data/{file_name}"):
+		loader = load_json
+	else:
+		loader = load_jsonc
+
+	data = loader(f"data/{file_name}")
+	save_json(data=data, file_path=f"data/{file_name}.cleaned.json")
+
+	with open(f"data/{file_name}.cleaned.json", "r") as file:
+		data = file.read()
+		assert "//" not in data
+		assert "/*" not in data
+		assert "*/" not in data
+
+	os.remove(f"data/{file_name}.cleaned.json")
diff --git a/tests/test_format_helpers.py b/tests/test_format_helpers.py
index 18569a3..aabbbd3 100644
--- a/tests/test_format_helpers.py
+++ b/tests/test_format_helpers.py
@@ -9,31 +9,38 @@
 {
     "name": "John",
     "age": 30
+	// this is comment inside of the data
 }'''  # JSONC (JSON with comments, not valid JSON for is_json)
 
+
 # Test for `is_json`
-@pytest.mark.parametrize("file_content, expected_result", [
-    (valid_json_content, True),  # Valid JSON should return True
-    (invalid_json_content, False),  # Invalid JSON should return False
-    (jsonc_content, False),  # JSONC content should also return False
-])
+@pytest.mark.parametrize(
+    "file_content, expected_result",
+    [
+        (valid_json_content, True),  # Valid JSON should return True
+        (invalid_json_content, False),  # Invalid JSON should return False
+        (jsonc_content, False),  # JSONC content should also return False
+    ])
 def test_is_json(mocker, file_content, expected_result):
-    # Mock opening a file and returning the file_content
-    mocker.patch("builtins.open", mock_open(read_data=file_content))
+	# Mock opening a file and returning the file_content
+	mocker.patch("builtins.open", mock_open(read_data=file_content))
 
-    # Call is_json function with a mock file path
-    assert is_json("mock_file.json") == expected_result
+	# Call is_json function with a mock file path
+	assert is_json("mock_file.json") == expected_result
 
 
 # Test for `get_format`
-@pytest.mark.parametrize("file_content, expected_format", [
-    (valid_json_content, "json"),  # Valid JSON should return "json"
-    (invalid_json_content, "jsonc"),  # Invalid JSON should default to "jsonc"
-    (jsonc_content, "jsonc"),  # JSONC content should return "jsonc"
-])
+@pytest.mark.parametrize(
+    "file_content, expected_format",
+    [
+        (valid_json_content, "json"),  # Valid JSON should return "json"
+        (invalid_json_content,
+         "jsonc"),  # Invalid JSON should default to "jsonc"
+        (jsonc_content, "jsonc"),  # JSONC content should return "jsonc"
+    ])
 def test_get_format(mocker, file_content, expected_format):
-    # Mock opening a file and returning the file_content
-    mocker.patch("builtins.open", mock_open(read_data=file_content))
+	# Mock opening a file and returning the file_content
+	mocker.patch("builtins.open", mock_open(read_data=file_content))
 
-    # Call get_format function with a mock file path
-    assert get_format("mock_file.json") == expected_format
\ No newline at end of file
+	# Call get_format function with a mock file path
+	assert get_format("mock_file.json") == expected_format
diff --git a/tests/test_json_cleaner.py b/tests/test_json_cleaner.py
index 996e05d..95e603d 100644
--- a/tests/test_json_cleaner.py
+++ b/tests/test_json_cleaner.py
@@ -5,132 +5,100 @@
 from pathlib import Path
 from jsonmaestro import load_jsonc, load_json, remove_comments, remove_duplicate_keys, add_schema_keys, sort_json_keys
 
-def create_temp_json(tmp_path, content):
-    file_path = tmp_path / "test_file.json"
-    try:
-        with open(file_path, 'w', encoding='utf-8') as f:
-            json.dump(content, f)
-        return file_path
-    except IOError as e:
-        raise AssertionError(f"Failed to create temporary file: {e}")
-
-def test_load_jsonc(tmp_path):
-    content = '''
-    {
-        "key1": "value1",
-        "key2": 42
-    }
-    '''
-    file_path = create_temp_json(tmp_path, content)
-    result = load_jsonc(str(file_path))
-    assert result == {"key1": "value1", "key2": 42}
 
-def test_load_json(tmp_path):
-    content = '''
-    {
-        "editor.fontSize": 14,
-        "files.autoSave": "onFocusChange"
-    }
-    '''
-    file_path = create_temp_json(tmp_path, content)
-    result = load_json(str(file_path))
-    assert result == {"editor.fontSize": 14, "files.autoSave": "onFocusChange"}
+def create_temp_json(tmp_path, content):
+	file_path = tmp_path / "test_file.json"
+	try:
+		with open(file_path, 'w', encoding='utf-8') as f:
+			json.dump(content, f)
+		return file_path
+	except IOError as e:
+		raise AssertionError(f"Failed to create temporary file: {e}")
 
-def test_remove_comments():
-    input_data = {
-        "key1": "value1",
-        "key2": [1, 2, 3],
-        "key3": {"nested": "value"},
-        "commented_key": "This is a comment"
-    }
-    result = remove_comments(input_data)
-    expected_result = {
-        "key1": "value1",
-        "key2": [1, 2, 3],
-        "key3": {"nested": "value"}
-    }
-    assert result == expected_result
 
 def test_remove_duplicate_keys():
-    input_data = {
-        "key1": "value1",
-        "key2": "value2",
-        "key1": "value3"  # Duplicate key
-    }
-    result = remove_duplicate_keys(input_data)
-    expected_result = {"key1": "value3", "key2": "value2"}
-    assert result == expected_result
+	input_data = {
+	    "key1": "value1",
+	    "key2": "value2",
+	    "key1": "value3"  # Duplicate key
+	}
+	result = remove_duplicate_keys(input_data)
+	expected_result = {"key1": "value3", "key2": "value2"}
+	assert result == expected_result
+
 
 def test_add_schema_keys():
-    input_data = {}
-    result = add_schema_keys(input_data)
-    assert "json.schemas" in result
-    assert isinstance(result["json.schemas"], list)
-    assert len(result["json.schemas"]) > 0
+	input_data = {}
+	result = add_schema_keys(input_data)
+	assert "json.schemas" in result
+	assert isinstance(result["json.schemas"], list)
+	assert len(result["json.schemas"]) > 0
+
 
 def test_sort_json_keys():
-    input_data = {
-        "c": 3,
-        "a": 1,
-        "b": 2
-    }
-    result = sort_json_keys(input_data)
-    assert list(result.keys()) == ["a", "b", "c"]
+	input_data = {"c": 3, "a": 1, "b": 2}
+	result = sort_json_keys(input_data)
+	assert list(result.keys()) == ["a", "b", "c"]
+
+	result_reverse = sort_json_keys(input_data, reverse=True)
+	assert list(result_reverse.keys()) == ["c", "b", "a"]
 
-    result_reverse = sort_json_keys(input_data, reverse=True)
-    assert list(result_reverse.keys()) == ["c", "b", "a"]
 
 @pytest.fixture(scope='function')
 def cleanup_temp_files(tmp_path_factory):
-    yield
-    for file in tmp_path_factory.mktemp("cleanup").iterdir():
-        os.remove(file)
+	yield
+	for file in tmp_path_factory.mktemp("cleanup").iterdir():
+		os.remove(file)
+
 
 def test_cleanup_temp_files(cleanup_temp_files):
-    content = '''
+	content = '''
     {
         "test_key": "test_value"
     }
     '''
-    file_path = create_temp_json(tmp_path_factory.mktemp("test_dir"), content)
-    assert file_path.exists()
-    cleanup_temp_files
-    assert not file_path.exists()
+	file_path = create_temp_json(tmp_path_factory.mktemp("test_dir"), content)
+	assert file_path.exists()
+	cleanup_temp_files
+	assert not file_path.exists()
+
 
 def test_create_multiple_files(tmp_path_factory):
-    content1 = '{"key1": "value1"}'
-    content2 = '{"key2": "value2"}'
+	content1 = '{"key1": "value1"}'
+	content2 = '{"key2": "value2"}'
+
+	file1 = create_temp_json(tmp_path_factory.mktemp("test_dir_1"), content1)
+	file2 = create_temp_json(tmp_path_factory.mktemp("test_dir_2"), content2)
 
-    file1 = create_temp_json(tmp_path_factory.mktemp("test_dir_1"), content1)
-    file2 = create_temp_json(tmp_path_factory.mktemp("test_dir_2"), content2)
+	assert file1.exists()
+	assert file2.exists()
+	assert file1 != file2
 
-    assert file1.exists()
-    assert file2.exists()
-    assert file1 != file2
+	cleanup_temp_files()
+	assert not file1.exists()
+	assert not file2.exists()
 
-    cleanup_temp_files()
-    assert not file1.exists()
-    assert not file2.exists()
 
 def test_parse_json_content(tmp_path):
-    content = '''
+	content = '''
     {
         "key1": "value1",
         "key2": 42
     }
     '''
-    file_path = create_temp_json(tmp_path, content)
-    with open(file_path, 'r') as f:
-        parsed_content = json.load(f)
-    assert parsed_content == {"key1": "value1", "key2": 42}
+	file_path = create_temp_json(tmp_path, content)
+	with open(file_path, 'r') as f:
+		parsed_content = json.load(f)
+	assert parsed_content == {"key1": "value1", "key2": 42}
+
 
 def test_tempfile_usage():
-    with tempfile.TemporaryDirectory() as temp_dir:
-        file_path = Path(temp_dir) / "test_file.txt"
-        with file_path.open(mode="w") as f:
-            f.write("Hello, pytest!")
+	with tempfile.TemporaryDirectory() as temp_dir:
+		file_path = Path(temp_dir) / "test_file.txt"
+		with file_path.open(mode="w") as f:
+			f.write("Hello, pytest!")
 
-        assert file_path.exists()
-        assert file_path.read_text() == "Hello, pytest!"
+		assert file_path.exists()
+		assert file_path.read_text() == "Hello, pytest!"
 
-        # Cleanup happens automatically when exiting the context manager
+		# Cleanup happens automatically when exiting the context manager
diff --git a/tests/test_loaders.py b/tests/test_loaders.py
new file mode 100644
index 0000000..d508ee7
--- /dev/null
+++ b/tests/test_loaders.py
@@ -0,0 +1,35 @@
+import pytest
+from unittest import mock
+
+from jsonmaestro import load_jsonc, load_json
+
+
+@pytest.mark.parametrize("file_name", ["jsonc.json", "json.json"])
+def test_load_jsonc(file_name):
+	# we are mocking sys.exit to check if it has been called
+	with mock.patch("sys.exit") as mock_exit:
+		result = load_jsonc(f"data/{file_name}")
+		if file_name == "jsonc.json":
+			assert result == {"key1": "value1", "key2": 42, "key3": True}
+			mock_exit.assert_not_called()
+		elif file_name == "json.json":
+			assert result == {
+			    "editor.fontSize": 14,
+			    "files.autoSave": "onFocusChange"
+			}
+			mock_exit.assert_not_called()
+
+
+@pytest.mark.parametrize("file_name", ["jsonc.json", "json.json"])
+def test_load_json(file_name):
+	# we are mocking sys.exit to check if it has been called
+	with mock.patch("sys.exit") as mock_exit:
+		result = load_json(f"data/{file_name}")
+		if file_name == "jsonc.json":
+			mock_exit.assert_called_once_with(1)
+		elif file_name == "json.json":
+			assert result == {
+			    "editor.fontSize": 14,
+			    "files.autoSave": "onFocusChange"
+			}
+			mock_exit.assert_not_called()
diff --git a/tests/test_modules.py b/tests/test_modules.py
new file mode 100644
index 0000000..f0a504e
--- /dev/null
+++ b/tests/test_modules.py
@@ -0,0 +1,40 @@
+def test_import_helpers():
+	"""
+	Test if the helpers module can be imported and is not None.
+	"""
+	try:
+		# Attempt to import the module
+		import jsonmaestro.helpers as helpers
+
+		# Assert that the module is imported correctly (not None)
+		assert helpers is not None, "Failed to import helpers"
+
+	except ModuleNotFoundError as e:
+		# If the module cannot be found, fail the test
+		assert False, f"ModuleNotFoundError: {e}"
+
+
+def test_import_core():
+	"""
+		Test if the module containing core functionality can be imported and is not None.
+	"""
+	try:
+		import jsonmaestro.jsonmaestro as core
+
+		assert core is not None, "Failed to import core"
+
+	except ModuleNotFoundError as e:
+		assert False, f"ModuleNotFoundError: {e}"
+
+
+def test_import_jsonmaestro():
+	"""
+		Test if the jsonmaestro itself is importable
+	"""
+	try:
+		import jsonmaestro as jm
+
+		assert jm is not None, "Failed to import jsonmaestro"
+
+	except ModuleNotFoundError as e:
+		assert False, f"ModuleNotFoundError: {e}"
\ No newline at end of file