Skip to content

Commit

Permalink
removed any_is_subfolder_of()
Browse files Browse the repository at this point in the history
new any_is_subfolder_of() that doesn't exit the script but returns a value

improved message in validate_arguments() when it finds invalid folders
  • Loading branch information
niradar committed Jun 17, 2024
1 parent 9337d4c commit c49d86d
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 29 deletions.
17 changes: 16 additions & 1 deletion duplicate_files_in_folders/file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import logging
from collections import deque
from typing import Dict, List
from typing import Dict, List, Tuple
import tqdm

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -310,6 +310,21 @@ def delete_empty_folders_in_tree(self, base_path: str, show_progress: bool = Fal

return deleted_folders

@staticmethod
def any_is_subfolder_of(folders: List[str]) -> Tuple[bool, List[Tuple[str, str]]]:
"""
Check if any folder is a subfolder of another folder.
:param folders: list of folder paths
:return: Tuple containing a boolean and a list of subfolder relationships
"""
subfolder_pairs = []
for i in range(len(folders)):
for j in range(len(folders)):
if i != j and folders[i].startswith(folders[j]):
subfolder_pairs.append((folders[i], folders[j]))
return bool(subfolder_pairs), subfolder_pairs

def reset_all(self):
"""Reset the protected_dirs and allowed_dirs to empty sets."""
self.protected_dirs = set()
Expand Down
23 changes: 8 additions & 15 deletions duplicate_files_in_folders/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,6 @@ def detect_pytest():
return 'PYTEST_CURRENT_TEST' in os.environ


def any_is_subfolder_of(folders: List[str]) -> bool:
"""
Check if any folder is a subfolder of another folder.
:param folders: list of folder paths
:return: False if no folder is a subfolder of another folder, otherwise exit the script
"""
for i in range(len(folders)):
for j in range(len(folders)):
if i != j and folders[i].startswith(folders[j]):
logger.error(f"{folders[i]} is a subfolder of {folders[j]}")
sys.exit(1)
return False


def parse_size(size_str: str | int) -> int:
"""
Parse a size string with units (B, KB, MB) to an integer size in bytes.
Expand Down Expand Up @@ -116,7 +102,14 @@ def validate_arguments(args, parser, check_folders=True):
parser.error(f"{name} folder does not exist.")
if not os.listdir(folder):
parser.error(f"{name} folder is empty.")
any_is_subfolder_of([args.scan_dir, args.reference_dir, args.move_to])

is_subfolder, relationships = FileManager.any_is_subfolder_of([args.scan_dir, args.reference_dir, args.move_to])
if is_subfolder:
for subfolder, parent in relationships:
if subfolder != parent:
parser.error(f"{subfolder} is a subfolder of {parent}")
else:
parser.error(f"Several arguments are the same folder: {subfolder}")

# for testing, barely used
if args.extra_logging:
Expand Down
65 changes: 55 additions & 10 deletions tests/test_file_manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from tests.helpers_testing import *
from pathlib import Path
from duplicate_files_in_folders.file_manager import FileManager


def test_move_file(setup_teardown):
scan_dir, reference_dir, move_to_dir, common_args = setup_teardown
setup_test_files(range(1, 6), [2])
fm = file_manager.FileManager(True).reset_all()
fm = FileManager(True).reset_all()
fm.add_protected_dir(reference_dir)
file_to_move = os.path.join(scan_dir, "1.jpg")
dst_file = os.path.join(reference_dir, "1.jpg")
Expand Down Expand Up @@ -41,7 +42,7 @@ def test_move_file(setup_teardown):
def test_copy_file(setup_teardown):
scan_dir, reference_dir, move_to_dir, common_args = setup_teardown
setup_test_files(range(1, 6), [2, 3])
fm = file_manager.FileManager(True).reset_all()
fm = FileManager(True).reset_all()
fm.add_protected_dir(reference_dir)
file_to_copy = os.path.join(scan_dir, "1.jpg")
dst_file = os.path.join(reference_dir, "1.jpg")
Expand Down Expand Up @@ -85,7 +86,7 @@ def test_copy_file(setup_teardown):
def test_delete_file(setup_teardown):
scan_dir, reference_dir, move_to_dir, common_args = setup_teardown
setup_test_files(range(1, 6), [2, 3])
fm = file_manager.FileManager(True).reset_all()
fm = FileManager(True).reset_all()
fm.add_protected_dir(reference_dir)
file_to_delete = os.path.join(scan_dir, "1.jpg")

Expand Down Expand Up @@ -117,7 +118,7 @@ def test_delete_file(setup_teardown):

def test_make_dirs(setup_teardown):
scan_dir, reference_dir, move_to_dir, common_args = setup_teardown
fm = file_manager.FileManager(True).reset_all()
fm = FileManager(True).reset_all()
fm.add_protected_dir(reference_dir)
dir_to_make = os.path.join(scan_dir, "new_dir")

Expand Down Expand Up @@ -150,7 +151,7 @@ def test_make_dirs(setup_teardown):

def test_rmdir(setup_teardown):
scan_dir, reference_dir, move_to_dir, common_args = setup_teardown
fm = file_manager.FileManager(True).reset_all()
fm = FileManager(True).reset_all()
fm.add_protected_dir(reference_dir)
dir_to_remove = os.path.join(scan_dir, "new_dir")
os.makedirs(dir_to_remove)
Expand Down Expand Up @@ -188,16 +189,16 @@ def test_rmdir(setup_teardown):

# The FileManager class should be a singleton, so we should not be able to create multiple instances of it.
def test_singleton():
fm1 = file_manager.FileManager(True)
fm2 = file_manager.FileManager(True)
fm1 = FileManager(True)
fm2 = FileManager(True)
assert fm1 is fm2
assert fm1 == fm2
assert fm1 is not None
assert fm2 is not None


def test_add_protected_dir():
fm = file_manager.FileManager(True).reset_all()
fm = FileManager(True).reset_all()
fm.add_protected_dir("C:\\")
fm.add_protected_dir("D:\\")
assert len(fm.protected_dirs) == 2
Expand All @@ -216,7 +217,7 @@ def get_folder_files_as_set(folder):
def test_list_tree_os_scandir_bfs_simple(setup_teardown):
scan_dir, reference_dir, move_to_dir, common_args = setup_teardown
setup_test_files(range(1, 6), [2, 3])
fm = file_manager.FileManager.get_instance()
fm = FileManager.get_instance()

scan_files = get_folder_files_as_set(scan_dir)
scan_tree = fm.list_tree_os_scandir_bfs(scan_dir) # result is in the form of full path
Expand Down Expand Up @@ -247,12 +248,56 @@ def test_list_tree_os_scandir_bfs_tree_with_many_subfolders(setup_teardown):
copy_files(range(2, 5), os.path.join(scan_dir, "sub2", "sub1"))
copy_files(range(1, 5), os.path.join(scan_dir, "sub2", "sub2"))

fm = file_manager.FileManager.get_instance()
fm = FileManager.get_instance()
scan_files = get_folder_files_as_set(scan_dir)
scan_tree = fm.list_tree_os_scandir_bfs(scan_dir) # result is in the form of full path
assert set(scan_tree) == scan_files


def test_file_manager_any_is_subfolder_of():
# Test case 1: one folder is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["C:\\Users\\user\\Desktop\\folder", "C:\\Users\\user\\Desktop\\folder\\subfolder"])
assert is_subfolder is True

# Test case 2: no folder is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["C:\\Users\\user\\Desktop\\folder1", "C:\\Users\\user\\Desktop\\folder2"])
assert is_subfolder is False

# Test case 3: one folder is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["/path/to/folder", "/path/to/folder/subfolder"])
assert is_subfolder is True

# Test case 4: no folder is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["/path/to/folder1", "/path/to/folder2"])
assert is_subfolder is False

# Test case 5: 3 folders, one is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["/path/to/folder1", "/path/to/folder2", "/path/to/folder2/subfolder"])
assert is_subfolder is True

# Test case 6: 3 folders, no folder is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["/path/to/folder1", "/path/to/folder2", "/path/to/folder3"])
assert is_subfolder is False

# Test case 7: 3 folders, one is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["C:\\Users\\user\\Desktop\\folder1", "C:\\Users\\user\\Desktop\\folder2",
"C:\\Users\\user\\Desktop\\folder2\\subfolder"])
assert is_subfolder is True

# Test case 8: 3 folders, no folder is subfolder of another
is_subfolder, relationships = FileManager.any_is_subfolder_of(
["C:\\Users\\user\\Desktop\\folder1", "C:\\Users\\user\\Desktop\\folder2",
"C:\\Users\\user\\Desktop\\folder3"])
assert is_subfolder is False


def test_python_source_files():
"""
Test all python files in the project under duplicate_files_in_folders folder. Make sure that all python files
Expand Down
6 changes: 3 additions & 3 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from duplicate_files_in_folders.duplicates_finder import clean_scan_dir_duplications, find_duplicates_files_v3, \
process_duplicates
from duplicate_files_in_folders.file_manager import FileManager
from duplicate_files_in_folders.utils import parse_arguments, any_is_subfolder_of, parse_size, \
check_and_update_filename
from duplicate_files_in_folders.utils import parse_arguments, parse_size, check_and_update_filename
from duplicate_files_in_folders.initializer import setup_file_manager

from tests.helpers_testing import *
Expand Down Expand Up @@ -118,7 +117,8 @@ def test_parse_arguments():
assert excinfo.type == SystemExit

with pytest.raises(SystemExit) as excinfo: # invalid value for reference_dir - subfolder of scan_dir
parse_arguments(['--scan', scan_dir, '--reference_dir', os.path.join(scan_dir, 'subfolder'), '--move_to', move_to_folder], False)
parse_arguments(['--scan', scan_dir, '--reference_dir', os.path.join(scan_dir, 'subfolder'),
'--move_to', move_to_folder], False)
assert excinfo.type == SystemExit

with pytest.raises(SystemExit) as excinfo: # invalid value for reference_dir - subfolder of move_to
Expand Down

0 comments on commit c49d86d

Please sign in to comment.