Restructuration de l'application

This commit is contained in:
2026-08-22 13:39:20 +02:00
parent dc25b5fefa
commit b9e9d00e7d
44 changed files with 561 additions and 8675 deletions
+50 -27
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
import importlib.util
import importlib
import io
import json
import os
@@ -31,6 +31,7 @@ from copienator import (
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
from copienator.filesystem import staged_directory, staged_files
from copienator.dispatcher import COMMANDS, main as dispatcher_main
from copienator_gui.app import (
CopienatorApp,
DEFAULT_HTTPS_PROXY,
@@ -45,8 +46,8 @@ from copienator_gui.diagnostics import collect_diagnostics
from copienator_gui.runner import ProcessRunner
from copienator_gui.state import StateStore
from copienator_gui.workflow import build_command, build_workflow, evaluation_argument
from copies_tools import rename_all, rotate_all
from platform_utils import (
from copienator.commands.copies_tools import rename_all, rotate_all
from copienator.platform import (
WindowsLabelError,
replace_with_link_or_copy,
safe_filename,
@@ -57,18 +58,16 @@ from platform_utils import (
REPOSITORY = Path(__file__).resolve().parents[1]
def load_script_module(filename: str, module_name: str):
spec = importlib.util.spec_from_file_location(module_name, REPOSITORY / filename)
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load {filename}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
except Exception:
sys.modules.pop(module_name, None)
raise
return module
def load_script_module(filename: str, _module_name: str):
stem = Path(filename).stem.replace("-", "_")
if stem == "import":
stem = "import_annotations"
return importlib.import_module(f"copienator.commands.{stem}")
def command_arguments(command: list[str]) -> list[str]:
assert command[2:4] == ["-m", "copienator"]
return command[5:]
class WorkspaceTests(unittest.TestCase):
@@ -129,6 +128,24 @@ class WorkspaceTests(unittest.TestCase):
self.assertEqual(workspace.names_file(), root / "names")
class DispatcherTests(unittest.TestCase):
def test_unknown_command_has_argument_error_code(self) -> None:
with redirect_stderr(io.StringIO()):
self.assertEqual(dispatcher_main(["does-not-exist"]), 2)
def test_every_gui_python_command_is_registered(self) -> None:
for step in build_workflow(True):
for variant in step.variants:
if variant.kind == "python":
with self.subTest(step=step.id, command=variant.program):
self.assertIn(variant.program, COMMANDS)
def test_root_contains_only_configuration_python_modules(self) -> None:
root_modules = {path.name for path in REPOSITORY.glob("*.py")}
self.assertIn("default_config.py", root_modules)
self.assertLessEqual(root_modules, {"config.py", "default_config.py"})
class AtomicJsonTests(unittest.TestCase):
def test_atomic_binary_round_trip(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@@ -561,7 +578,9 @@ class StandardCliTests(unittest.TestCase):
variant = next(item for item in step.variants if item.id == variant_id)
command = build_command(REPOSITORY, step, variant, values, evaluation)
with self.subTest(script=module_name):
parsed = self.modules[module_name].build_parser().parse_args(command[3:])
parsed = self.modules[module_name].build_parser().parse_args(
command_arguments(command)
)
parsed_path = getattr(parsed, "evaluation", None) or parsed.target
self.assertEqual(str(parsed_path), evaluation)
@@ -577,7 +596,7 @@ class StandardCliTests(unittest.TestCase):
)
with self.subTest(script=f"copies_tools:{step_id}"):
parsed = self.modules["copies_tools"].build_parser().parse_args(
command[3:]
command_arguments(command)
)
self.assertEqual(parsed.operation, step_id)
self.assertEqual(str(parsed.evaluation), evaluation)
@@ -1521,7 +1540,7 @@ class WorkflowTests(unittest.TestCase):
"default",
{"target": self.evaluation, "annotation_dir": "Bnot"},
)
self.assertEqual(command[3:], [self.evaluation, "Bnot"])
self.assertEqual(command_arguments(command), [self.evaluation, "Bnot"])
def test_copy_listing_prefers_processed_copies(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@@ -1607,27 +1626,31 @@ class WorkflowTests(unittest.TestCase):
"live",
{"target": self.evaluation, "overwrite": True, "limit": "0"},
)
self.assertEqual(command[3:], [self.evaluation, "--overwrite", "--limit", "0"])
self.assertIn("correction.py", command[2])
self.assertEqual(
command_arguments(command),
[self.evaluation, "--overwrite", "--limit", "0"],
)
self.assertEqual(command[4], "correct")
def test_hybrid_correction_arguments(self) -> None:
command = self.command(
"correction", "hybrid", {"target": self.evaluation, "batch_from": "Ex 4"}
)
self.assertEqual(command[3:], [self.evaluation, "--batch-from", "Ex 4"])
self.assertEqual(
command_arguments(command), [self.evaluation, "--batch-from", "Ex 4"]
)
def test_annotation_variants_are_exclusive_commands(self) -> None:
command = self.command(
"annotation", "grouped", {"target": self.evaluation, "overwrite": True}
)
self.assertIn("annotating_by_label.py", command[2])
self.assertNotIn("annotating.py", command[2])
self.assertEqual(command[4], "annotate-grouped")
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})
self.assertEqual(rotate[2:], [str(REPOSITORY / "copies_tools.py"), "rotate", self.evaluation])
self.assertEqual(rename[2:], [str(REPOSITORY / "copies_tools.py"), "rename", self.evaluation])
self.assertEqual(rotate[2:], ["-m", "copienator", "copies", "rotate", self.evaluation])
self.assertEqual(rename[2:], ["-m", "copienator", "copies", "rename", self.evaluation])
class CrossPlatformFileTests(unittest.TestCase):
@@ -1665,8 +1688,8 @@ class CrossPlatformFileTests(unittest.TestCase):
source = root / "source.txt"
destination = root / "destination.txt"
source.write_text("content", encoding="utf-8")
with patch("platform_utils.os.link", side_effect=OSError), patch(
"platform_utils.os.symlink", side_effect=OSError
with patch("copienator.platform.os.link", side_effect=OSError), patch(
"copienator.platform.os.symlink", side_effect=OSError
):
method = replace_with_link_or_copy(source, destination)
self.assertEqual(method, "copy")