Claude commited on
Commit
a23e336
·
unverified ·
1 Parent(s): 5e13c0d

chore: finir le retrait execution_mode (audit du commit precedent)

Browse files

Audit du commit 5e13c0d a revele 13 defauts ; ce commit les corrige.

## Zombies de code production

- adapters/ocr/pero_ocr.py : retire les 2 mentions ProcessPool
qui motivaient le lazy-init du parser. Le pattern reste valable
(eviter de charger PyTorch au constructeur), seul son rationale
fictif disparait.
- adapters/ocr/calamari.py : ajoute la justification GIL/TensorFlow
(asymetrie avec tesseract/pero/kraken corrigee).

## Mensonges documentation actifs

- docs/explanation/architecture.md : runner.py n'est plus decrit
comme "ProcessPool/ThreadPool" mais "ThreadPool unique".
- docs/reference/specification.md : la table "4.2 Moteurs OCR
livres" ne dit plus "CPU (ProcessPool)" / "IO (ThreadPool)"
mais decrit le profil d'execution reel (sous-processus C, PyTorch,
appel HTTP).
- docs/operations/deployment-institutional.md : le sizing institutionnel
ne parle plus de "3 GB RAM par worker ProcessPool" ni de
"ProcessPool 8 workers" mais bien du ThreadPool reel et de
max_in_flight.

## Rechutes soft sur l'option B

- pipeline/runner.py : la docstring retire la phrase "redeviendra
recevable... commit 047ab1b" qui contredisait le choix YAGNI.
- specification.md (section 4.1) : meme nettoyage, la phrase
"un dispatch multi-pool avait ete prototype puis suspendu"
remplacee par une instruction de revue de PR.
- README.md : "all supported adapters release the GIL" recadre en
"today's adapters happen to release the GIL" pour ne pas faire
croire a un invariant du projet.
- CHANGELOG : retire l'enumeration des cas d'usage hypothetiques
(overnorm, voter, MUFI) ; ces fonctionnalites ne sont pas sur
la roadmap publique, les nommer dans un changelog est du wishful
thinking architectural.

## Inconsistances introduites

- test_doc_paths.py : commentaire au-dessus de BROKEN_PATHS_BASELINE
raconte maintenant la transition 6 -> 5 et liste les 5 chemins
restants (etait reste a "6 chemins" apres modification de la
constante).

## Garde-fou trop etroit

- test_no_execution_mode_resurrection.py : scanne maintenant aussi
README.md, CLAUDE.md et docs/*.md (hors docs/archive/) en plus du
code Python. Limite assumee (symbole exact, pas concept renomme)
explicitee dans la docstring. CHANGELOG.md et test_doc_paths.py
whitelistes (mentions historiques legitimes).

## Mensonge oublie dans conftest.py

- tests/conftest.py : le workaround tqdm._monitor invoquait
"ProcessPoolExecutor du runner Picarones" pour justifier la
desactivation. Le runner n'est plus ProcessPool. Commentaire
reecrit pour assumer que la cause racine a pu disparaitre et
qu'il faudra revalider au prochain passage.

## Resultat

- 6058 tests passent (idem, 0 regression).
- ruff check vert.
- +55 LOC nets (essentiellement docstring guardrail elargie + recits
honnetes des choix dans la doc).

https://claude.ai/code/session_01B93huMjNh4CG2rNcexgDeL

CHANGELOG.md CHANGED
@@ -118,10 +118,7 @@ L'attribut était un mensonge structurel. L'historique git portait
118
  une tentative de dispatch multi-pool (``MultiDomainCorpusRunner``,
119
  commit ``047ab1b``) qui a été suspendue (``cd67184``). Aucun adapter
120
  actuel n'est GIL-bound en pratique — chacun délègue à du C qui relâche
121
- le GIL. Le jour un adapter Python-pur GIL-bound deviendra utile
122
- (détecteur d'over-normalisation, voter d'ensemble, MUFI scorer en
123
- step de pipeline plutôt qu'en métrique), la décision sera explicite
124
- plutôt que portée par un attribut décoratif.
125
 
126
  ---
127
 
 
118
  une tentative de dispatch multi-pool (``MultiDomainCorpusRunner``,
119
  commit ``047ab1b``) qui a été suspendue (``cd67184``). Aucun adapter
120
  actuel n'est GIL-bound en pratique — chacun délègue à du C qui relâche
121
+ le GIL. Retirer plutôt que documenter un attribut zombi : YAGNI.
 
 
 
122
 
123
  ---
124
 
README.md CHANGED
@@ -432,10 +432,12 @@ CONTRIBUTING, SECURITY, ACCESSIBILITY, and the
432
  `logger.warning("[module] degraded feature: %s", e)`.
433
  - One canonical home per module — circle dependency direction
434
  enforced by tests.
435
- - The `CorpusRunner` is thread-only by design all supported
436
- adapters (OCR via C binary, LLM/VLM via httpx, ML via
437
- PyTorch/TensorFlow) release the GIL during their blocking
438
- call, so a single thread pool delivers the expected throughput.
 
 
439
  - Hardcoded UI strings forbidden — always go through i18n
440
  (cf. [`docs/developer/extending-i18n.md`](docs/developer/extending-i18n.md)).
441
 
 
432
  `logger.warning("[module] degraded feature: %s", e)`.
433
  - One canonical home per module — circle dependency direction
434
  enforced by tests.
435
+ - The `CorpusRunner` is thread-only by design. Today's adapters
436
+ (OCR via C binary, LLM/VLM via httpx, ML via PyTorch/TensorFlow)
437
+ happen to release the GIL during their blocking call, which is
438
+ why a single thread pool works. A future adapter doing pure-Python
439
+ CPU work would not benefit from parallelism — that's a constraint
440
+ for adapter authors to verify, not a global invariant.
441
  - Hardcoded UI strings forbidden — always go through i18n
442
  (cf. [`docs/developer/extending-i18n.md`](docs/developer/extending-i18n.md)).
443
 
docs/explanation/architecture.md CHANGED
@@ -102,7 +102,7 @@ Orchestration mono-document d'une pipeline composée :
102
  | `executor.py` | `PipelineExecutor` — exécute un `PipelineSpec` step par step, capture `StepResult`, filtre outputs sur `step.output_types` |
103
  | `planner.py` | `PipelinePlanner` — résout les `inputs_from`, valide la spec, calcule les métriques aux jonctions |
104
  | `validation.py` | Validation statique d'une `PipelineSpec` (types s'enchaînent, pas de cycle) |
105
- | `runner.py` | `CorpusRunner` — orchestration corpus-wide avec ProcessPool/ThreadPool, backpressure, timeout, cancellation |
106
  | `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
107
  | `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
108
  | `llm_pipeline_builder.py` | `make_ocr_llm_pipeline_spec` (3 modes : text_only, text_and_image, zero_shot) |
 
102
  | `executor.py` | `PipelineExecutor` — exécute un `PipelineSpec` step par step, capture `StepResult`, filtre outputs sur `step.output_types` |
103
  | `planner.py` | `PipelinePlanner` — résout les `inputs_from`, valide la spec, calcule les métriques aux jonctions |
104
  | `validation.py` | Validation statique d'une `PipelineSpec` (types s'enchaînent, pas de cycle) |
105
+ | `runner.py` | `CorpusRunner` — orchestration corpus-wide (ThreadPool unique), backpressure, timeout, cancellation |
106
  | `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
107
  | `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
108
  | `llm_pipeline_builder.py` | `make_ocr_llm_pipeline_spec` (3 modes : text_only, text_and_image, zero_shot) |
docs/operations/deployment-institutional.md CHANGED
@@ -17,8 +17,10 @@
17
  - **Python 3.11 ou 3.12** (3.13 informationnel).
18
  - **Tesseract OCR ≥ 5.3** (avec packs `fra`, `lat`, `eng` au
19
  minimum).
20
- - **3 GB RAM par worker** (le ProcessPool spawne un sous-processus
21
- par moteur ; profil mémoire dominé par Pillow + jiwer).
 
 
22
  - **5 GB de disque** pour l'application + 50 GB recommandés pour
23
  les uploads et la base SQLite des jobs.
24
 
@@ -272,7 +274,7 @@ uniquement** (jamais sur le filesystem en clair). Voir [`SECURITY.md`](../../SEC
272
  | Charge | Configuration |
273
  |---|---|
274
  | < 5 jobs/h, < 5 utilisateurs | Mono-instance, SQLite, 2 vCPU / 4 GB RAM |
275
- | 5–50 jobs/h, < 20 utilisateurs | Mono-instance, SQLite, 4 vCPU / 8 GB RAM, ProcessPool 8 workers |
276
  | > 50 jobs/h | Multi-instance derrière LB, PostgreSQL centralisé, NFS uploads |
277
  | > 500 jobs/h | Considérer un orchestrateur de tâches dédié (Celery + Redis), hors scope Picarones |
278
 
 
17
  - **Python 3.11 ou 3.12** (3.13 informationnel).
18
  - **Tesseract OCR ≥ 5.3** (avec packs `fra`, `lat`, `eng` au
19
  minimum).
20
+ - **3 GB RAM** pour un worker FastAPI exécutant `max_in_flight=4`
21
+ documents en parallèle dans le ThreadPool du `CorpusRunner`
22
+ (profil mémoire dominé par Pillow + jiwer + les modèles OCR locaux
23
+ chargés une fois par instance).
24
  - **5 GB de disque** pour l'application + 50 GB recommandés pour
25
  les uploads et la base SQLite des jobs.
26
 
 
274
  | Charge | Configuration |
275
  |---|---|
276
  | < 5 jobs/h, < 5 utilisateurs | Mono-instance, SQLite, 2 vCPU / 4 GB RAM |
277
+ | 5–50 jobs/h, < 20 utilisateurs | Mono-instance, SQLite, 4 vCPU / 8 GB RAM, `max_in_flight=8` |
278
  | > 50 jobs/h | Multi-instance derrière LB, PostgreSQL centralisé, NFS uploads |
279
  | > 500 jobs/h | Considérer un orchestrateur de tâches dédié (Celery + Redis), hors scope Picarones |
280
 
docs/reference/specification.md CHANGED
@@ -308,23 +308,23 @@ ses `input_types` et `output_types` et implémente
308
  `execute(inputs, params, context, control) -> dict[ArtifactType, Artifact]`.
309
 
310
  Le `CorpusRunner` orchestre l'exécution via un `ThreadPoolExecutor`
311
- unique pour tous les adapters. Les adapters supportés (OCR via
312
  binaire C, LLM/VLM via httpx, ML via PyTorch/TensorFlow) relâchent
313
  le GIL pendant leur travail bloquant, donc un thread pool donne
314
- les performances attendues. Un dispatch multi-pool (ThreadPool +
315
- ProcessPool selon le profil d'exécution) avait été prototypé puis
316
- suspendu il pourra être réintroduit si un adapter Python-pur
317
- GIL-bound apparaît.
318
 
319
  ### 4.2 Moteurs OCR livrés
320
 
321
- | Moteur | Type | Mode d'exécution | Confidence native exposée ? |
322
  |---|---|---|---|
323
- | **Tesseract 5** | Local CLI | CPU (ProcessPool) | ✅ Sprint 47 (`image_to_data`) |
324
- | **Pero OCR** | Local Python | CPU (ProcessPool) | ✅ Sprint 48 (`transcription_confidence` ligne) |
325
- | **Mistral OCR** | Cloud API | IO (ThreadPool) | ✅ Sprint 49 (quand disponible côté API) |
326
- | **Google Vision** | Cloud API | IO (ThreadPool) | ✅ Sprint 50 (`Word.confidence` en mode `DOCUMENT_TEXT_DETECTION`) |
327
- | **Azure Doc Intelligence** | Cloud API | IO (ThreadPool) | ✅ Sprint 51 (`Word.confidence`) |
328
 
329
  Quand un moteur expose ses confidences natives, le runner calcule
330
  automatiquement les métriques de calibration (ECE, MCE, reliability
 
308
  `execute(inputs, params, context, control) -> dict[ArtifactType, Artifact]`.
309
 
310
  Le `CorpusRunner` orchestre l'exécution via un `ThreadPoolExecutor`
311
+ unique pour tous les adapters. Les adapters actuels (OCR via
312
  binaire C, LLM/VLM via httpx, ML via PyTorch/TensorFlow) relâchent
313
  le GIL pendant leur travail bloquant, donc un thread pool donne
314
+ les performances attendues. Un adapter qui ferait du calcul Python
315
+ pur ne profiterait pas de la parallélisation : c'est à l'auteur de
316
+ l'adapter de vérifier en revue de PR que son `execute()` n'est pas
317
+ GIL-bound avant de prétendre tourner en parallèle.
318
 
319
  ### 4.2 Moteurs OCR livrés
320
 
321
+ | Moteur | Type | Profil dominant | Confidence native exposée ? |
322
  |---|---|---|---|
323
+ | **Tesseract 5** | Local CLI | Sous-processus C (GIL relâché) | ✅ Sprint 47 (`image_to_data`) |
324
+ | **Pero OCR** | Local Python | Inférence PyTorch (GIL relâché) | ✅ Sprint 48 (`transcription_confidence` ligne) |
325
+ | **Mistral OCR** | Cloud API | Appel HTTP | ✅ Sprint 49 (quand disponible côté API) |
326
+ | **Google Vision** | Cloud API | Appel HTTP | ✅ Sprint 50 (`Word.confidence` en mode `DOCUMENT_TEXT_DETECTION`) |
327
+ | **Azure Doc Intelligence** | Cloud API | Appel HTTP | ✅ Sprint 51 (`Word.confidence`) |
328
 
329
  Quand un moteur expose ses confidences natives, le runner calcule
330
  automatiquement les métriques de calibration (ECE, MCE, reliability
picarones/adapters/ocr/calamari.py CHANGED
@@ -9,6 +9,8 @@ Calamari est un OCR open-source basé TensorFlow / Keras, conçu pour
9
  les imprimés historiques et la transcription ligne par ligne.
10
  Modèles disponibles via OCR-D, Wikisource, et le hub Calamari.
11
  Particulièrement performant en ensemble (vote multi-modèles).
 
 
12
 
13
  Configuration
14
  -------------
 
9
  les imprimés historiques et la transcription ligne par ligne.
10
  Modèles disponibles via OCR-D, Wikisource, et le hub Calamari.
11
  Particulièrement performant en ensemble (vote multi-modèles).
12
+ L'inférence passe par TensorFlow (extension C++) qui relâche le GIL,
13
+ donc le ``CorpusRunner`` thread-only suffit.
14
 
15
  Configuration
16
  -------------
picarones/adapters/ocr/pero_ocr.py CHANGED
@@ -36,9 +36,9 @@ Anti-sur-ingénierie
36
  -------------------
37
  - Pas de support GPU explicite (Pero OCR le gère via la config).
38
  - Pas de retry, pas d'extraction de confidences (à ajouter quand un caller en aura besoin).
39
- - ``_parser`` lazy-init si l'instance est sérialisée pour
40
- ProcessPool, le parser est re-instancié dans le worker (cohérent
41
- avec Pero OCR qui charge ses modèles à l'instanciation).
42
  """
43
 
44
  from __future__ import annotations
@@ -96,8 +96,8 @@ class PeroOCRAdapter(BaseOCRAdapter):
96
  self._name = name
97
  self._config_path = Path(config_path)
98
  # Le parser est instancié paresseusement au premier execute()
99
- # pour que la sérialisation ProcessPool fonctionne (un parser
100
- # contenant des modèles PyTorch n'est pas sérialisable).
101
  self._parser: Any = None
102
 
103
  @property
 
36
  -------------------
37
  - Pas de support GPU explicite (Pero OCR le gère via la config).
38
  - Pas de retry, pas d'extraction de confidences (à ajouter quand un caller en aura besoin).
39
+ - ``_parser`` lazy-init : le modèle PyTorch n'est chargé qu'au
40
+ premier ``execute()``, pas au constructeur permet d'instancier
41
+ l'adapter sans pénalité même si le doc n'est jamais exécuté.
42
  """
43
 
44
  from __future__ import annotations
 
96
  self._name = name
97
  self._config_path = Path(config_path)
98
  # Le parser est instancié paresseusement au premier execute()
99
+ # le constructeur reste léger et l'on évite de charger les
100
+ # modèles PyTorch tant qu'aucun document n'est traité.
101
  self._parser: Any = None
102
 
103
  @property
picarones/pipeline/runner.py CHANGED
@@ -25,14 +25,14 @@ avec trois propriétés critiques que l'ancien
25
 
26
  Limites assumées
27
  ----------------
28
- - **Pool d'exécution unique : ThreadPool.** Tous les adapters
29
- supportés (OCR via binaire C, LLM/VLM via httpx, ML via
30
- PyTorch/TensorFlow) délèguent leur travail bloquant à du code
31
- natif qui relâche le GIL, donc un thread pool unique donne les
32
- performances attendues. Si un futur adapter fait du calcul
33
- Python pur GIL-bound, un dispatch vers ``ProcessPoolExecutor``
34
- redeviendra recevable un prototype existe dans l'historique
35
- git (commit 047ab1b, depuis suspendu).
36
  - **Cancel coopératif sur les in-flight.** Quand ``cancel_event``
37
  est signalé, le runner appelle ``trigger_cancel()`` sur le
38
  ``RunControl`` de chaque doc en cours : les adapters qui ont
 
25
 
26
  Limites assumées
27
  ----------------
28
+ - **Pool d'exécution unique : ThreadPool.** Choix de conception
29
+ assumé. Les adapters actuels (OCR via binaire C, LLM/VLM via
30
+ httpx, ML via PyTorch/TensorFlow) délèguent leur travail bloquant
31
+ à du code natif qui relâche le GIL, donc un thread pool unique
32
+ suffit en pratique. Un adapter qui ferait du calcul Python pur
33
+ ne profiterait pas de la parallélisation — c'est sa charge de
34
+ vérifier que son ``execute()`` n'est pas GIL-bound (cf. revue de
35
+ PR) avant de prétendre tourner en parallèle.
36
  - **Cancel coopératif sur les in-flight.** Quand ``cancel_event``
37
  est signalé, le runner appelle ``trigger_cancel()`` sur le
38
  ``RunControl`` de chaque doc en cours : les adapters qui ont
tests/architecture/test_doc_paths.py CHANGED
@@ -162,11 +162,13 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
162
  #: pré-v2.0 ont été consolidés sous ``docs/archive/``. Les 35
163
  #: chemins cassés qu'ils portaient sortent du périmètre actif (cf.
164
  #: ``EXCLUDED_PATH_PREFIXES`` ci-dessous).
 
 
 
165
  #:
166
- #: Les 6 chemins restants sont dans la doc active :
167
  #: - CHANGELOG.md (4) : refs Sprint H.4/H.6 dans la section
168
  #: migration v2.0 (intouchables sans réécrire l'historique 2.0) ;
169
- #: - SPECS.md (1) : exemple YAML legacy à corriger en Phase 2 ou v2.1 ;
170
  #: - docs/explanation/architecture.md (1) : ref historique au shim
171
  #: ``picarones/pipeline/spec.py`` supprimé en Sprint S7.
172
  BROKEN_PATHS_BASELINE = 5
 
162
  #: pré-v2.0 ont été consolidés sous ``docs/archive/``. Les 35
163
  #: chemins cassés qu'ils portaient sortent du périmètre actif (cf.
164
  #: ``EXCLUDED_PATH_PREFIXES`` ci-dessous).
165
+ #: Retrait ``execution_mode`` (mai 2026) : 6 → 5. La section 4.1 de
166
+ #: ``docs/reference/specification.md`` ne pointe plus vers
167
+ #: ``picarones/adapters/legacy_engines/base.py`` (path supprimé).
168
  #:
169
+ #: Les 5 chemins restants sont dans la doc active :
170
  #: - CHANGELOG.md (4) : refs Sprint H.4/H.6 dans la section
171
  #: migration v2.0 (intouchables sans réécrire l'historique 2.0) ;
 
172
  #: - docs/explanation/architecture.md (1) : ref historique au shim
173
  #: ``picarones/pipeline/spec.py`` supprimé en Sprint S7.
174
  BROKEN_PATHS_BASELINE = 5
tests/architecture/test_no_execution_mode_resurrection.py CHANGED
@@ -21,8 +21,25 @@ Ce test bloque sa réintroduction silencieuse. Si tu le vois échouer :
21
  profite pas du thread pool), réintroduire l'attribut **avec** le
22
  dispatch effectif dans le runner — sinon c'est une fausse promesse.
23
 
24
- Le test scanne les sources Python du package et de la suite de tests
25
- pour détecter toute réapparition.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  """
27
 
28
  from __future__ import annotations
@@ -34,20 +51,54 @@ import pytest
34
 
35
  _FORBIDDEN_TOKENS = ("execution_mode", "ExecutionMode")
36
  _REPO_ROOT = Path(__file__).resolve().parent.parent.parent
37
- _SCAN_ROOTS = (_REPO_ROOT / "picarones", _REPO_ROOT / "tests")
 
 
 
 
 
38
  _SELF = Path(__file__).resolve()
39
 
 
 
 
 
 
 
 
40
 
41
- @pytest.mark.parametrize("token", _FORBIDDEN_TOKENS)
42
- def test_no_execution_mode_resurrection(token: str) -> None:
43
- offenders: list[str] = []
44
- for root in _SCAN_ROOTS:
 
 
 
 
45
  for py in root.rglob("*.py"):
46
  if py.resolve() == _SELF:
47
  continue
48
- text = py.read_text(encoding="utf-8")
49
- if token in text:
50
- offenders.append(str(py.relative_to(_REPO_ROOT)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  assert not offenders, (
52
  f"{token!r} a été réintroduit dans : "
53
  + ", ".join(offenders)
 
21
  profite pas du thread pool), réintroduire l'attribut **avec** le
22
  dispatch effectif dans le runner — sinon c'est une fausse promesse.
23
 
24
+ Portée du scan
25
+ --------------
26
+ Le test couvre :
27
+
28
+ - ``picarones/`` et ``tests/`` (fichiers ``*.py``) — empêche la
29
+ réintroduction du symbole dans le code et les tests ;
30
+ - ``README.md``, ``CLAUDE.md``, ``CHANGELOG.md`` et ``docs/``
31
+ (``*.md``) — empêche la réintroduction de la **promesse** dans
32
+ la documentation active, là où le drift est historiquement le
33
+ plus dommageable.
34
+
35
+ Le ``CHANGELOG.md`` peut légitimement mentionner le symbole pour
36
+ relater le retrait — l'entrée concernée est donc whitelistée.
37
+
38
+ Limite assumée : on ne détecte que le symbole exact ``execution_mode``
39
+ / ``ExecutionMode``. Un attribut renommé (``runtime_mode``,
40
+ ``exec_profile``…) passerait ce test ; il appartient au revieweur de
41
+ ne pas réintroduire le concept sous un autre nom sans le dispatch
42
+ effectif.
43
  """
44
 
45
  from __future__ import annotations
 
51
 
52
  _FORBIDDEN_TOKENS = ("execution_mode", "ExecutionMode")
53
  _REPO_ROOT = Path(__file__).resolve().parent.parent.parent
54
+ _PY_ROOTS = (_REPO_ROOT / "picarones", _REPO_ROOT / "tests")
55
+ _DOC_ROOTS = (_REPO_ROOT / "docs",)
56
+ _DOC_FILES = (
57
+ _REPO_ROOT / "README.md",
58
+ _REPO_ROOT / "CLAUDE.md",
59
+ )
60
  _SELF = Path(__file__).resolve()
61
 
62
+ #: Fichiers qui mentionnent légitimement les symboles retirés pour
63
+ #: documenter leur disparition (CHANGELOG, ratchet doc-paths).
64
+ #: Le whitelister garde le test utile sans bloquer l'historique narratif.
65
+ _WHITELIST: frozenset[Path] = frozenset({
66
+ (_REPO_ROOT / "CHANGELOG.md").resolve(),
67
+ (_REPO_ROOT / "tests" / "architecture" / "test_doc_paths.py").resolve(),
68
+ })
69
 
70
+ #: ``docs/archive/`` capture l'historique pré-0.9.0 (sprints, ADRs
71
+ #: rejetées, plans de migration). Réécrire ces archives serait une
72
+ #: réécriture d'historique.
73
+ _EXCLUDED_DOC_PREFIXES: tuple[str, ...] = ("docs/archive/",)
74
+
75
+
76
+ def _iter_scanned_files():
77
+ for root in _PY_ROOTS:
78
  for py in root.rglob("*.py"):
79
  if py.resolve() == _SELF:
80
  continue
81
+ yield py
82
+ for root in _DOC_ROOTS:
83
+ for md in root.rglob("*.md"):
84
+ rel = md.relative_to(_REPO_ROOT).as_posix()
85
+ if any(rel.startswith(p) for p in _EXCLUDED_DOC_PREFIXES):
86
+ continue
87
+ yield md
88
+ for path in _DOC_FILES:
89
+ if path.exists():
90
+ yield path
91
+
92
+
93
+ @pytest.mark.parametrize("token", _FORBIDDEN_TOKENS)
94
+ def test_no_execution_mode_resurrection(token: str) -> None:
95
+ offenders: list[str] = []
96
+ for path in _iter_scanned_files():
97
+ if path.resolve() in _WHITELIST:
98
+ continue
99
+ text = path.read_text(encoding="utf-8")
100
+ if token in text:
101
+ offenders.append(str(path.relative_to(_REPO_ROOT)))
102
  assert not offenders, (
103
  f"{token!r} a été réintroduit dans : "
104
  + ", ".join(offenders)
tests/conftest.py CHANGED
@@ -50,20 +50,19 @@ os.environ.setdefault("PICARONES_RATE_LIMIT_PER_HOUR", "0")
50
 
51
 
52
  # (3) Désactivation préventive du thread daemon de tqdm.
53
- # Sur Python 3.12+ (ubuntu-latest en CI), le combo
54
- # ``tqdm._monitor`` + ``ProcessPoolExecutor`` (utilisé par
55
- # ``picarones.measurements.runner.orchestration`` pour les moteurs
56
- # CPU-bound : Tesseract, Pero OCR) provoque un hang du shutdown de
57
- # l'interpréteur après ``=== passed ===``. Le ``_python_exit`` de
58
- # ``concurrent.futures.process`` essaie de joindre les workers du
59
- # pool, mais le thread monitor de tqdm bloque la sortie globale —
60
- # le hang dépasse le timeout GNU configuré dans ci.yml (9 min) et
61
- # le job échoue avec exit code 124.
62
  #
63
- # ``monitor_interval=0`` désactive le polling thread de tqdm, qui
64
- # n'est utile qu'à l'affichage interactif des progress bars (sans
65
- # valeur ajoutée en CI où stdout est captured). Fix idiomatique
66
- # pour ce flake spécifique.
 
 
 
 
67
  try:
68
  from tqdm import tqdm as _tqdm
69
 
 
50
 
51
 
52
  # (3) Désactivation préventive du thread daemon de tqdm.
53
+ # Workaround historique pour un hang de shutdown observé en CI
54
+ # Python 3.12+ (ubuntu-latest) : ``tqdm._monitor`` reste bloqué et
55
+ # empêche la sortie de l'interpréteur après ``=== passed ===``, ce
56
+ # qui faisait dépasser le timeout GNU de ci.yml (9 min, exit 124).
 
 
 
 
 
57
  #
58
+ # Le diagnostic originel pointait une interaction avec
59
+ # ``ProcessPoolExecutor`` du runner historique ; ce runner a été
60
+ # remplacé par un ``ThreadPoolExecutor`` (cf.
61
+ # ``picarones/pipeline/runner.py``), donc la cause racine peut
62
+ # avoir disparu — à revalider la prochaine fois que ce workaround
63
+ # est révisé. En attendant, ``monitor_interval=0`` désactive le
64
+ # polling thread de tqdm (sans valeur en CI où stdout est captured)
65
+ # et reste inoffensif.
66
  try:
67
  from tqdm import tqdm as _tqdm
68