From c3f471f7145fbd5605677111f50e9db9f89cbab7 Mon Sep 17 00:00:00 2001 From: Kyle Cain Date: Tue, 6 Aug 2024 15:04:56 -0400 Subject: [PATCH] add ignore directories and prefixes --- README.md | 6 +- example_config.json | 4 +- replace_text/replace_text.py | 37 ++-- replace_text/tests/test_replace_text.py | 217 +++++++++++++++--------- 4 files changed, 170 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 614774a..8074e2e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This project replaces text in files based on a dictionary, given user input to s - Replace text in files based on dictionaries defined in a configuration file. - Allows user to specify the direction of replacement (keys-to-values or values-to-keys). -- Allows user to specify which file extensions to ignore. +- Allows user to specify which file extensions, file prefixes, or directories to ignore. - Automatically uses the only dictionary if only one is defined in the configuration file. ## Requirements @@ -59,7 +59,9 @@ This project replaces text in files based on a dictionary, given user input to s "python": "rocks" } }, - "ignore_extensions": [".png", ".jpg", ".gif"] + "ignore_extensions": [".exe", ".dll", ".bin"], + "ignore_directories": ["node_modules", "venv", ".git"], + "ignore_file_prefixes": [".", "_"] } ``` diff --git a/example_config.json b/example_config.json index 2ba3777..f1fde8a 100644 --- a/example_config.json +++ b/example_config.json @@ -11,5 +11,7 @@ "python": "rocks" } }, - "ignore_extensions": [".png", ".jpg", ".gif"] + "ignore_extensions": [".exe", ".dll", ".bin"], + "ignore_directories": ["node_modules", "venv", ".git"], + "ignore_file_prefixes": [".", "_"] } \ No newline at end of file diff --git a/replace_text/replace_text.py b/replace_text/replace_text.py index 0f2289c..9d464ee 100644 --- a/replace_text/replace_text.py +++ b/replace_text/replace_text.py @@ -28,13 +28,15 @@ def replace_text(direction: int, folder: str, dict_name: str) -> None: folder (str): Path to the folder containing text files. dict_name (str): Name of the dictionary to use from config.json. """ - # Load dictionaries and ignore extensions from config file + # Load dictionaries and configuration from config file with open("config.json", "r") as config_file: config = json.load(config_file) - # Retrieve the dictionaries and ignore extensions + # Retrieve the dictionaries and configuration options dictionaries = config.get("dictionaries", {}) ignore_extensions = config.get("ignore_extensions", []) + ignore_directories = config.get("ignore_directories", []) + ignore_file_prefixes = config.get("ignore_file_prefixes", []) if not dictionaries: print("No dictionaries found in config.json") @@ -59,22 +61,37 @@ def replace_text(direction: int, folder: str, dict_name: str) -> None: replacement_dict = {v: k for k, v in replacement_dict.items()} # Process each file in the folder - for root, _, files in os.walk(folder): + for root, dirs, files in os.walk(folder): + # Remove ignored directories from the dirs list + dirs[:] = [d for d in dirs if d not in ignore_directories] + for file in files: file_path = os.path.join(root, file) + + # Skip files with ignored extensions if any(file.endswith(ext) for ext in ignore_extensions): print(f"Skipped file (ignored extension): {file_path}") continue - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - for key, value in replacement_dict.items(): - content = content.replace(key, value) + # Skip files with ignored prefixes + if any(file.startswith(prefix) for prefix in ignore_file_prefixes): + print(f"Skipped file (ignored prefix): {file_path}") + continue - with open(file_path, "w", encoding="utf-8") as f: - f.write(content) + print(f"Processing file: {file}") + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + for key, value in replacement_dict.items(): + content = content.replace(key, value) - print(f"Processed file: {file_path}") + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + + print(f"Processed file: {file_path}") + except Exception as e: + print(f"Error processing file: {file}, continuing..") + continue if __name__ == "__main__": diff --git a/replace_text/tests/test_replace_text.py b/replace_text/tests/test_replace_text.py index 510ad24..68aa2b0 100644 --- a/replace_text/tests/test_replace_text.py +++ b/replace_text/tests/test_replace_text.py @@ -1,110 +1,165 @@ -import os -import unittest -from unittest.mock import patch, mock_open, call +import unittest, json, os from click.testing import CliRunner from replace_text.replace_text import replace_text class TestReplaceText(unittest.TestCase): - def assert_path_any_call(self, mock_obj, expected_path, mode, encoding): - normalized_expected_path = os.path.normpath(expected_path) - for mock_call in mock_obj.call_args_list: - args, kwargs = mock_call - if len(args) >= 1: - normalized_actual_path = os.path.normpath(args[0]) - if ( - normalized_actual_path == normalized_expected_path - and args[1] == mode - and kwargs.get("encoding") == encoding - ): - return - raise AssertionError( - f"Expected call not found: open('{normalized_expected_path}', '{mode}', encoding='{encoding}')" - ) + def setUp(self): + self.runner = CliRunner() + self.test_folder = "test_folder" + self.config_file = "config.json" + + # Create test folder and files + os.makedirs(self.test_folder, exist_ok=True) + with open(os.path.join(self.test_folder, "test1.txt"), "w") as f: + f.write("Hello world") + with open(os.path.join(self.test_folder, "test2.txt"), "w") as f: + f.write("Python is awesome") - @patch("builtins.open", new_callable=mock_open, read_data="key1 content key2") - @patch("os.walk") - @patch("json.load") - def test_replace_text_keys_to_values_single_dict( - self, mock_json_load, mock_os_walk, mock_file - ): - mock_json_load.return_value = { + # Create config file + config = { "dictionaries": { - "example1": {"key1": "value1", "key2": "value2", "key3": "value3"} + "test_dict": {"Hello": "Bonjour", "world": "monde", "Python": "Java"} }, - "ignore_extensions": [".png", ".jpg"], + "ignore_extensions": [".ignore"], + "ignore_directories": ["ignore_dir"], + "ignore_file_prefixes": ["ignore_"], } - mock_os_walk.return_value = [ - ("/mocked/path", ("subdir",), ("file1.txt", "file2.jpg")) - ] + with open(self.config_file, "w") as f: + json.dump(config, f) - runner = CliRunner() - result = runner.invoke( + def tearDown(self): + # Clean up test files and folders + for root, dirs, files in os.walk(self.test_folder, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(self.test_folder) + os.remove(self.config_file) + + def test_replace_text_keys_to_values(self): + result = self.runner.invoke( replace_text, - ["--direction", "1", "--folder", "/mocked/path", "--dict-name", "example1"], + [ + "--direction", + "1", + "--folder", + self.test_folder, + "--dict-name", + "test_dict", + ], ) - - self.assert_path_any_call(mock_file, "/mocked/path/file1.txt", "r", "utf-8") - self.assert_path_any_call(mock_file, "/mocked/path/file1.txt", "w", "utf-8") - mock_file().write.assert_called_with("value1 content value2") self.assertEqual(result.exit_code, 0) - @patch("builtins.open", new_callable=mock_open, read_data="value1 content value2") - @patch("os.walk") - @patch("json.load") - def test_replace_text_values_to_keys_multiple_dicts( - self, mock_json_load, mock_os_walk, mock_file - ): - mock_json_load.return_value = { - "dictionaries": { - "example1": {"key1": "value1", "key2": "value2", "key3": "value3"}, - "example2": {"hello": "world", "foo": "bar", "python": "rocks"}, - }, - "ignore_extensions": [".png", ".jpg"], - } - mock_os_walk.return_value = [ - ("/mocked/path", ("subdir",), ("file1.txt", "file2.jpg")) - ] + with open(os.path.join(self.test_folder, "test1.txt"), "r") as f: + content = f.read() + self.assertEqual(content, "Bonjour monde") - runner = CliRunner() - result = runner.invoke( + with open(os.path.join(self.test_folder, "test2.txt"), "r") as f: + content = f.read() + self.assertEqual(content, "Java is awesome") + + def test_replace_text_values_to_keys(self): + # First, replace keys with values + self.runner.invoke( replace_text, - ["--direction", "2", "--folder", "/mocked/path", "--dict-name", "example1"], + [ + "--direction", + "1", + "--folder", + self.test_folder, + "--dict-name", + "test_dict", + ], ) - self.assert_path_any_call(mock_file, "/mocked/path/file1.txt", "r", "utf-8") - self.assert_path_any_call(mock_file, "/mocked/path/file1.txt", "w", "utf-8") - mock_file().write.assert_called_with("key1 content key2") + # Then, test replacing values with keys + result = self.runner.invoke( + replace_text, + [ + "--direction", + "2", + "--folder", + self.test_folder, + "--dict-name", + "test_dict", + ], + ) self.assertEqual(result.exit_code, 0) - @patch("builtins.open", new_callable=mock_open, read_data="hello content foo") - @patch("os.walk") - @patch("json.load") - def test_replace_text_with_dict_name_flag( - self, mock_json_load, mock_os_walk, mock_file - ): - mock_json_load.return_value = { - "dictionaries": { - "example1": {"key1": "value1", "key2": "value2", "key3": "value3"}, - "example2": {"hello": "world", "foo": "bar", "python": "rocks"}, - }, - "ignore_extensions": [".png", ".jpg"], - } - mock_os_walk.return_value = [ - ("/mocked/path", ("subdir",), ("file1.txt", "file2.jpg")) - ] + with open(os.path.join(self.test_folder, "test1.txt"), "r") as f: + content = f.read() + self.assertEqual(content, "Hello world") + + with open(os.path.join(self.test_folder, "test2.txt"), "r") as f: + content = f.read() + self.assertEqual(content, "Python is awesome") - runner = CliRunner() - result = runner.invoke( + def test_ignore_extensions(self): + with open(os.path.join(self.test_folder, "test.ignore"), "w") as f: + f.write("Hello world") + + result = self.runner.invoke( replace_text, - ["--direction", "1", "--folder", "/mocked/path", "--dict-name", "example2"], + [ + "--direction", + "1", + "--folder", + self.test_folder, + "--dict-name", + "test_dict", + ], ) + self.assertEqual(result.exit_code, 0) + + with open(os.path.join(self.test_folder, "test.ignore"), "r") as f: + content = f.read() + self.assertEqual(content, "Hello world") # Content should remain unchanged + + def test_ignore_directories(self): + os.makedirs(os.path.join(self.test_folder, "ignore_dir"), exist_ok=True) + with open(os.path.join(self.test_folder, "ignore_dir", "test.txt"), "w") as f: + f.write("Hello world") - self.assert_path_any_call(mock_file, "/mocked/path/file1.txt", "r", "utf-8") - self.assert_path_any_call(mock_file, "/mocked/path/file1.txt", "w", "utf-8") - mock_file().write.assert_called_with("world content bar") + result = self.runner.invoke( + replace_text, + [ + "--direction", + "1", + "--folder", + self.test_folder, + "--dict-name", + "test_dict", + ], + ) self.assertEqual(result.exit_code, 0) + with open(os.path.join(self.test_folder, "ignore_dir", "test.txt"), "r") as f: + content = f.read() + self.assertEqual(content, "Hello world") # Content should remain unchanged + + def test_ignore_file_prefixes(self): + with open(os.path.join(self.test_folder, "ignore_test.txt"), "w") as f: + f.write("Hello world") + + result = self.runner.invoke( + replace_text, + [ + "--direction", + "1", + "--folder", + self.test_folder, + "--dict-name", + "test_dict", + ], + ) + self.assertEqual(result.exit_code, 0) + + with open(os.path.join(self.test_folder, "ignore_test.txt"), "r") as f: + content = f.read() + self.assertEqual(content, "Hello world") # Content should remain unchanged + if __name__ == "__main__": unittest.main()