Skip to content

Commit

Permalink
add ignore directories and prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
cainky committed Aug 6, 2024
1 parent 29b7e64 commit c3f471f
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 94 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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": [".", "_"]
}
```

Expand Down
4 changes: 3 additions & 1 deletion example_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [".", "_"]
}
37 changes: 27 additions & 10 deletions replace_text/replace_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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__":
Expand Down
217 changes: 136 additions & 81 deletions replace_text/tests/test_replace_text.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit c3f471f

Please sign in to comment.