From 22c04f6e152c5341bb2dd88ced9b4b77b86fa492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Tue, 6 Oct 2026 11:26:03 +0200 Subject: [PATCH] Clear import directory, possibly --- copienator/commands/annotating.py | 11 ++++++-- copienator/commands/annotating_by_label.py | 15 ++++++++--- copienator/commands/annotating_with_checks.py | 9 +++++++ copienator/commands/import_annotations.py | 27 ++++++++++++++++++- copienator_gui/workflow.py | 6 +++++ tests/test_gui_convenience.py | 13 +++++++++ tests/test_gui_core.py | 13 +++++++++ 7 files changed, 88 insertions(+), 6 deletions(-) diff --git a/copienator/commands/annotating.py b/copienator/commands/annotating.py index 9713ac1..0f311fa 100644 --- a/copienator/commands/annotating.py +++ b/copienator/commands/annotating.py @@ -30,6 +30,7 @@ from copienator import ( workspace_from_args, ) from copienator.annotation_data import load_annotation_data +from copienator.commands.import_annotations import clear_import_directory from copienator.answer_info import build_answer_info from copienator.filesystem import staged_directory from copienator.feedback_boxes import valid_feedback_box @@ -525,6 +526,8 @@ def process_correction(root_dir, data, all_labels, overwrite=False): def build_parser() -> argparse.ArgumentParser: parser = evaluation_parser("Generate simple annotated copies.") + parser.add_argument("--clear-import", action="store_true", + help="Empty IMPORT_DIR after successful annotation generation") parser.add_argument( "--overwrite", action="store_true", @@ -533,7 +536,8 @@ def build_parser() -> argparse.ArgumentParser: return parser -def run(workspace: EvaluationWorkspace, *, overwrite: bool = False) -> ExitCode: +def run(workspace: EvaluationWorkspace, *, overwrite: bool = False, + clear_import: bool = False) -> ExitCode: workspace.require_files("labels", "correction.json") workspace.require_directories("Copies", "Par label") labels = utils.read_all_labels(workspace.root) @@ -551,6 +555,8 @@ def run(workspace: EvaluationWorkspace, *, overwrite: bool = False) -> ExitCode: ) if loaded.warnings and result == ExitCode.SUCCESS: return ExitCode.PARTIAL + if clear_import and result == ExitCode.SUCCESS: + clear_import_directory(workspace) return result @@ -559,7 +565,8 @@ def main(argv: Sequence[str] | None = None) -> int: return execute( parser, argv, - lambda args: run(workspace_from_args(args), overwrite=args.overwrite), + lambda args: run(workspace_from_args(args), overwrite=args.overwrite, + clear_import=args.clear_import), ) diff --git a/copienator/commands/annotating_by_label.py b/copienator/commands/annotating_by_label.py index 35eb457..3e252f0 100644 --- a/copienator/commands/annotating_by_label.py +++ b/copienator/commands/annotating_by_label.py @@ -24,6 +24,7 @@ from copienator import ( ) from copienator.annotation_data import load_annotation_data from copienator.commands import annotating, annotating_with_checks +from copienator.commands.import_annotations import clear_import_directory from copienator.filesystem import staged_directory from copienator.utils import natural_key @@ -347,7 +348,8 @@ def _generate_groups( def run( - workspace: EvaluationWorkspace, *, overwrite: bool = False, refaire: bool = False + workspace: EvaluationWorkspace, *, overwrite: bool = False, refaire: bool = False, + clear_import: bool = False, ) -> ExitCode: workspace.require_files("labels", "correction.json") workspace.require_directories("Copies", "Par label") @@ -408,11 +410,17 @@ def run( return ExitCode.PARTIAL if generated == 0: print("No new grouped annotations were required.") - return ExitCode.PARTIAL if loaded.warnings else ExitCode.SUCCESS + if loaded.warnings: + return ExitCode.PARTIAL + if clear_import: + clear_import_directory(workspace) + return ExitCode.SUCCESS def build_parser() -> argparse.ArgumentParser: parser = evaluation_parser("Generate annotated PDFs grouped by labels.") + parser.add_argument("--clear-import", action="store_true", + help="Empty IMPORT_DIR after successful annotation generation") parser.add_argument( "--overwrite", action="store_true", help="Replace annotation outputs safely" ) @@ -430,7 +438,8 @@ def main(argv: Sequence[str] | None = None) -> int: parser, argv, lambda args: run( - workspace_from_args(args), overwrite=args.overwrite, refaire=args.refaire + workspace_from_args(args), overwrite=args.overwrite, refaire=args.refaire, + clear_import=args.clear_import, ), ) diff --git a/copienator/commands/annotating_with_checks.py b/copienator/commands/annotating_with_checks.py index b13a37c..badaac0 100644 --- a/copienator/commands/annotating_with_checks.py +++ b/copienator/commands/annotating_with_checks.py @@ -27,6 +27,7 @@ from copienator import ( ) from copienator.annotation_data import load_annotation_data from copienator.commands import annotating +from copienator.commands.import_annotations import clear_import_directory from copienator.filesystem import staged_directory from copienator.utils import natural_key @@ -269,6 +270,7 @@ def run( *, overwrite: bool = False, refaire: bool = False, + clear_import: bool = False, ) -> ExitCode: workspace.require_files("labels", "correction.json") workspace.require_directories("Copies", "Par label") @@ -319,6 +321,8 @@ def run( except IncompleteRedo: print("Warning: incomplete redo generation; previous BRnot was preserved.") return ExitCode.PARTIAL + if clear_import: + clear_import_directory(workspace) return ExitCode.SUCCESS statuses: list[str] = [] @@ -338,11 +342,15 @@ def run( statuses.append(future.result()) if loaded.warnings or "partial" in statuses: return ExitCode.PARTIAL + if clear_import: + clear_import_directory(workspace) return ExitCode.SUCCESS def build_parser() -> argparse.ArgumentParser: parser = target_parser("Generate annotated PDFs with checkboxes.") + parser.add_argument("--clear-import", action="store_true", + help="Empty IMPORT_DIR after successful annotation generation") parser.add_argument( "--overwrite", action="store_true", help="Replace existing outputs" ) @@ -364,6 +372,7 @@ def main(argv: Sequence[str] | None = None) -> int: target, overwrite=args.overwrite, refaire=args.refaire, + clear_import=args.clear_import, ) return execute(parser, argv, handler) diff --git a/copienator/commands/import_annotations.py b/copienator/commands/import_annotations.py index 9eb1292..24006e0 100644 --- a/copienator/commands/import_annotations.py +++ b/copienator/commands/import_annotations.py @@ -5,15 +5,40 @@ from collections.abc import Sequence from pathlib import Path from copienator import ( + CliError, EvaluationWorkspace, ExitCode, evaluation_parser, execute, workspace_from_args, ) -from copienator.configuration import IMPORT_DIR +from copienator import configuration ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot") +IMPORT_DIR = getattr(configuration, "IMPORT_DIR", None) + + +def clear_import_directory(workspace: EvaluationWorkspace) -> None: + """Empty the configured import folder while preserving the folder itself.""" + if IMPORT_DIR is None or not str(IMPORT_DIR).strip(): + print("Aucun dossier d’import configuré ; aucun fichier supprimé.") + return + directory = Path(IMPORT_DIR).expanduser().resolve() + if workspace.root.resolve().is_relative_to(directory) or Path.cwd().resolve().is_relative_to(directory): + raise CliError(f"Le dossier d’import ne peut pas contenir l’évaluation ou le projet : {directory}", + ExitCode.INVALID_ARGUMENTS) + if not directory.exists(): + print(f"Dossier d’import absent ; aucun fichier supprimé : {directory}") + return + if not directory.is_dir(): + raise CliError(f"Le dossier d’import n’est pas un dossier : {directory}", + ExitCode.INVALID_ARGUMENTS) + for child in directory.iterdir(): + if child.is_dir() and not child.is_symlink(): + shutil.rmtree(child) + else: + child.unlink() + print(f"Dossier d’import vidé : {directory}", flush=True) def sync_annotated( diff --git a/copienator_gui/workflow.py b/copienator_gui/workflow.py index 4861265..a1b06c5 100644 --- a/copienator_gui/workflow.py +++ b/copienator_gui/workflow.py @@ -527,6 +527,12 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]: ), arguments=( arg_target("le dossier de l’évaluation"), + ArgumentSpec( + "clear_import", "Vider le dossier d’import", "bool", "--clear-import", + default=bool(str(getattr(configuration, "IMPORT_DIR", "") or "").strip()), + help="Supprime tous les fichiers et sous-dossiers du dossier IMPORT_DIR configuré " + "après une génération réussie des copies annotées. Le dossier lui-même est conservé.", + ), ArgumentSpec( "overwrite", "Écraser les sorties", diff --git a/tests/test_gui_convenience.py b/tests/test_gui_convenience.py index f29d08a..3d51611 100644 --- a/tests/test_gui_convenience.py +++ b/tests/test_gui_convenience.py @@ -52,6 +52,19 @@ class GuiConvenienceTests(unittest.TestCase): start.assert_not_called() self.assertEqual(self.app.state_store.step(step.id)["status"], "success") + def test_annotation_import_cleanup_can_be_unchecked(self): + self.app.tree.selection_set("annotation") + self.app.update() + self.assertTrue(self.app.arg_vars["clear_import"].get()) + self.assertIn("--clear-import", self.app._make_command()) + self.app.arg_vars["clear_import"].set(False) + self.assertNotIn("--clear-import", self.app._make_command()) + self.app.tree.selection_set("export") + self.app.update() + self.app.tree.selection_set("annotation") + self.app.update() + self.assertFalse(self.app.arg_vars["clear_import"].get()) + def test_cutleft_visual_check_option_is_below_and_off_by_default(self): step = self.app.step_by_id["cutleft"] self.app.current_step = step diff --git a/tests/test_gui_core.py b/tests/test_gui_core.py index 1f2d785..b698628 100644 --- a/tests/test_gui_core.py +++ b/tests/test_gui_core.py @@ -2705,6 +2705,19 @@ class WorkflowTests(unittest.TestCase): ) self.assertEqual(command[4], "annotate-grouped") + def test_annotation_import_cleanup_defaults_to_configured_folder(self) -> None: + for configured in (None, "", Path("Import")): + with patch.object(workflow_module.configuration, "IMPORT_DIR", configured): + step = next(step for step in build_workflow(False) if step.id == "annotation") + for variant in step.variants: + with self.subTest(configured=configured, variant=variant.id): + command = build_command(REPOSITORY, step, variant, {}, self.evaluation) + self.assertEqual("--clear-import" in command, bool(configured)) + command = build_command( + REPOSITORY, step, variant, {"clear_import": False}, self.evaluation + ) + self.assertNotIn("--clear-import", command) + def test_copy_preparation_commands_are_python(self) -> None: rotate = self.command("rotate", "rotate", {"target": self.evaluation}) rename = self.command("rename", "rename", {"target": self.evaluation})