Remove `Arguments supplémentaires' boxes

This commit is contained in:
2026-09-09 18:11:00 +02:00
parent 0a167072ed
commit f7b3689f23
8 changed files with 277 additions and 20 deletions
+2
View File
@@ -31,6 +31,8 @@ if CONFIG_PATH is None:
else: else:
_configuration = _load_user_config(CONFIG_PATH) _configuration = _load_user_config(CONFIG_PATH)
# Keep new optional settings compatible with older personal configuration files.
ALWAYS_CROP = False
for _name in dir(_configuration): for _name in dir(_configuration):
if not _name.startswith("_"): if not _name.startswith("_"):
globals()[_name] = getattr(_configuration, _name) globals()[_name] = getattr(_configuration, _name)
+39 -5
View File
@@ -178,6 +178,7 @@ class CopienatorApp(tk.Tk):
self.api_key_var = tk.StringVar(value=os.environ.get("GEMINI_API_KEY", "")) self.api_key_var = tk.StringVar(value=os.environ.get("GEMINI_API_KEY", ""))
self.proxy_var = tk.StringVar(value=DEFAULT_HTTPS_PROXY) self.proxy_var = tk.StringVar(value=DEFAULT_HTTPS_PROXY)
self.use_proxy_var = tk.BooleanVar(value=False) self.use_proxy_var = tk.BooleanVar(value=False)
self.verbose_var = tk.BooleanVar(value=False)
self.copy_var = tk.StringVar() self.copy_var = tk.StringVar()
self.variant_var = tk.StringVar() self.variant_var = tk.StringVar()
self.extra_var = tk.StringVar() self.extra_var = tk.StringVar()
@@ -186,6 +187,7 @@ class CopienatorApp(tk.Tk):
self._build_ui(show_personal_steps) self._build_ui(show_personal_steps)
self.extra_var.trace_add("write", lambda *_args: self._update_command_preview()) self.extra_var.trace_add("write", lambda *_args: self._update_command_preview())
self.verbose_var.trace_add("write", lambda *_args: self._update_command_preview())
self.after(60, self._poll_runner) self.after(60, self._poll_runner)
if initial_evaluation and initial_evaluation.is_dir(): if initial_evaluation and initial_evaluation.is_dir():
@@ -237,6 +239,11 @@ class CopienatorApp(tk.Tk):
).pack(side="left", padx=(0, 6)) ).pack(side="left", padx=(0, 6))
self.proxy_entry = ttk.Entry(environment, textvariable=self.proxy_var, width=28, state="disabled") self.proxy_entry = ttk.Entry(environment, textvariable=self.proxy_var, width=28, state="disabled")
self.proxy_entry.pack(side="left") self.proxy_entry.pack(side="left")
ttk.Checkbutton(
environment,
text="Afficher les détails en cas derreur (--verbose)",
variable=self.verbose_var,
).pack(side="left", padx=(10, 0))
profile = "standard + personnel" if show_personal_steps else "standard" profile = "standard + personnel" if show_personal_steps else "standard"
ttk.Label(environment, text=f"Profil : {profile}").pack(side="left", padx=(12, 0)) ttk.Label(environment, text=f"Profil : {profile}").pack(side="left", padx=(12, 0))
@@ -672,12 +679,24 @@ class CopienatorApp(tk.Tk):
row += 1 row += 1
row += 1 row += 1
if not step.is_manual and step.section != REFAIRE_SECTION: show_extra = bool(step.extra_arguments_help) and (
not step.extra_arguments_variants
or variant.id in step.extra_arguments_variants
)
if show_extra and not step.is_manual and step.section != REFAIRE_SECTION:
ttk.Label(self.form, text="Arguments supplémentaires").grid( ttk.Label(self.form, text="Arguments supplémentaires").grid(
row=row, column=0, sticky="w", pady=(10, 4), padx=(0, 8) row=row, column=0, sticky="w", pady=(10, 4), padx=(0, 8)
) )
ttk.Entry(self.form, textvariable=self.extra_var).grid(row=row, column=1, sticky="ew", pady=(10, 4)) ttk.Entry(self.form, textvariable=self.extra_var).grid(row=row, column=1, sticky="ew", pady=(10, 4))
row += 1 row += 1
ttk.Label(
self.form,
text=step.extra_arguments_help,
foreground="#666666",
wraplength=540,
justify="left",
).grid(row=row, column=1, sticky="w")
row += 1
if self.show_personal_steps and step.id == "statement": if self.show_personal_steps and step.id == "statement":
actions = ttk.LabelFrame(self.form, text="Après génération — facultatif", padding=7) actions = ttk.LabelFrame(self.form, text="Après génération — facultatif", padding=7)
@@ -953,7 +972,11 @@ class CopienatorApp(tk.Tk):
self.current_step.id, self.current_step.id,
variant=self.variant_var.get() or self.current_step.variants[0].id, variant=self.variant_var.get() or self.current_step.variants[0].id,
values=merged_values, values=merged_values,
extra=self.extra_var.get(), extra=(
self.extra_var.get()
if self.current_step.extra_arguments_help
else ""
),
) )
def _evaluation_arg(self) -> str: def _evaluation_arg(self) -> str:
@@ -1020,21 +1043,32 @@ class CopienatorApp(tk.Tk):
choice = self.state_store.step("refaire_selection").get("values", {}).get("layout", "auto") choice = self.state_store.step("refaire_selection").get("values", {}).get("layout", "auto")
layout = resolve_layout(selection, EvaluationWorkspace(self.evaluation).read_labels(), choice) layout = resolve_layout(selection, EvaluationWorkspace(self.evaluation).read_labels(), choice)
variant = replace(variant, program="annotate-grouped" if layout == "grouped" else "annotate-checks") variant = replace(variant, program="annotate-grouped" if layout == "grouped" else "annotate-checks")
return [build_command(self.repository, self.current_step, variant, return [build_command(
{**values, "target": target}, self._evaluation_arg()) for target in targets] self.repository,
self.current_step,
variant,
{**values, "target": target},
self._evaluation_arg(),
verbose=bool(self.verbose_var.get()),
) for target in targets]
def _make_command(self) -> list[str]: def _make_command(self) -> list[str]:
if not self.current_step: if not self.current_step:
return [] return []
if self.current_step.section == REFAIRE_SECTION: if self.current_step.section == REFAIRE_SECTION:
return self._refaire_commands()[0] return self._refaire_commands()[0]
allow_extra = bool(self.current_step.extra_arguments_help) and (
not self.current_step.extra_arguments_variants
or self._current_variant().id in self.current_step.extra_arguments_variants
)
return build_command( return build_command(
self.repository, self.repository,
self.current_step, self.current_step,
self._current_variant(), self._current_variant(),
self._values(), self._values(),
self._evaluation_arg(), self._evaluation_arg(),
self.extra_var.get(), self.extra_var.get() if allow_extra else "",
verbose=bool(self.verbose_var.get()),
) )
def _update_command_preview(self) -> None: def _update_command_preview(self) -> None:
+83 -12
View File
@@ -7,6 +7,8 @@ from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from copienator.configuration import ALWAYS_CROP
EVALUATION = "${evaluation}" EVALUATION = "${evaluation}"
@@ -33,6 +35,7 @@ class CommandVariant:
fixed_args_before_positionals: bool = False fixed_args_before_positionals: bool = False
dangerous: bool = False dangerous: bool = False
danger_warning: str | None = None danger_warning: str | None = None
supports_verbose: bool = False
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -50,6 +53,8 @@ class StepDefinition:
auto_start_first_visit: bool = False auto_start_first_visit: bool = False
skip_for_live_correction: bool = False skip_for_live_correction: bool = False
skip_without_manual_conflicts: bool = False skip_without_manual_conflicts: bool = False
extra_arguments_help: str = ""
extra_arguments_variants: tuple[str, ...] = ()
@property @property
def is_manual(self) -> bool: def is_manual(self) -> bool:
@@ -68,9 +73,16 @@ def arg_target(help_text: str = "Dossier d’évaluation ou fichier à traiter")
def build_workflow(show_personal_steps: bool) -> list[StepDefinition]: def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
python = lambda ident, label, script, **kwargs: CommandVariant( def python(ident, label, script, **kwargs):
ident, label, script, "python", **kwargs supports_verbose = kwargs.pop("supports_verbose", True)
) return CommandVariant(
ident,
label,
script,
"python",
supports_verbose=supports_verbose,
**kwargs,
)
manual = lambda ident, label="Étape manuelle": CommandVariant( manual = lambda ident, label="Étape manuelle": CommandVariant(
ident, label, None, "manual" ident, label, None, "manual"
) )
@@ -111,7 +123,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"statement_groups", "Prétraitement de l’énoncé", "Regrouper les questions avec Gemini", "statement_groups", "Prétraitement de l’énoncé", "Regrouper les questions avec Gemini",
"Facultatif après la génération : remplace les groupes par exercice par des groupes " "Facultatif après la génération : remplace les groupes par exercice par des groupes "
"proposés par Gemini, en conservant les labels, les énoncés, les solutions et les barèmes.", "proposés par Gemini, en conservant les labels, les énoncés, les solutions et les barèmes.",
(CommandVariant("default", "Groupes Gemini", "statement", fixed_args=("--groups-only",)),), (python("default", "Groupes Gemini", "statement", fixed_args=("--groups-only",)),),
arguments=(arg_target("Dossier de l’évaluation"),), arguments=(arg_target("Dossier de l’évaluation"),),
optional=True, personal=True, requires=("labels", "Text2", "Sol2"), optional=True, personal=True, requires=("labels", "Text2", "Sol2"),
), ),
@@ -119,7 +131,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"statement_persp", "Prétraitement de l’énoncé", "Remplacer les barèmes par Gemini", "statement_persp", "Prétraitement de l’énoncé", "Remplacer les barèmes par Gemini",
"Facultatif : remplace Persp par des barèmes Gemini sur 4 points, pour les groupes actuels. " "Facultatif : remplace Persp par des barèmes Gemini sur 4 points, pour les groupes actuels. "
"Les énoncés et les solutions personnels sont conservés.", "Les énoncés et les solutions personnels sont conservés.",
(CommandVariant("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),), (python("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
arguments=(arg_target("Dossier de l’évaluation"),), arguments=(arg_target("Dossier de l’évaluation"),),
optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"), optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"),
), ),
@@ -145,6 +157,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"python", "python",
("rotate",), ("rotate",),
fixed_args_before_positionals=True, fixed_args_before_positionals=True,
supports_verbose=True,
), ),
), ),
arguments=(arg_target("Dossier de l’évaluation"),), arguments=(arg_target("Dossier de l’évaluation"),),
@@ -163,6 +176,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"python", "python",
("rename",), ("rename",),
fixed_args_before_positionals=True, fixed_args_before_positionals=True,
supports_verbose=True,
), ),
), ),
arguments=(arg_target("Dossier de l’évaluation"),), arguments=(arg_target("Dossier de l’évaluation"),),
@@ -188,6 +202,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
ArgumentSpec("workers", "Copies traitées en parallèle", "int", "--workers", default=5)), ArgumentSpec("workers", "Copies traitées en parallèle", "int", "--workers", default=5)),
optional=True, optional=True,
requires=("Copies",), requires=("Copies",),
auto_start_first_visit=ALWAYS_CROP,
), ),
StepDefinition( StepDefinition(
"cutleft", "cutleft",
@@ -215,6 +230,10 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
), ),
requires=("labels", "Copies", "Cutleft"), requires=("labels", "Copies", "Cutleft"),
artifacts=("Copies/*.json",), artifacts=("Copies/*.json",),
extra_arguments_help=(
"PDF de copie ou images Cutleft supplémentaires de cette évaluation, "
"séparés par des espaces. Mettez entre guillemets les chemins contenant des espaces."
),
), ),
StepDefinition( StepDefinition(
"plotting", "plotting",
@@ -257,6 +276,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
), ),
optional=True, optional=True,
requires=("Copies/Copie*/*.pdf",), requires=("Copies/Copie*/*.pdf",),
auto_start_first_visit=ALWAYS_CROP,
), ),
StepDefinition( StepDefinition(
"grouping", "grouping",
@@ -289,12 +309,29 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
), ),
arguments=( arguments=(
arg_target("Évaluation ou image Group_X.jpg"), arg_target("Évaluation ou image Group_X.jpg"),
ArgumentSpec("overwrite", "Écraser les corrections existantes", "bool", "--overwrite", variants=("live",)), ArgumentSpec(
ArgumentSpec("limit", "Limite dappels Pro", "int", "--limit", variants=("live",)), "overwrite",
"Écraser les corrections existantes",
"bool",
"--overwrite",
variants=("live", "batch", "hybrid", "refaire"),
),
ArgumentSpec(
"limit",
"Limite dappels Pro",
"int",
"--limit",
variants=("live", "hybrid", "refaire"),
),
ArgumentSpec("batch_from", "Premier label envoyé en batch", "text", "--batch-from", variants=("hybrid",)), ArgumentSpec("batch_from", "Premier label envoyé en batch", "text", "--batch-from", variants=("hybrid",)),
), ),
requires=("Par label", "Persp", "labels"), requires=("Par label", "Persp", "labels"),
artifacts=("correction.json", "batch_requests_*.jsonl"), artifacts=("correction.json", "batch_requests_*.jsonl"),
extra_arguments_help=(
"Images Group_X.jpg supplémentaires de cette évaluation, séparées par des espaces. "
"Mettez entre guillemets les chemins contenant des espaces."
),
extra_arguments_variants=("live", "batch", "hybrid", "refaire"),
), ),
StepDefinition( StepDefinition(
"submit_batches", "submit_batches",
@@ -313,7 +350,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"Consulter l’état des batchs", "Consulter l’état des batchs",
"Affiche les jobs Gemini en cours. Lidentifiant de téléchargement est facultatif.", "Affiche les jobs Gemini en cours. Lidentifiant de téléchargement est facultatif.",
(python("default", "État des batchs", "batch-status"),), (python("default", "État des batchs", "batch-status"),),
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),), arguments=(
ArgumentSpec("download", "Télécharger le job", "text", "--download"),
ArgumentSpec(
"output",
"Fichier JSONL de destination",
"path",
"--output",
help="Utilisé avec un identifiant de téléchargement.",
),
),
optional=True, optional=True,
skip_for_live_correction=True, skip_for_live_correction=True,
), ),
@@ -360,7 +406,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
arguments=( arguments=(
arg_target("Dossier de l’évaluation"), arg_target("Dossier de l’évaluation"),
ArgumentSpec("overwrite", "Écraser les sorties", "bool", "--overwrite"), ArgumentSpec("overwrite", "Écraser les sorties", "bool", "--overwrite"),
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("checks",)), ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("checks", "grouped")),
), ),
requires=("correction.json",), requires=("correction.json",),
artifacts=("Anot", "Bnot", "BGnot"), artifacts=("Anot", "Bnot", "BGnot"),
@@ -424,6 +470,15 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
arg_target("Dossier de l’évaluation"), arg_target("Dossier de l’évaluation"),
ArgumentSpec("update_score", "Réappliquer les score.json", "bool", "--update-score"), ArgumentSpec("update_score", "Réappliquer les score.json", "bool", "--update-score"),
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("grouped",)), ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("grouped",)),
ArgumentSpec(
"annotation_dir",
"Passage principal du mode refaire",
"choice",
"--annotation-dir",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
variants=("grouped",),
),
), ),
), ),
StepDefinition( StepDefinition(
@@ -466,7 +521,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"Étapes personnelles", "Étapes personnelles",
"Mettre à jour le fichier ODS", "Mettre à jour le fichier ODS",
"Transfère les scores, avec la possibilité de n’écrire que leur somme.", "Transfère les scores, avec la possibilité de n’écrire que leur somme.",
(python("default", "Mise à jour ODS", "update-ods"),), (python("default", "Mise à jour ODS", "update-ods", supports_verbose=False),),
arguments=( arguments=(
arg_target("Dossier de l’évaluation"), arg_target("Dossier de l’évaluation"),
ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"), ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"),
@@ -494,7 +549,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"Étapes personnelles", "Étapes personnelles",
"Ajouter le score final", "Ajouter le score final",
"Génère les fichiers de diffusion avec le score final.", "Génère les fichiers de diffusion avec le score final.",
(python("default", "Score final", "add-final-score"),), (python("default", "Score final", "add-final-score", supports_verbose=False),),
arguments=(arg_target("Dossier de l’évaluation"),), arguments=(arg_target("Dossier de l’évaluation"),),
personal=True, personal=True,
), ),
@@ -540,6 +595,12 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
"Continuer ?" "Continuer ?"
), ),
), ),
python(
"dry_run",
"Prévisualiser sans supprimer",
"clean",
fixed_args=("--dry-run",),
),
), ),
arguments=(arg_target("Dossier de l’évaluation"),), arguments=(arg_target("Dossier de l’évaluation"),),
optional=True, optional=True,
@@ -580,7 +641,14 @@ def build_refaire_workflow() -> list[StepDefinition]:
requirements += ("BRnot",) requirements += ("BRnot",)
steps.append(StepDefinition( steps.append(StepDefinition(
f"refaire_{suffix}", SECTION, title, description, f"refaire_{suffix}", SECTION, title, description,
(CommandVariant("default", title, program, "python" if program else "manual", flags),), (CommandVariant(
"default",
title,
program,
"python" if program else "manual",
flags,
supports_verbose=program is not None,
),),
arguments=arguments, optional=optional, requires=requirements, arguments=arguments, optional=optional, requires=requirements,
)) ))
return steps return steps
@@ -597,6 +665,7 @@ def build_command(
values: dict[str, object], values: dict[str, object],
evaluation_arg: str, evaluation_arg: str,
extra_arguments: str = "", extra_arguments: str = "",
verbose: bool = False,
) -> list[str]: ) -> list[str]:
if variant.kind == "manual" or not variant.program: if variant.kind == "manual" or not variant.program:
return [] return []
@@ -633,6 +702,8 @@ def build_command(
if not variant.fixed_args_before_positionals: if not variant.fixed_args_before_positionals:
command.extend(variant.fixed_args) command.extend(variant.fixed_args)
command.extend(options) command.extend(options)
if verbose and variant.supports_verbose:
command.append("--verbose")
if extra_arguments.strip(): if extra_arguments.strip():
command.extend(shlex.split(extra_arguments, posix=os.name != "nt")) command.extend(shlex.split(extra_arguments, posix=os.name != "nt"))
return command return command
+3
View File
@@ -9,6 +9,9 @@ IMPORT_DIR = Path("Import")
# Les étapes gestion_classe, ODS et publication sont masquées par défaut. # Les étapes gestion_classe, ODS et publication sont masquées par défaut.
SHOW_PERSONAL_STEPS = False SHOW_PERSONAL_STEPS = False
# Lance automatiquement les deux étapes facultatives de rognage dans le GUI.
ALWAYS_CROP = False
# Chemins utilisés uniquement par les étapes personnelles. # Chemins utilisés uniquement par les étapes personnelles.
CURRENT_SCORE_ODS_PATH = Path("current_eval.ods") CURRENT_SCORE_ODS_PATH = Path("current_eval.ods")
FINAL_SCORE_ODS_PATH = Path("simple_eval.ods") FINAL_SCORE_ODS_PATH = Path("simple_eval.ods")
+1 -1
View File
@@ -116,7 +116,7 @@ class CropExerciseBottomsCommandTests(unittest.TestCase):
self.assertEqual(steps[index - 1].id, "splitting") self.assertEqual(steps[index - 1].id, "splitting")
self.assertEqual(steps[index + 1].id, "grouping") self.assertEqual(steps[index + 1].id, "grouping")
self.assertTrue(steps[index].optional) self.assertTrue(steps[index].optional)
self.assertFalse(steps[index].auto_start_first_visit) self.assertTrue(steps[index].auto_start_first_visit)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -182,7 +182,7 @@ class CropMarginsCommandTests(unittest.TestCase):
self.assertEqual(steps[index-1].id, "page_splitter") self.assertEqual(steps[index-1].id, "page_splitter")
self.assertEqual(steps[index+1].id, "cutleft") self.assertEqual(steps[index+1].id, "cutleft")
self.assertTrue(steps[index].optional) self.assertTrue(steps[index].optional)
self.assertFalse(steps[index].auto_start_first_visit) self.assertTrue(steps[index].auto_start_first_visit)
if __name__ == "__main__": if __name__ == "__main__":
+32
View File
@@ -27,6 +27,18 @@ class GuiConvenienceTests(unittest.TestCase):
self.app.destroy() self.app.destroy()
self.temp.cleanup() self.temp.cleanup()
@staticmethod
def _label_texts(widget):
return [
text
for child in widget.winfo_children()
for text in (
[str(child.cget("text"))]
if isinstance(child, ttk.Label)
else GuiConvenienceTests._label_texts(child)
)
]
def test_reload_detects_added_and_removed_files_without_advancing(self): def test_reload_detects_added_and_removed_files_without_advancing(self):
for name in ("enonce.pdf", "enonce.tex", "correction.tex"): for name in ("enonce.pdf", "enonce.tex", "correction.tex"):
(self.evaluation / name).touch() (self.evaluation / name).touch()
@@ -57,6 +69,7 @@ class GuiConvenienceTests(unittest.TestCase):
copies.mkdir() copies.mkdir()
source = copies/"Copie01.pdf" source = copies/"Copie01.pdf"
source.touch() source.touch()
self.app.state_store.update_step("crop_blank_margins", visited=True)
self.app.tree.selection_set("crop_blank_margins") self.app.tree.selection_set("crop_blank_margins")
self.app.update() self.app.update()
self.assertEqual(self.app.current_step.id, "crop_blank_margins") self.assertEqual(self.app.current_step.id, "crop_blank_margins")
@@ -72,6 +85,7 @@ class GuiConvenienceTests(unittest.TestCase):
answers = self.evaluation / "Copies" / "Copie01" answers = self.evaluation / "Copies" / "Copie01"
answers.mkdir(parents=True) answers.mkdir(parents=True)
(answers / "Ex 1.pdf").touch() (answers / "Ex 1.pdf").touch()
self.app.state_store.update_step("crop_exercise_bottoms", visited=True)
self.app.tree.selection_set("crop_exercise_bottoms") self.app.tree.selection_set("crop_exercise_bottoms")
self.app.update() self.app.update()
self.assertEqual(self.app.current_step.id, "crop_exercise_bottoms") self.assertEqual(self.app.current_step.id, "crop_exercise_bottoms")
@@ -135,6 +149,24 @@ class GuiConvenienceTests(unittest.TestCase):
self.assertIn("Cibler la copie sélectionnée", controls) self.assertIn("Cibler la copie sélectionnée", controls)
self.assertIn("Cibler tout le dossier", controls) self.assertIn("Cibler tout le dossier", controls)
def test_free_form_arguments_are_shown_only_when_documented(self):
self.app.tree.selection_set("labels")
self.app.update()
labels_text = self._label_texts(self.app.form)
self.assertIn("Arguments supplémentaires", labels_text)
self.assertTrue(any("Cutleft" in text for text in labels_text))
self.app.tree.selection_set("plotting")
self.app.update()
self.assertNotIn("Arguments supplémentaires", self._label_texts(self.app.form))
def test_verbose_checkbox_updates_supported_commands(self):
self.app.tree.selection_set("labels")
self.app.update()
self.assertNotIn("--verbose", self.app._make_command())
self.app.verbose_var.set(True)
self.assertIn("--verbose", self.app._make_command())
def test_console_selection_survives_output_and_is_read_only(self): def test_console_selection_survives_output_and_is_read_only(self):
self.app._append_console("Première ligne\nDeuxième ligne\n") self.app._append_console("Première ligne\nDeuxième ligne\n")
self.app.console.tag_add("sel", "1.0", "1.end") self.app.console.tag_add("sel", "1.0", "1.end")
+116 -1
View File
@@ -1674,12 +1674,37 @@ class WorkflowTests(unittest.TestCase):
if step.skip_without_manual_conflicts if step.skip_without_manual_conflicts
} }
self.assertEqual(auto_start, {"splitting", "grouping"}) self.assertEqual(
auto_start,
{"crop_blank_margins", "splitting", "crop_exercise_bottoms", "grouping"},
)
self.assertEqual( self.assertEqual(
skip_for_live, {"submit_batches", "batch_status", "fetch_batches"} skip_for_live, {"submit_batches", "batch_status", "fetch_batches"}
) )
self.assertEqual(skip_without_conflicts, {"manual_resolution"}) self.assertEqual(skip_without_conflicts, {"manual_resolution"})
def test_crop_auto_start_follows_always_crop_configuration(self) -> None:
for enabled in (False, True):
with self.subTest(enabled=enabled), patch(
"copienator_gui.workflow.ALWAYS_CROP", enabled
):
steps = {step.id: step for step in build_workflow(False)}
self.assertEqual(
steps["crop_blank_margins"].auto_start_first_visit, enabled
)
self.assertEqual(
steps["crop_exercise_bottoms"].auto_start_first_visit, enabled
)
self.assertTrue(steps["crop_blank_margins"].optional)
self.assertTrue(steps["crop_exercise_bottoms"].optional)
def test_default_and_repository_crop_configuration(self) -> None:
import config
import default_config
self.assertFalse(default_config.ALWAYS_CROP)
self.assertTrue(config.ALWAYS_CROP)
def test_review_persp_has_shorter_title(self) -> None: def test_review_persp_has_shorter_title(self) -> None:
self.assertEqual(self.steps["review_persp"].title, "Relire les barèmes") self.assertEqual(self.steps["review_persp"].title, "Relire les barèmes")
@@ -1790,6 +1815,82 @@ class WorkflowTests(unittest.TestCase):
command_arguments(command), [self.evaluation, "--batch-from", "Ex 4"] command_arguments(command), [self.evaluation, "--batch-from", "Ex 4"]
) )
def test_only_multi_target_steps_offer_free_form_arguments(self) -> None:
documented = {
step.id: step
for step in self.steps.values()
if step.extra_arguments_help
}
self.assertEqual(set(documented), {"labels", "correction"})
self.assertIn("Cutleft", documented["labels"].extra_arguments_help)
self.assertIn("Group_X.jpg", documented["correction"].extra_arguments_help)
self.assertEqual(
documented["correction"].extra_arguments_variants,
("live", "batch", "hybrid", "refaire"),
)
def test_options_previously_requiring_free_form_arguments_have_controls(self) -> None:
status = self.command(
"batch_status",
"default",
{"download": "files/job-1", "output": "/tmp/result.jsonl"},
)
self.assertEqual(
command_arguments(status),
["--download", "files/job-1", "--output", "/tmp/result.jsonl"],
)
grouped = self.command(
"read_annotations",
"grouped",
{
"target": self.evaluation,
"refaire": True,
"annotation_dir": "Bnot",
},
)
self.assertEqual(
command_arguments(grouped),
[self.evaluation, "--refaire", "--annotation-dir", "Bnot"],
)
def test_verbose_is_added_only_to_commands_that_support_it(self) -> None:
labels = self.steps["labels"]
labels_command = build_command(
REPOSITORY,
labels,
labels.variants[0],
{"target": self.evaluation},
self.evaluation,
verbose=True,
)
self.assertEqual(command_arguments(labels_command), [self.evaluation, "--verbose"])
rotate = self.steps["rotate"]
rotate_command = build_command(
REPOSITORY,
rotate,
rotate.variants[0],
{"target": self.evaluation},
self.evaluation,
verbose=True,
)
self.assertEqual(
command_arguments(rotate_command),
["rotate", self.evaluation, "--verbose"],
)
update = self.steps["update_ods"]
update_command = build_command(
REPOSITORY,
update,
update.variants[0],
{"target": self.evaluation},
self.evaluation,
verbose=True,
)
self.assertNotIn("--verbose", update_command)
def test_annotation_variants_are_exclusive_commands(self) -> None: def test_annotation_variants_are_exclusive_commands(self) -> None:
command = self.command( command = self.command(
"annotation", "grouped", {"target": self.evaluation, "overwrite": True} "annotation", "grouped", {"target": self.evaluation, "overwrite": True}
@@ -1815,6 +1916,20 @@ class WorkflowTests(unittest.TestCase):
), ),
[self.evaluation, "--yes"], [self.evaluation, "--yes"],
) )
preview = next(variant for variant in clean.variants if variant.id == "dry_run")
self.assertFalse(preview.dangerous)
self.assertEqual(
command_arguments(
build_command(
REPOSITORY,
clean,
preview,
{"target": self.evaluation},
self.evaluation,
)
),
[self.evaluation, "--dry-run"],
)
class CrossPlatformFileTests(unittest.TestCase): class CrossPlatformFileTests(unittest.TestCase):