Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/ramble/ramble/cmd/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def is_object(f):
"bin/**",
"lib/ramble/ramble/**",
"var/ramble/repos/**",
"share/ramble/scripts/**",
"conftest.py",
]

Expand Down Expand Up @@ -592,7 +593,9 @@ def run_mypy(mypy_cmd, file_list, args):
mypy_args.extend(get_tool_args(args, "mypy"))

if file_list:
mypy_files = [f for f in file_list if f.startswith("lib/ramble/ramble/")]
mypy_files = [
f for f in file_list if f.startswith(("lib/ramble/ramble/", "share/ramble/scripts/"))
]
if not mypy_files:
print_tool_result("mypy", 0)
return 0
Expand Down
103 changes: 103 additions & 0 deletions lib/ramble/ramble/test/util/cleaner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright 2022-2026 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.

import subprocess
import sys

import pytest

import ramble.util.cleaner as cleaner
import ramble.util.file_editor as file_editor


def test_cleaner_and_editor_script_getters():
cleaner_content = cleaner.get_cleaner_script()
assert "Ramble Directory Cleaner" in cleaner_content
assert cleaner.get_cleaner_exec_path() == "{workspace_shared}/utilities/_ramble_cleaner.py"

editor_content = file_editor.get_file_editor_script()
assert "Ramble File Editor" in editor_content
assert (
file_editor.get_file_editor_exec_path()
== "{workspace_shared}/utilities/_ramble_file_editor.py"
)


@pytest.fixture
def cleaner_script_path(tmpdir):
script_path = tmpdir.join("test_cleaner.py")
script_path.write(cleaner.get_cleaner_script())
return str(script_path)


@pytest.fixture
def run_cleaner(cleaner_script_path):
def _run(*args):
return subprocess.run(
[sys.executable, cleaner_script_path] + list(args),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)

return _run


def test_cleaner_script_execution_non_recursive(tmpdir, run_cleaner):
# Setup test files
dir_path = tmpdir.mkdir("test_clean_dir")
file_keep = dir_path.join("keep.txt")
file_keep.write("keep me")
file_remove = dir_path.join("remove.tmp")
file_remove.write("delete me")

res = run_cleaner(
"--directory",
str(dir_path),
"--regex",
r".*\.tmp$",
)

assert res.returncode == 0
assert file_keep.exists()
assert not file_remove.exists()


def test_cleaner_script_execution_recursive(tmpdir, run_cleaner):
# Setup test nested structure
dir_path = tmpdir.mkdir("test_recursive_dir")
subdir = dir_path.mkdir("sub")
file_sub_keep = subdir.join("keep.txt")
file_sub_keep.write("keep me")
file_sub_del = subdir.join("delete.log")
file_sub_del.write("delete me")

res = run_cleaner(
"--directory",
str(dir_path),
"--regex",
r".*\.log$",
"--recurse",
)

assert res.returncode == 0
assert file_sub_keep.exists()
assert not file_sub_del.exists()


def test_cleaner_script_invalid_regex(tmpdir, run_cleaner):
dir_path = tmpdir.mkdir("test_invalid_dir")
res = run_cleaner(
"--directory",
str(dir_path),
"--regex",
r"[invalid",
)
assert res.returncode == 1
assert "Invalid regex pattern" in res.stderr
90 changes: 12 additions & 78 deletions lib/ramble/ramble/util/cleaner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,90 +6,24 @@
# option. This file may not be copied, modified, or distributed
# except according to those terms.


def get_cleaner_exec_path():
"""Returns the path to the cleaner script to use"""
return f"{{workspace_shared}}/utilities/{HELPER_SCRIPT_NAME}"


def get_cleaner_script():
"""Returns the content of the standalone cleaner script"""

return r"""# Copyright 2022-2026 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.

import os
import re
import shutil
import sys
import argparse

def matches_whole_path(regex, path):
return regex.fullmatch(path) is not None

def perform_cleanup(args):
directory = args.directory
pattern = args.regex
recurse = args.recurse

if not os.path.isdir(directory):
print(f"Directory {directory} does not exist. Skipping cleanup.", file=sys.stderr)
return 0

try:
regex = re.compile(pattern)
except re.error as e:
print(f"Invalid regex pattern '{pattern}': {e}", file=sys.stderr)
return 1
import ramble.paths

paths_to_delete = []
if recurse:
for root, dirs, files in os.walk(directory, topdown=True):
for d in list(dirs):
d_path = os.path.join(root, d)
if matches_whole_path(regex, d_path):
paths_to_delete.append(d_path)
dirs.remove(d)
for f in files:
f_path = os.path.join(root, f)
if matches_whole_path(regex, f_path):
paths_to_delete.append(f_path)
else:
try:
for entry in os.scandir(directory):
if matches_whole_path(regex, entry.path):
paths_to_delete.append(entry.path)
except Exception as e:
print(f"Error scanning directory {directory}: {e}", file=sys.stderr)
return 1
HELPER_SCRIPT_NAME = "_ramble_cleaner.py"

for path in paths_to_delete:
try:
if os.path.islink(path):
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
except Exception as e:
print(f"Error removing {path}: {e}", file=sys.stderr)

return 0
def get_cleaner_source_path():
"""Returns the source path to the cleaner script in Ramble's share directory"""
return os.path.join(ramble.paths.share_path, "scripts", HELPER_SCRIPT_NAME)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Ramble Directory Cleaner')
parser.add_argument('--directory', required=True, help='Directory to clean')
parser.add_argument('--regex', required=True, help='Regex pattern to match paths')
parser.add_argument('--recurse', action='store_true', help='Search recursively')

args = parser.parse_args()
sys.exit(perform_cleanup(args))
"""
def get_cleaner_exec_path():
"""Returns the path to the cleaner script to use in a workspace"""
return f"{{workspace_shared}}/utilities/{HELPER_SCRIPT_NAME}"


HELPER_SCRIPT_NAME = "_ramble_cleaner.py"
def get_cleaner_script():
"""Returns the content of the standalone cleaner script"""
with open(get_cleaner_source_path(), encoding="utf-8") as f:
return f.read()
124 changes: 13 additions & 111 deletions lib/ramble/ramble/util/file_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,123 +6,25 @@
# option. This file may not be copied, modified, or distributed
# except according to those terms.


def get_file_editor_exec_path():
"""Returns the path to the file editor script to use"""
return f"{{workspace_shared}}/utilities/{HELPER_SCRIPT_NAME}"


def get_file_editor_script():
"""Returns the content of the standalone file editor script"""

return r"""# Copyright 2022-2026 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.


import re
import os
import argparse
import sys
import shutil

def edit_file(args):
if not os.path.exists(args.file):
content = ""
else:
# Use newline='' to preserve original line endings across platforms
with open(args.file, 'r', newline='', encoding='utf-8') as f:
content = f.read()

def unescape(s):
if s is None:
return None
return s.encode('utf-8').decode('unicode_escape')

new_content = content
if args.mode == 'regex':
if args.function:
func = None
if args.import_module:
import importlib.util
if not os.path.exists(args.import_module):
print(f"Error: Module file {args.import_module} not found.")
return 1
spec = importlib.util.spec_from_file_location(
"custom_functions", args.import_module
)
custom_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(custom_module)
func = getattr(custom_module, args.function, None)
else:
func = globals().get(args.function)

if func:
new_content = func(content)
if not isinstance(new_content, str):
print(
f"Error: Function {args.function} must return a string, "
f"not {type(new_content).__name__}"
)
return 1
else:
print(f"Error: Function {args.function} not found.")
return 1
import ramble.paths

if args.match and args.replace is not None:

new_content = re.sub(args.match, unescape(args.replace), new_content)
if args.append:
new_content = new_content + unescape(args.append)
if args.prepend:
new_content = unescape(args.prepend) + new_content
elif args.mode == 'patch':
if not shutil.which('patch'):
print("Error: 'patch' utility not found.")
print("On Windows, please ensure a tool providing 'patch' "
"(e.g., Git Bash, MSYS2, Cygwin) is installed and in your PATH.")
return 1

import subprocess
try:
subprocess.run(['patch', args.file, args.patch_file], check=True)
return 0
except Exception as e:
print(f"Error applying patch: {e}")
return 1
HELPER_SCRIPT_NAME = "_ramble_file_editor.py"
CUSTOM_EDIT_FUNCTIONS_NAME = "custom_edit_functions.py"

needs_write = new_content != content or not os.path.exists(args.file)
if needs_write:
# Ensure the directory exists
os.makedirs(os.path.dirname(os.path.abspath(args.file)), exist_ok=True)
with open(args.file, 'w', newline='', encoding='utf-8') as f:
f.write(new_content)

return 0
def get_file_editor_source_path():
"""Returns the source path to the file editor script in Ramble's share directory"""
return os.path.join(ramble.paths.share_path, "scripts", HELPER_SCRIPT_NAME)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Ramble File Editor')
parser.add_argument('--mode', choices=['regex', 'patch'], required=True)
parser.add_argument('--file', required=True)
parser.add_argument('--match', help='Regex pattern to match')
parser.add_argument('--replace', help='Replacement string')
parser.add_argument('--append', help='String to append')
parser.add_argument('--prepend', help='String to prepend')
parser.add_argument('--patch-file', help='Path to patch file')
parser.add_argument('--function', help='Name of custom function to execute')
parser.add_argument(
'--import-module',
help='Path to python module containing custom functions'
)

args = parser.parse_args()
sys.exit(edit_file(args))
"""
def get_file_editor_exec_path():
"""Returns the path to the file editor script to use in a workspace"""
return f"{{workspace_shared}}/utilities/{HELPER_SCRIPT_NAME}"


HELPER_SCRIPT_NAME = "_ramble_file_editor.py"
CUSTOM_EDIT_FUNCTIONS_NAME = "custom_edit_functions.py"
def get_file_editor_script():
"""Returns the content of the standalone file editor script"""
with open(get_file_editor_source_path(), encoding="utf-8") as f:
return f.read()
16 changes: 6 additions & 10 deletions lib/ramble/ramble/workspace/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,23 +823,19 @@ def write_utilities(self):
if ramble.config.get("config:generate_file_editing_scripts", True):
fs.mkdirp(self.shared_utilities_dir)

# Write to base shared utilities directory
base_script_content = ramble.util.file_editor.get_file_editor_script()
# Copy file editor helper script to workspace shared utilities
base_script_path = os.path.join(
self.shared_utilities_dir, ramble.util.file_editor.HELPER_SCRIPT_NAME
)
with open(base_script_path, "w+", encoding="utf-8") as f:
f.write(base_script_content)
shutil.copyfile(
ramble.util.file_editor.get_file_editor_source_path(), base_script_path
)

# Write cleaner utility script
# TODO: Currently this is guarded by the generate_file_editing_scripts
# config. Should it use its own config setting?
cleaner_script_content = ramble.util.cleaner.get_cleaner_script()
# Copy cleaner helper script to workspace shared utilities
cleaner_script_path = os.path.join(
self.shared_utilities_dir, ramble.util.cleaner.HELPER_SCRIPT_NAME
)
with open(cleaner_script_path, "w+", encoding="utf-8") as f:
f.write(cleaner_script_content)
shutil.copyfile(ramble.util.cleaner.get_cleaner_source_path(), cleaner_script_path)

def write_auxiliary_software_files(self):
"""Write all auxiliary software files out to workspace"""
Expand Down
Loading
Loading