Support evaluation-local configuration and names overrides

This commit is contained in:
sebastien committed 2026-11-03 18:44:56 +01:00
1 parent 0bb6f7d97c
commit d4404e4c27
36 files changed
+409 -184

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
"""Evaluation files override shared settings without leaking across evaluations."""
import contextlib
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from copienator import EvaluationWorkspace
REPOSITORY = Path(__file__).resolve().parents[1]
class LocalConfigurationTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.evaluation = self.root / "Exam"
self.evaluation.mkdir()
(self.evaluation / "labels").write_text("Ex 1\n")
self.global_config = self.root / "config.py"
self.global_config.write_text("MODEL_FLASH_ID = 'global-model'\nRETURN_PDF_ENABLED = False\n")
def run_python(self, source, *, explicit=True):
env = dict(os.environ, PYTHONPATH=str(REPOSITORY))
env.pop("COPIENATOR_CONFIG", None)
if explicit:
env["COPIENATOR_CONFIG"] = str(self.global_config)
result = subprocess.run([sys.executable, "-c", source], cwd=self.root, env=env,
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
return result.stdout
def test_partial_local_config_inherits_global_and_defaults(self):
(self.evaluation / "config.py").write_text("MODEL_FLASH_ID = 'local-model'\n")
for explicit in (False, True):
with self.subTest(explicit=explicit):
self.run_python("""
from copienator import configuration as c
c.use_evaluation('Exam')
assert c.MODEL_FLASH_ID == 'local-model'
assert c.RETURN_PDF_ENABLED is False
assert c.RETURN_JPEG_ENABLED is True
assert c.CONFIG_PATH.name == 'config.py'
assert c.LOCAL_CONFIG_PATH.parent.name == 'Exam'
c.use_evaluation(None)
assert c.MODEL_FLASH_ID == 'global-model'
""", explicit=explicit)
def test_switching_evaluations_removes_previous_overrides(self):
(self.evaluation / "config.py").write_text("RETURN_JPEG_ENABLED = False\n")
(self.root / "Other").mkdir()
self.run_python("""
from copienator import configuration as c
c.use_evaluation('Exam')
assert c.RETURN_JPEG_ENABLED is False
c.use_evaluation('Other')
assert c.RETURN_JPEG_ENABLED is True
assert c.RETURN_PDF_ENABLED is False
assert c.LOCAL_CONFIG_PATH is None
""")
def test_command_loads_config_for_directory_and_nested_target(self):
(self.evaluation / "config.py").write_text("MODEL_FLASH_ID = 'local-model'\n")
self.run_python("""
from copienator import configuration as c
from copienator.cli import execute, evaluation_parser, target_parser
from copienator.commands import correction
def check(args):
assert correction.settings.MODEL_FLASH_ID == 'local-model'
return 0
assert execute(evaluation_parser('test'), ['Exam'], check) == 0
c.use_evaluation(None)
assert execute(target_parser('test'), ['Exam/labels'], check) == 0
""")
def test_annotation_limits_and_gui_defaults_follow_active_config(self):
(self.evaluation / "config.py").write_text(
"ANNOTATION_MAX_HEIGHT_PX = 12345\nALWAYS_CROP = True\n")
self.run_python("""
from copienator import configuration as c
from copienator.commands import annotating_by_label, annotating_with_checks
from copienator_gui.workflow import build_workflow
c.use_evaluation('Exam')
assert annotating_by_label.annotation_limits()[0] == 12345
assert annotating_with_checks.annotation_limits()[0] == 12345
steps = {step.id: step for step in build_workflow(False)}
assert steps['crop_blank_margins'].auto_start_first_visit
""")
@unittest.skipUnless(os.environ.get("DISPLAY"), "GUI display unavailable")
def test_gui_switches_evaluation_settings(self):
(self.evaluation / "config.py").write_text("ALWAYS_CROP = True\n")
(self.root / "Other").mkdir()
self.run_python("""
from pathlib import Path
from copienator import configuration as c
from copienator_gui.app import CopienatorApp
app = CopienatorApp(Path.cwd(), False, Path('Exam').resolve())
try:
assert app.step_by_id['crop_blank_margins'].auto_start_first_visit
app.evaluation_var.set(str(Path('Other').resolve()))
app._load_evaluation()
assert not app.step_by_id['crop_blank_margins'].auto_start_first_visit
assert c.LOCAL_CONFIG_PATH is None
finally:
app.destroy()
""")
def test_explicit_command_option_overrides_local_config(self):
(self.evaluation / "config.py").write_text("RETURN_ANSWERS_CONTEXT = True\n")
self.run_python("""
from unittest.mock import patch
from copienator.commands import reading_annotations as command
from copienator import configuration as c
def check(workspace, **options):
assert c.RETURN_ANSWERS_CONTEXT is True
assert options['return_answers_context'] is False
return 0
with patch.object(command, 'run', check):
assert command.main(['Exam', '--no-return-answers-context']) == 0
""")
def test_invalid_local_config_reports_error_before_handler(self):
(self.evaluation / "config.py").write_text("ANNOTATION_MAX_HEIGHT_PX = 0\n")
self.run_python("""
from unittest.mock import Mock
from copienator.cli import execute, evaluation_parser
handler = Mock()
assert execute(evaluation_parser('test'), ['Exam'], handler) == 1
handler.assert_not_called()
""")
def test_local_names_replace_global_including_an_empty_roster(self):
(self.root / "names").write_text("Global\n")
workspace = EvaluationWorkspace(self.evaluation, self.root)
self.assertEqual(workspace.names_file(), self.root / "names")
local = self.evaluation / "names"
local.write_text("Local\n")
self.assertEqual(workspace.names_file(), local)
local.write_text("")
self.assertEqual(workspace.names_file().read_text(), "")
with contextlib.chdir(self.root):
self.assertEqual(EvaluationWorkspace(self.evaluation).names_file(), local)
local.unlink()
self.assertEqual(EvaluationWorkspace(self.evaluation).names_file(), self.root / "names")