from pathlib import PurePath from typing import Set class MarkDuplicateError(Exception): """ Tried to mark a file that was already marked. """ def __init__(self, path: PurePath): super().__init__(f"A previous file already used path {path}") self.path = path class MarkConflictError(Exception): """ Marking the path would have caused a conflict. A conflict can have two reasons: Either the new file has the same path as the parent directory of a known file, or a parent directory of the new file has the same path as a known file. In either case, adding the new file would require a file and a directory to share the same path, which is usually not possible. """ def __init__(self, path: PurePath, collides_with: PurePath): super().__init__(f"File at {path} collides with previous file at {collides_with}") self.path = path self.collides_with = collides_with # TODO Use PurePath.is_relative_to when updating to 3.9 def is_relative_to(a: PurePath, b: PurePath) -> bool: try: a.relative_to(b) return True except ValueError: return False class Report: """ A report of a synchronization. Includes all files found by the crawler, as well as the set of changes made to local files. """ def __init__(self) -> None: self.reserved_files: Set[PurePath] = set() self.known_files: Set[PurePath] = set() self.new_files: Set[PurePath] = set() self.changed_files: Set[PurePath] = set() self.deleted_files: Set[PurePath] = set() def mark_reserved(self, path: PurePath) -> None: self.reserved_files.add(path) def mark(self, path: PurePath) -> None: """ Mark a previously unknown file as known. May throw a MarkDuplicateError or a MarkConflictError. For more detail, see the respective exception's docstring. """ for other in self.marked: if path == other: raise MarkDuplicateError(path) if is_relative_to(path, other) or is_relative_to(other, path): raise MarkConflictError(path, other) self.known_files.add(path) @property def marked(self) -> Set[PurePath]: return self.known_files | self.reserved_files def is_marked(self, path: PurePath) -> bool: return path in self.marked def add_file(self, path: PurePath) -> None: """ Unlike mark(), this function accepts any paths. """ self.new_files.add(path) def change_file(self, path: PurePath) -> None: """ Unlike mark(), this function accepts any paths. """ self.changed_files.add(path) def delete_file(self, path: PurePath) -> None: """ Unlike mark(), this function accepts any paths. """ self.deleted_files.add(path)