Skip to content

Commit 9d9b607

Browse files
committed
loader: passer a une decouverte 100% dynamique des engines
- supprime les chemins hardcodes d'enregistrement - importe les modules engines de maniere namespacée et robuste - enregistre dynamiquement les classes CompilerEngine par introspection - aligne engine_sdk.registry sur le registre actif EngineLoader - ajoute des logs explicites en cas d'echec de chargement de tab
1 parent a2d9c7b commit 9d9b607

3 files changed

Lines changed: 123 additions & 36 deletions

File tree

EngineLoader/Loader/EngineLoader.py

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,95 @@
1414
# limitations under the License.
1515

1616
import importlib
17+
import inspect
1718
import os
1819
import pkgutil
1920
import sys
2021

2122

22-
def _discover_external_plugins(base_path: str) -> None:
23-
"""Import all top-level modules and packages under base_path (ENGINES/),
24-
recursively importing subpackages to trigger engine registration side-effects.
25-
"""
26-
try:
27-
if not os.path.isdir(base_path):
28-
return
29-
if base_path not in sys.path:
30-
sys.path.insert(0, base_path)
31-
# Import only top-level PACKAGES (directories with __init__.py). Skip bare modules.
32-
for _finder, name, ispkg in pkgutil.iter_modules([base_path]):
33-
if not ispkg:
34-
# Enforce package-only engines
35-
continue
23+
def _import_engine_modules(base_path: str, project_root: str | None = None) -> list[object]:
24+
"""Import all engine packages and submodules found under ENGINES/ dynamically."""
25+
imported: list[object] = []
26+
if not os.path.isdir(base_path):
27+
return imported
28+
29+
# Prefer project-root namespace imports (ENGINES.<name>) to avoid collisions.
30+
if project_root and project_root not in sys.path:
31+
sys.path.insert(0, project_root)
32+
if base_path not in sys.path:
33+
sys.path.insert(0, base_path)
34+
35+
for _finder, name, ispkg in pkgutil.iter_modules([base_path]):
36+
if not ispkg:
37+
continue
38+
39+
module_names: list[str] = []
40+
if project_root:
41+
module_names.append(f"ENGINES.{name}")
42+
module_names.append(name)
43+
44+
package = None
45+
for mod_name in module_names:
3646
try:
37-
# Import the package so its __init__ can self-register the engine
38-
importlib.import_module(name)
39-
# Import submodules to trigger additional registrations if needed
40-
pkg_path = os.path.join(base_path, name)
41-
for __f, subname, __ispkg in pkgutil.walk_packages(
42-
[pkg_path], prefix=f"{name}."
43-
):
44-
try:
45-
importlib.import_module(subname)
46-
except Exception:
47-
# Ignore broken submodules to avoid crashing global discovery
48-
pass
47+
package = importlib.import_module(mod_name)
48+
imported.append(package)
49+
break
4950
except Exception:
50-
# Ignore broken plugins; do not crash host discovery
51-
pass
51+
continue
52+
if package is None:
53+
continue
54+
55+
pkg_name = getattr(package, "__name__", "")
56+
pkg_path = os.path.join(base_path, name)
57+
try:
58+
for _f, sub_name, _is_pkg in pkgutil.walk_packages(
59+
[pkg_path], prefix=f"{pkg_name}."
60+
):
61+
try:
62+
sub = importlib.import_module(sub_name)
63+
imported.append(sub)
64+
except Exception:
65+
continue
66+
except Exception:
67+
continue
68+
69+
return imported
70+
71+
72+
def _register_discovered_engines(modules: list[object]) -> None:
73+
"""Register all CompilerEngine subclasses discovered in imported modules."""
74+
try:
75+
from EngineLoader import registry as _registry # local import to avoid cycles
76+
from EngineLoader.base import CompilerEngine
5277
except Exception:
53-
# Never let discovery break the host application
54-
pass
78+
return
79+
80+
for mod in modules:
81+
try:
82+
for _, cls in inspect.getmembers(mod, inspect.isclass):
83+
try:
84+
if not issubclass(cls, CompilerEngine) or cls is CompilerEngine:
85+
continue
86+
except Exception:
87+
continue
88+
try:
89+
eid = getattr(cls, "id", None)
90+
if not isinstance(eid, str) or not eid.strip():
91+
continue
92+
_registry.register(cls)
93+
except Exception:
94+
continue
95+
except Exception:
96+
continue
5597

5698

5799
def _auto_discover() -> None:
58-
"""Discover and register external engine plugins ONLY from the 'ENGINES' folder at project root."""
100+
"""Discover and register engine plugins dynamically from the ENGINES folder."""
59101
base_dir = os.path.dirname(__file__)
60102
try:
61103
project_root = os.path.abspath(os.path.join(base_dir, os.pardir, os.pardir))
62104
external_dir = os.path.join(project_root, "ENGINES")
63-
_discover_external_plugins(external_dir)
105+
modules = _import_engine_modules(external_dir, project_root=project_root)
106+
_register_discovered_engines(modules)
64107
except Exception:
65108
pass

EngineLoader/registry.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,26 @@ def _wrap_tab_scroll(widget):
253253
except Exception:
254254
return widget
255255

256+
def _debug_log(level: str, fr: str, en: str) -> None:
257+
try:
258+
from .base import log_i18n_level as _log_i18n_level
259+
260+
_log_i18n_level(gui, level, fr, en)
261+
return
262+
except Exception:
263+
pass
264+
try:
265+
msg = en
266+
if hasattr(gui, "tr") and callable(getattr(gui, "tr")):
267+
msg = gui.tr(fr, en)
268+
line = f"[{level.upper()}] {msg}"
269+
if hasattr(gui, "log") and getattr(gui, "log", None) is not None:
270+
gui.log.append(line)
271+
else:
272+
print(line)
273+
except Exception:
274+
pass
275+
256276
for eid in list(_ORDER):
257277
try:
258278
engine = create(eid)
@@ -276,6 +296,14 @@ def _wrap_tab_scroll(widget):
276296
else:
277297
idx = tabs.addTab(widget, label)
278298
_TAB_INDEX[eid] = int(idx)
299+
try:
300+
_debug_log(
301+
"state",
302+
f"Moteur charge dans les tabs: {eid}",
303+
f"Engine loaded into tabs: {eid}",
304+
)
305+
except Exception:
306+
pass
279307
# Apply engine i18n immediately if GUI already has active translations
280308
try:
281309
tr = getattr(gui, "_tr", None)
@@ -284,7 +312,12 @@ def _wrap_tab_scroll(widget):
284312
fn(gui, tr)
285313
except Exception:
286314
pass
287-
except Exception:
315+
except Exception as e:
316+
_debug_log(
317+
"warning",
318+
f"Echec chargement tab moteur '{eid}': {e}",
319+
f"Failed loading engine tab '{eid}': {e}",
320+
)
288321
# keep UI responsive even if a plugin tab fails
289322
continue
290323

@@ -294,6 +327,12 @@ def _wrap_tab_scroll(widget):
294327
tabs.tabBar().hideTab(hello_tab_index)
295328
except Exception:
296329
pass
330+
elif not any_engine_tab_created:
331+
_debug_log(
332+
"warning",
333+
"Aucun moteur n'a fourni de tab UI.",
334+
"No engine provided a UI tab.",
335+
)
297336
except Exception:
298337
# Swallow to avoid breaking app init
299338
pass

engine_sdk/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,16 @@
4949
)
5050
from .sysdep import SysDependencyManager # type: ignore
5151

52-
# Re-export engines registry for self-registration from engine packages
52+
# Re-export engines registry for self-registration from engine packages.
53+
# Priority is the active host registry used by current GUI/runtime.
5354
try:
54-
from utils.engines_loader import registry as registry # type: ignore
55+
from EngineLoader import registry as registry # type: ignore
5556
except Exception: # pragma: no cover
56-
registry = None # type: ignore
57+
try:
58+
# Backward-compat fallback for older layouts.
59+
from utils.engines_loader import registry as registry # type: ignore
60+
except Exception: # pragma: no cover
61+
registry = None # type: ignore
5762

5863

5964
__version__ = "3.2.3"

0 commit comments

Comments
 (0)