qq_lib.clear
Utilities for detecting and removing qq runtime files.
This module provides the Clearer class, which identifies and deletes
qq-generated runtime files from a directory. Files associated with active
or successfully completed jobs are preserved unless forced removal is requested.
1# Released under MIT License. 2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab 3 4""" 5Utilities for detecting and removing qq runtime files. 6 7This module provides the `Clearer` class, which identifies and deletes 8qq-generated runtime files from a directory. Files associated with active 9or successfully completed jobs are preserved unless forced removal is requested. 10""" 11 12from .clearer import Clearer 13 14__all__ = [ 15 "Clearer", 16]
class
Clearer:
31class Clearer: 32 """ 33 Handles detection and removal of qq runtime files from a directory. 34 """ 35 36 def __init__(self, directories: list[Path]): 37 """ 38 Initialize a Clearer for one or more directories. 39 40 Args: 41 directories (list[Path]): The directories to clear qq runtime files from. 42 """ 43 self._directories = directories 44 45 def clear(self, force: bool = False) -> None: 46 """ 47 Remove all qq runtime files from all directories that are safe to be removed. 48 49 Directories are cleared in parallel. Only qq files that do **not** 50 correspond to an active or successfully finished job will be removed, 51 unless `force` is set to True. A combined summary is logged at the end. 52 53 Args: 54 force (bool): If True, remove all qq runtime files, even if unsafe. 55 """ 56 results: list[_ClearResult] = [] 57 lock = threading.Lock() 58 59 def clear_single(directory: Path) -> None: 60 result = Clearer._clear_directory(directory, force) 61 with lock: 62 results.append(result) 63 64 with ThreadPoolExecutor( 65 max_workers=CFG.parallelization_options.clear_max_threads 66 ) as executor: 67 for directory in self._directories: 68 executor.submit(clear_single, directory) 69 70 total_detected = sum(r.detected for r in results) 71 total_deleted = sum(r.deleted for r in results) 72 total_excluded = sum(r.excluded for r in results) 73 74 if total_deleted == 0 and total_excluded == 0: 75 logger.info("Nothing to clear.") 76 return 77 78 if total_deleted > 0: 79 logger.info( 80 f"Removed {total_deleted} qq file{'s' if total_deleted > 1 else ''}." 81 ) 82 83 if total_excluded > 0 and total_detected > 0: 84 actually_excluded = ( 85 total_excluded if total_excluded <= total_detected else total_detected 86 ) 87 88 logger.info( 89 f"{actually_excluded} qq file{'s' if actually_excluded > 1 else ''} could not be safely cleared. " 90 f"Rerun as '{CFG.binary_name} clear --force' to clear {'them' if actually_excluded > 1 else 'it'} forcibly." 91 ) 92 93 @staticmethod 94 def _clear_directory(directory: Path, force: bool) -> _ClearResult: 95 """ 96 Clear qq runtime files from a single directory and return the result. 97 98 Args: 99 directory (Path): The directory to clear. 100 force (bool): If True, remove all qq runtime files, even if unsafe. 101 102 Returns: 103 _ClearResult: The number of deleted and excluded files. 104 """ 105 files = Clearer._collect_runtime_files(directory) 106 logger.debug(f"All qq runtime files in '{directory}': {files}.") 107 if not files: 108 return _ClearResult(detected=0) 109 110 excluded = Clearer._collect_excluded_files(directory) if not force else set() 111 logger.debug(f"Files excluded from clearing in '{directory}': {excluded}.") 112 113 to_delete = files - excluded 114 logger.debug(f"Files to delete in '{directory}': {to_delete}.") 115 116 if to_delete: 117 Clearer._delete_files(to_delete) 118 119 return _ClearResult( 120 detected=len(files), deleted=len(to_delete), excluded=len(excluded) 121 ) 122 123 @staticmethod 124 def _collect_runtime_files(directory: Path) -> set[Path]: 125 """ 126 Collect all qq runtime files in the directory. 127 128 Returns: 129 set[Path]: Paths to all files matching qq-specific suffixes. 130 """ 131 return set(get_runtime_files(directory)) 132 133 @staticmethod 134 def _collect_excluded_files(directory: Path) -> set[Path]: 135 """ 136 Collect qq runtime files that should **not** be deleted. 137 138 Runtime files corresponding to active or successfully finished jobs are included. 139 140 Returns: 141 set[Path]: Paths to qq runtime files that should not be deleted. 142 """ 143 excluded = [] 144 145 # iterate through info files 146 for file in get_info_files(directory): 147 try: 148 informer = Informer.from_file(file) 149 state = informer.get_real_state() 150 logger.debug(f"Job state: {str(state)}.") 151 except QQError: 152 # ignore the file if it cannot be read 153 continue 154 155 if state not in [ 156 RealState.KILLED, 157 RealState.FAILED, 158 RealState.IN_AN_INCONSISTENT_STATE, 159 ]: 160 excluded.append(file) # qq info file 161 excluded.append(directory / informer.info.stdout_file) # script stdout 162 excluded.append(directory / informer.info.stderr_file) # script stderr 163 excluded.append( 164 (directory / informer.info.job_name).with_suffix( 165 CFG.suffixes.qq_out 166 ) 167 ) # qq out file 168 169 return set(excluded) 170 171 @staticmethod 172 def _delete_files(files: Iterable[Path]) -> None: 173 """ 174 Delete all specified files. 175 176 Args: 177 files (Iterable[Path]): The list of files to delete. 178 """ 179 for file in files: 180 logger.debug(f"Removing file '{file}'.") 181 file.unlink()
Handles detection and removal of qq runtime files from a directory.
Clearer(directories: list[pathlib._local.Path])
36 def __init__(self, directories: list[Path]): 37 """ 38 Initialize a Clearer for one or more directories. 39 40 Args: 41 directories (list[Path]): The directories to clear qq runtime files from. 42 """ 43 self._directories = directories
Initialize a Clearer for one or more directories.
Arguments:
- directories (list[Path]): The directories to clear qq runtime files from.
def
clear(self, force: bool = False) -> None:
45 def clear(self, force: bool = False) -> None: 46 """ 47 Remove all qq runtime files from all directories that are safe to be removed. 48 49 Directories are cleared in parallel. Only qq files that do **not** 50 correspond to an active or successfully finished job will be removed, 51 unless `force` is set to True. A combined summary is logged at the end. 52 53 Args: 54 force (bool): If True, remove all qq runtime files, even if unsafe. 55 """ 56 results: list[_ClearResult] = [] 57 lock = threading.Lock() 58 59 def clear_single(directory: Path) -> None: 60 result = Clearer._clear_directory(directory, force) 61 with lock: 62 results.append(result) 63 64 with ThreadPoolExecutor( 65 max_workers=CFG.parallelization_options.clear_max_threads 66 ) as executor: 67 for directory in self._directories: 68 executor.submit(clear_single, directory) 69 70 total_detected = sum(r.detected for r in results) 71 total_deleted = sum(r.deleted for r in results) 72 total_excluded = sum(r.excluded for r in results) 73 74 if total_deleted == 0 and total_excluded == 0: 75 logger.info("Nothing to clear.") 76 return 77 78 if total_deleted > 0: 79 logger.info( 80 f"Removed {total_deleted} qq file{'s' if total_deleted > 1 else ''}." 81 ) 82 83 if total_excluded > 0 and total_detected > 0: 84 actually_excluded = ( 85 total_excluded if total_excluded <= total_detected else total_detected 86 ) 87 88 logger.info( 89 f"{actually_excluded} qq file{'s' if actually_excluded > 1 else ''} could not be safely cleared. " 90 f"Rerun as '{CFG.binary_name} clear --force' to clear {'them' if actually_excluded > 1 else 'it'} forcibly." 91 )
Remove all qq runtime files from all directories that are safe to be removed.
Directories are cleared in parallel. Only qq files that do not
correspond to an active or successfully finished job will be removed,
unless force is set to True. A combined summary is logged at the end.
Arguments:
- force (bool): If True, remove all qq runtime files, even if unsafe.