diff --git a/.gitignore b/.gitignore index d730361..aeb5943 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -# file-renamer project-specific +# file-manager project-specific testing_grounds/ +preset/ + +# configuration files +settings/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/file-renamer.iml b/.idea/file-manager.iml similarity index 54% rename from .idea/file-renamer.iml rename to .idea/file-manager.iml index 74d515a..5319422 100644 --- a/.idea/file-renamer.iml +++ b/.idea/file-manager.iml @@ -3,6 +3,11 @@ + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 959ac8d..1caff08 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 70f2d55..1a85d97 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..a8d5688 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1697301999451 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/folder_icon.png b/assets/folder_icon.png new file mode 100644 index 0000000..51e4a29 Binary files /dev/null and b/assets/folder_icon.png differ diff --git a/common.py b/common.py new file mode 100644 index 0000000..52cd595 --- /dev/null +++ b/common.py @@ -0,0 +1,219 @@ +# This is a project for an automated file renamer +# TODO: make the empty-prefixed calls identity operations for those base directories + +import os +# from exceptions import EmptyArgsException +from exceptions import EmptyArgsException + +# Rules for prefixes: +# 1. File names are obligated to start with a prefix if there is one. +# 2. Prefixes can't contain any whitespaces, underscores ('_') or digits. +# 3. Prefixes are obligated to be terminated with an underscore + + +def validate_prefix(prefix): + return ''.join((char for char in prefix if char not in ' _0123456789')) # Deleting unwanted characters + + +def validate_args(**args): + empty_args = [] + for key, value in args.items(): + if value == '' or value is None: + empty_args.append(key) + if empty_args: + raise EmptyArgsException(empty_args) + + +def find_end_of_prefix(file): + for i in range(len(file)): + if file[i].isdigit(): + return -1 # No prefix found + elif file[i] == '_': + return i + return -1 # No prefix found + + +def find_end_of_exercise_number_from_file(file): + prefix_end = find_end_of_prefix(file) + 1 + for i in range(prefix_end, len(file)): + if file[i].isalpha(): + return prefix_end # No exercise number found + elif file[i] == '_': + return i + return prefix_end # No exercise number found + + +def find_end_of_exercise_number_from_directory(directory): + return os.path.basename(directory).split('_')[1] + + +def main_file_name(directory, prefix=''): + res = os.path.basename(directory) + pos = find_end_of_prefix(res) + 1 + res = res[:pos] + 'f_' + res[pos:] + # for i in reversed(range(len(res))): + # if res[i] == '_': + # res = res[:i+1] + prefix + 'f_' + res[i+1:] + return res + + +def renamer(base_dir, prefix, problem_name=None): + prefix = validate_prefix(prefix) + for directory in os.listdir(base_dir): + prefix_end = find_end_of_prefix(directory) + 1 + new_dir_name = base_dir + '/' + prefix + '_' + directory[prefix_end:] + directory = base_dir + '/' + directory + if os.path.isdir(directory): + if 'common' not in os.path.basename(directory): + # TODO: consider a more elegant solution using the split() method + for file in os.listdir(directory): + prefix_end = find_end_of_prefix(file) + 1 + new_file_name = directory + '/' + prefix + '_' + file[prefix_end:] + os.rename(directory + '/' + file, new_file_name) # Adding a prefix + # TODO: probably worth it to find a good way to get rid of this 'if' block + # Checking for if the character at the prefix_end is a letter prevents us from adding a + # problem number for no good reason + if file[prefix_end].isalpha(): # Adding a corresponding number to the files with solutions + # tmp = '' + # for char in os.path.basename(directory): + # # TODO: introduce a flag to check if we have stumbled upon a number, and use it to stop the + # # loop when we stumble upon an underscore + # if char.isdigit(): + # tmp += char + # if tmp != '': + # tmp += '_' + problem_number = find_end_of_exercise_number_from_directory(directory) + + # new_name = file[:prefix_end] + problem_number + file[ + # prefix_end:] # The 'cutting' point should not be at index 3, but after + # # the prefix + + if 'test' in os.path.basename(file): + file_data = '' + with open(directory + '/' + file, 'r') as f: + file_data = f.read() + + pos_1 = file_data.find('from') + 5 + pos_2 = file_data.find('import', pos_1) - 1 + file_data = file_data[:pos_1] + main_file_name(directory) + file_data[pos_2:] + + with open(directory + '/' + file, 'w') as f: + f.write(file_data) + + new_name = file[:prefix_end] + problem_number + file[ + prefix_end:] # The 'cutting' point should not be at index 3, but after + # the prefix + else: + new_name = file[:prefix_end] + problem_number + 'f_' + file[ + prefix_end:] # The 'cutting' point should not be at index 3, but after + # the prefix + os.rename(directory + '/' + file, directory + '/' + new_name) + + + + + os.rename(directory, new_dir_name) + + +# TODO: unify this with the renamer function +def imp_adjustment(base_dir, prefix=None, problem_name=None): + for directory in os.listdir(base_dir): + directory = base_dir + '/' + directory + if os.path.isdir(directory): + if 'common' in os.path.basename(directory): + continue + for file in os.listdir(directory): + # A check that makes sure that we are dealing with a test file + if 'test' in file: + file_data = '' + with open(directory + '/' + file, 'r') as f: + file_data = f.read() + + pos_1 = file_data.find('from') + 5 + pos_2 = file_data.find('import', pos_1) - 1 + file_data = file_data[:pos_1] + main_file_name(directory) + file_data[pos_2:] + + with open(directory + '/' + file, 'w') as f: + f.write(file_data) + + else: + end_of_exercise_number = find_end_of_exercise_number_from_file(file) + # In case that we have already marked the file as a file with 'f_', we don't do that again + if file[end_of_exercise_number:end_of_exercise_number + 2] != 'f_': + new_name = file[:end_of_exercise_number] + 'f_' + file[end_of_exercise_number:] + os.rename(directory + '/' + file, directory + '/' + new_name) + # else: + # for i in reversed(range(len(file))): + # if file[i] == '_': + # new_name = file[:i + 1] + 'f_' + file[i + 1:] + # os.rename(directory + '/' + file, directory + '/' + new_name) + # break + + +# The function for transforming names of problems into appropriate file names +# TODO: currently, only supports Leetcode. Need to make it more general. +def get_file_name(problem_name): + trans_dict = str.maketrans(' ', '_', '.') + res = problem_name.translate(trans_dict).lower() + res = res[:res.find('_') + 1] + 'f_' + res[res.find('_') + 1:] + return res + + +# TODO: check for errors using a decorator? +def file_creation(base_dir, prefix, problem_name): + try: + validate_args(base_dir=base_dir, prefix=prefix, problem_name=problem_name) + except EmptyArgsException: + return + + prefix = validate_prefix(prefix) + + main_file_name = prefix + '_' + get_file_name(problem_name) + directory_name = os.path.join(base_dir, main_file_name.replace('f_', '')) + test_file_name = main_file_name[:find_end_of_exercise_number_from_file(main_file_name)] + '_test' + + if not os.path.isdir(directory_name): + os.mkdir(directory_name) + + try: + with open(directory_name + '/' + main_file_name + '.py', 'x') as f: + pass + except FileExistsError: + pass + + with open(directory_name + '/' + test_file_name + '.py', 'w') as new_test_file: + try: + with open(base_dir + f'/{prefix}_common/{prefix}_test_template.py', 'r') as template: + for line in template: + new_test_file.write(line) + except FileNotFoundError: + pass + + +METHODS_DICT = {'renamer': renamer, 'import_adjustment': imp_adjustment, 'file_creation': file_creation} + + +def general_function_handler(method, base_dir, prefix, problem_name): + METHODS_DICT[method](base_dir, prefix, problem_name) + + +if __name__ == '__main__': + # TODO: consider generalising the handling of the method prompt using the function dictionary + # function_dict = {'renamer': renamer, 'import_adjustment': imp_adjustment, 'file_creation': file_creation} + + # TODO: consider allowing users to save some parameters during execution + while True: + + method = str(input("Method: ")) + if method == 'renamer': + base_dir = str(input("Base directory: ")) + prefix = str(input("Prefix: ")) + renamer(base_dir, prefix) + elif method == 'import_adjustment': + base_dir = str(input("Base directory: ")) + imp_adjustment(base_dir) + elif method == 'file_creation': + base_dir = str(input("Base directory: ")) + prefix = str(input("Prefix: ")) + problem_name = str(input("Problem name: ")) + file_creation(base_dir, prefix, problem_name) \ No newline at end of file diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..bc63f23 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,10 @@ +class BaseException(Exception): + # Base class for exceptions in this project + pass + + +class EmptyArgsException(BaseException): + # Exception raised when some of the arguments passed are empty strings or None + def __init__(self, empty_args): + self.empty_args = empty_args + self.message = 'the following necessary fields are empty: ' + ', '.join(empty_args) \ No newline at end of file diff --git a/main.py b/main.py index 00eae00..ff64ecd 100644 --- a/main.py +++ b/main.py @@ -1,33 +1,121 @@ -# This is a project for an automated file renamer -# TODO: implement GUI -# TODO: make the empty-prefixed calls identity operations for those base directories - -import os - - -def renamer(prefix, base_dir): - for directory in os.listdir(base_dir): - if os.path.isdir(base_dir + '/' + directory): - for file in os.listdir(base_dir + '/' + directory): - if prefix != '': - new_file_name = base_dir + '/' + directory + '/' + prefix + '_' + file - os.rename(base_dir + '/' + directory + '/' + file, new_file_name) # Adding a prefix - # TODO: probably worth it to find a good way to get rid of this 'if' block - if file[3].isalpha(): # Adding a corresponding number to the files with solutions - tmp = '' - for char in directory: - if char.isdigit(): - tmp += char - if tmp != '': - tmp += '_' - new_name = file[:3] + tmp + file[3:] # The 'cutting' point should not be at index 3, but after - # the prefix - os.rename(base_dir + '/' + directory + '/' + file, base_dir + '/' + directory + '/' + new_name) - new_dir_name = base_dir + '/' + prefix + '_' + directory - os.rename(base_dir + '/' + directory, new_dir_name) +import os.path +import tkinter as tk +from tkinter import filedialog +from common import general_function_handler +from PIL import Image, ImageTk + + +# Master section +root = tk.Tk() +root.title('File Manager') +root.minsize(400, 250) + +# TODO: consider organising things into classes + +# Frame section +# TODO: make the font size bigger, and make the sizes of other elements depend on the font size +# # Main body frame +# main_body_frame = tk.Frame(root) +# main_body_frame.pack(padx=50, pady=10, fill='x') +# for r in range(2): main_body_frame.rowconfigure(index=r, weight=1) +# Label-and-entry frame +label_and_entry_frame = tk.Frame(root, relief='raised', borderwidth=5) # TODO: remove relief and borderwidth after done testing +# label_and_entry_frame.grid(row=0, column=0, sticky='nsew') +label_and_entry_frame.pack(padx=50, pady=10, fill='x') +for c in range(4): label_and_entry_frame.columnconfigure(index=c, weight=1) +for r in range(6): label_and_entry_frame.rowconfigure(index=r, weight=1) +label_and_entry_frame.columnconfigure(index=1, weight=2) +label_and_entry_frame.rowconfigure(index=4, minsize=16) +# # Preset frame +# preset_frame = tk.Frame(main_body_frame) +# preset_frame.grid(row=1, column=0, sticky='nsew') +# for c in range(4): +# bottom frame +bottom_frame = tk.Frame(root) +bottom_frame.pack(side='bottom', fill='x') + +# Label section +method_label = tk.Label(label_and_entry_frame, text='Method: ') +method_label.grid(row=0, column=0, sticky='E') +base_dir_label = tk.Label(label_and_entry_frame, text='Base directory: ') +base_dir_label.grid(row=1, column=0, sticky='E') +prefix_label = tk.Label(label_and_entry_frame, text='Prefix: ') +prefix_label.grid(row=2, column=0, sticky='E') +problem_name_label = tk.Label(label_and_entry_frame, text='Problem name: ') +problem_name_label.grid(row=3, column=0, sticky='E') +preset_name_label = tk.Label(label_and_entry_frame, text='Preset name: ') +preset_name_label.grid(row=5, column=0, sticky='E') + +# Entry section +# TODO: make method_entry a drop-down menu +method_entry = tk.Entry(label_and_entry_frame) +method_entry.grid(row=0, column=1, sticky='EW') +base_dir_entry = tk.Entry(label_and_entry_frame) +base_dir_entry.grid(row=1, column=1, sticky='EW') +prefix_entry = tk.Entry(label_and_entry_frame) +prefix_entry.grid(row=2, column=1, sticky='EW') +problem_name_entry = tk.Entry(label_and_entry_frame) +problem_name_entry.grid(row=3, column=1, sticky='EW') +preset_name_entry = tk.Entry(label_and_entry_frame) +preset_name_entry.grid(row=5, column=1, sticky='EW') + +ENTRY_OBJECT_DICT = {'method': method_entry, 'base dir': base_dir_entry, 'prefix': prefix_entry, + 'problem name': problem_name_entry, 'preset name': preset_name_entry} + +# Button section +ok_button = tk.Button(bottom_frame, text='Ok', + command=lambda: general_function_handler(method_entry.get(), base_dir_entry.get(), + prefix_entry.get(), problem_name_entry.get())) +ok_button.pack() + +def set_base_dir(): + base_dir_entry.delete(0, tk.END) + base_dir_entry.insert(0, filedialog.askdirectory()) + return + +original_image = Image.open(r'assets\folder_icon.png') +directory_icon = ImageTk.PhotoImage(original_image.resize((16, 16))) +directory_button = tk.Button(label_and_entry_frame, image=directory_icon, command=lambda: set_base_dir()) +directory_button.grid(row=1, column=2, sticky='W') + + +def save_preset(): + param_dict = {} + for key, entry_object in ENTRY_OBJECT_DICT.items(): + param_dict[key] = entry_object.get() + if not os.path.isdir('./preset'): + os.makedirs('./preset') + # TODO: consider the case where the name is empty + with open('./preset/' + param_dict['preset name'] + '.txt', 'w') as file: + for key, value in param_dict.items(): + if not key == 'preset name': + file.write(key + ': ' + value + '\n') + +save_preset_button = tk.Button(label_and_entry_frame, text='save preset', command=save_preset) +save_preset_button.grid(row=5, column=2, sticky='W') + + +def choose_preset(): + filename = filedialog.askopenfilename() + param_dict = {} + for key in ENTRY_OBJECT_DICT.keys(): + param_dict[key] = None + with open(filename, 'r') as file: + param_dict['preset name'] = os.path.basename(file.name) + for line in file: + if line.strip() == '': + continue + key, value = line.split(': ', 1) + param_dict[key] = value.replace('\n', '') + for key, value in param_dict.items(): + if value is not None: + ENTRY_OBJECT_DICT[key].delete(0, tk.END) + ENTRY_OBJECT_DICT[key].insert(0, value) + +choose_preset_button = tk.Button(label_and_entry_frame, text='choose preset', command=choose_preset) +choose_preset_button.grid(row=5, column=3, sticky='W') if __name__ == '__main__': - prefix = input("Prefix: ") - base_dir = input("Base directory: ") - renamer(prefix, base_dir) + saved_base_dir = '' + root.mainloop() \ No newline at end of file