mirror of
https://github.com/kevinveenbirkenbach/bulk-string-replacer.git
synced 2025-07-31 09:31:09 +02:00
Added test_.py handling
This commit is contained in:
parent
40ff300a92
commit
82784bdcff
79
main.py
79
main.py
@ -2,6 +2,7 @@
|
||||
import os
|
||||
import argparse
|
||||
|
||||
|
||||
def replace_content(path, old_string, new_string, preview, verbose):
|
||||
"""
|
||||
Replace occurrences of old_string with new_string inside the file at path.
|
||||
@ -18,25 +19,37 @@ def replace_content(path, old_string, new_string, preview, verbose):
|
||||
except UnicodeDecodeError:
|
||||
print_verbose(f"Warning: Unicode decode error in file {path}. Skipping.", verbose)
|
||||
|
||||
|
||||
def print_verbose(message, verbose):
|
||||
if verbose:
|
||||
print(message)
|
||||
|
||||
def process_directory(base_path, old_string, new_string, recursive,
|
||||
rename_folders, rename_files, replace_in_content,
|
||||
preview, verbose, include_hidden, rename_paths, auto_path):
|
||||
|
||||
def process_directory(
|
||||
base_path,
|
||||
old_string,
|
||||
new_string,
|
||||
recursive,
|
||||
rename_folders,
|
||||
rename_files,
|
||||
replace_in_content,
|
||||
preview,
|
||||
verbose,
|
||||
include_hidden,
|
||||
rename_paths,
|
||||
auto_path
|
||||
):
|
||||
"""
|
||||
Traverse directory tree and perform operations based on flags:
|
||||
Traverse the directory tree and perform operations based on flags:
|
||||
- replace_in_content: replace inside file contents
|
||||
- rename_files: rename files whose names contain old_string
|
||||
- rename_folders: rename folders whose names contain old_string
|
||||
- rename_paths: match old_string as a relative path and move matching items to new_string path.
|
||||
When rename_paths is set, for each matching line in Python files, show 3 lines of context before and after,
|
||||
and prompt whether to apply slash-based or dot-based replacement, unless auto_path is True.
|
||||
- rename_paths: treat old_string as a relative path and move matching items to new_string path.
|
||||
When rename_paths is set, prompts occur for Python files unless auto_path is True.
|
||||
Additionally, for test_*.py files when auto_path is False, prompts to extract 'test_' prefix into a subdirectory.
|
||||
"""
|
||||
# Full-path move logic
|
||||
if rename_paths:
|
||||
# Move matching files and folders
|
||||
# Full-path moves and optional test_ prefix extraction
|
||||
for root, dirs, files in os.walk(base_path):
|
||||
if not include_hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
@ -46,6 +59,29 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
rel = os.path.relpath(full_src, base_path)
|
||||
if old_string in rel:
|
||||
new_rel = rel.replace(old_string, new_string)
|
||||
|
||||
# Special handling for test_*.py when auto_path is False
|
||||
if (
|
||||
os.path.isfile(full_src)
|
||||
and name.startswith('test_')
|
||||
and name.endswith('.py')
|
||||
and not auto_path
|
||||
):
|
||||
choice = None
|
||||
while choice not in ('y', 'n'):
|
||||
choice = input(
|
||||
f"File {full_src} is a test module. "
|
||||
"Extract 'test_' prefix into subdirectory? [y/N]: "
|
||||
).strip().lower() or 'n'
|
||||
if choice == 'y':
|
||||
rel_dir = os.path.dirname(rel)
|
||||
orig_basename = os.path.basename(rel)
|
||||
base_noext = orig_basename[:-3] # remove .py
|
||||
subdir_name = base_noext[len('test_'):]
|
||||
new_name_part = os.path.basename(new_string)
|
||||
new_filename = f"test_{new_name_part}.py"
|
||||
new_rel = os.path.join(rel_dir, subdir_name, new_filename)
|
||||
|
||||
full_dst = os.path.join(base_path, new_rel)
|
||||
print_verbose(f"Moving {full_src} → {full_dst}", verbose)
|
||||
if not preview:
|
||||
@ -53,7 +89,8 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
os.rename(full_src, full_dst)
|
||||
if not recursive:
|
||||
break
|
||||
# Line-by-line replacement in Python files
|
||||
|
||||
# Line-by-line content replacement in Python files
|
||||
for root, dirs, files in os.walk(base_path):
|
||||
if not include_hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
@ -61,7 +98,7 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
for f in files:
|
||||
if f.endswith('.py'):
|
||||
file_path = os.path.join(root, f)
|
||||
print_verbose(f"Processing Python file for path replacement: {file_path}", verbose)
|
||||
print_verbose(f"Processing Python file: {file_path}", verbose)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as ff:
|
||||
lines = ff.readlines()
|
||||
@ -73,14 +110,13 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
changed = False
|
||||
for idx, line in enumerate(lines):
|
||||
if old_slash in line:
|
||||
# Show context
|
||||
start = max(0, idx - 3)
|
||||
end = min(len(lines), idx + 4)
|
||||
print(f"\nContext for replacement in {file_path}, line {idx+1}:")
|
||||
print(f"\nContext in {file_path}, line {idx+1}:")
|
||||
for i in range(start, end):
|
||||
prefix = '>' if i == idx else ' '
|
||||
print(f"{prefix} {i+1}: {lines[i].rstrip()}")
|
||||
# Determine replacement style
|
||||
|
||||
if auto_path:
|
||||
choice = '1'
|
||||
else:
|
||||
@ -103,23 +139,22 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
fw.writelines(lines)
|
||||
if not recursive:
|
||||
break
|
||||
# Only return early when only path-mode is active
|
||||
|
||||
# Exit early if only path-mode is requested
|
||||
if not (rename_files or replace_in_content):
|
||||
return
|
||||
|
||||
# Collect folder renames to apply after traversal
|
||||
# Fallback operations: replace content, rename files, rename folders
|
||||
folders_to_rename = []
|
||||
for root, dirs, files in os.walk(base_path):
|
||||
if not include_hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
files = [f for f in files if not f.startswith('.')]
|
||||
|
||||
# Content replacement
|
||||
if replace_in_content:
|
||||
for f in files:
|
||||
replace_content(os.path.join(root, f), old_string, new_string, preview, verbose)
|
||||
|
||||
# File renaming
|
||||
if rename_files:
|
||||
for f in files:
|
||||
if old_string in f:
|
||||
@ -129,7 +164,6 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
if not preview:
|
||||
os.rename(src, dst)
|
||||
|
||||
# Gather folder renames
|
||||
if rename_folders:
|
||||
for d in dirs:
|
||||
if old_string in d:
|
||||
@ -140,12 +174,12 @@ def process_directory(base_path, old_string, new_string, recursive,
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
# Apply folder renames
|
||||
for src, dst in folders_to_rename:
|
||||
print_verbose(f"Renaming directory: {src} → {dst}", verbose)
|
||||
if not preview:
|
||||
os.rename(src, dst)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bulk string replacer with optional full-path moves."
|
||||
@ -165,9 +199,9 @@ def main():
|
||||
parser.add_argument('-p', '--preview', action='store_true',
|
||||
help="Preview only; no changes.")
|
||||
parser.add_argument('-P', '--path', dest='rename_paths', action='store_true',
|
||||
help="Match old_string as relative path and move to new_string path.")
|
||||
help="Treat old_string as path and move matching items.")
|
||||
parser.add_argument('-y', '--yes', dest='auto_path', action='store_true',
|
||||
help="Skip prompts in Python files and apply slash-based replacement by default.")
|
||||
help="Skip prompts and apply default replacements.")
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help="Verbose mode.")
|
||||
parser.add_argument('-H', '--hidden', action='store_true',
|
||||
@ -175,7 +209,6 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Disallow using --path and --folders together
|
||||
if args.rename_paths and args.folders:
|
||||
parser.error("Cannot use --path and --folders together.")
|
||||
|
||||
|
51
test.py
51
test.py
@ -22,6 +22,57 @@ class TestBulkStringReplacer(unittest.TestCase):
|
||||
f.write(content)
|
||||
return full
|
||||
|
||||
def test_test_prefix_extraction_prompt_no_auto(self):
|
||||
# Setup a test file with test_ prefix
|
||||
src = self.create_file('dir/test_module.py', 'print("hello")')
|
||||
# Monkeypatch input to simulate 'y'
|
||||
inputs = iter(['y'])
|
||||
original_input = __builtins__['input']
|
||||
__builtins__['input'] = lambda _: next(inputs)
|
||||
try:
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='module',
|
||||
new_string='module_new',
|
||||
recursive=True,
|
||||
rename_folders=False,
|
||||
rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False,
|
||||
verbose=False,
|
||||
include_hidden=True,
|
||||
rename_paths=True,
|
||||
auto_path=False
|
||||
)
|
||||
finally:
|
||||
__builtins__['input'] = original_input
|
||||
|
||||
# After extraction, expect dir/module/test_module_new.py
|
||||
dst = os.path.join(self.base, 'dir', 'module', 'test_module_new.py')
|
||||
self.assertTrue(os.path.exists(dst))
|
||||
|
||||
def test_test_prefix_default_no_extraction_auto(self):
|
||||
# Setup a test file with test_ prefix
|
||||
src = self.create_file('dir/test_file.py', 'print("hello")')
|
||||
process_directory(
|
||||
base_path=self.base,
|
||||
old_string='file',
|
||||
new_string='file_new',
|
||||
recursive=True,
|
||||
rename_folders=False,
|
||||
rename_files=False,
|
||||
replace_in_content=False,
|
||||
preview=False,
|
||||
verbose=False,
|
||||
include_hidden=True,
|
||||
rename_paths=True,
|
||||
auto_path=True
|
||||
)
|
||||
# With auto_path, should do simple path replace: dir/test_file.py -> dir/test_file_new.py
|
||||
dst = os.path.join(self.base, 'dir', 'test_file_new.py')
|
||||
self.assertTrue(os.path.exists(dst))
|
||||
|
||||
|
||||
def test_replace_content(self):
|
||||
f = self.create_file('foo.txt', 'hello OLD world')
|
||||
replace_content(f, 'OLD', 'NEW', preview=False, verbose=False)
|
||||
|
Loading…
x
Reference in New Issue
Block a user