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

Handles detection and removal of qq runtime files from a directory.

Clearer(directories: list[pathlib._local.Path])
35    def __init__(self, directories: list[Path]):
36        """
37        Initialize a Clearer for one or more directories.
38
39        Args:
40            directories (list[Path]): The directories to clear qq runtime files from.
41        """
42        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:
44    def clear(self, force: bool = False) -> None:
45        """
46        Remove all qq runtime files from all directories that are safe to be removed.
47
48        Directories are cleared in parallel. Only qq files that do **not**
49        correspond to an active or successfully finished job will be removed,
50        unless `force` is set to True. A combined summary is logged at the end.
51
52        Args:
53            force (bool): If True, remove all qq runtime files, even if unsafe.
54        """
55        results: list[_ClearResult] = []
56        lock = threading.Lock()
57
58        def clear_single(directory: Path) -> None:
59            result = Clearer._clear_directory(directory, force)
60            with lock:
61                results.append(result)
62
63        with ThreadPoolExecutor(
64            max_workers=CFG.parallelization_options.clear_max_threads
65        ) as executor:
66            for directory in self._directories:
67                executor.submit(clear_single, directory)
68
69        total_deleted = sum(r.deleted for r in results)
70        total_excluded = sum(r.excluded for r in results)
71
72        if total_deleted == 0 and total_excluded == 0:
73            logger.info("Nothing to clear.")
74            return
75
76        if total_deleted > 0:
77            logger.info(
78                f"Removed {total_deleted} qq file{'s' if total_deleted > 1 else ''}."
79            )
80
81        if total_excluded > 0:
82            logger.info(
83                f"{total_excluded} qq file{'s' if total_excluded > 1 else ''} could not be safely cleared. "
84                f"Rerun as '{CFG.binary_name} clear --force' to clear them forcibly."
85            )

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.