Skip to content

Commit aa5921a

Browse files
committed
bcasl: Update plugin registration and discovery
* Update `bcasl/Base.py` to add `bc_register` decorator and `bc_register` function * Update `bcasl/Loader.py` to support plugins registered with `@bc_register
1 parent 4d0ed7d commit aa5921a

5 files changed

Lines changed: 335 additions & 32 deletions

File tree

Plugins/Cleaner/__init__.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from pathlib import Path
1818
import shutil
1919
from typing import Optional
20+
21+
from bcasl import bc_register
2022
from Plugins_SDK.BcPluginContext import BcPluginBase, PluginMeta, PreCompileContext
2123
from Plugins_SDK.GeneralContext import Dialog
2224

@@ -27,7 +29,8 @@
2729
log = Dialog()
2830
dialog = Dialog()
2931

30-
META = PluginMeta(
32+
# Plugin metadata
33+
PLUGIN_META = PluginMeta(
3134
id="cleaner",
3235
name="Cleaner",
3336
version="1.0.0",
@@ -41,18 +44,19 @@
4144
required_general_context_version="1.0.0",
4245
)
4346

44-
# Plugin no longer uses i18n; static messages are used directly.
45-
4647

48+
@bc_register
4749
class Cleaner(BcPluginBase):
4850
"""Plugin de nettoyage du workspace avant compilation.
4951
5052
Supprime les fichiers .pyc et les dossiers __pycache__ pour réduire la taille
5153
et éviter les problèmes de cache lors de la compilation.
5254
"""
5355

56+
meta = PLUGIN_META
57+
5458
def __init__(self):
55-
super().__init__(META)
59+
super().__init__(meta=PLUGIN_META)
5660
self.cleaned_files = 0
5761
self.cleaned_dirs = 0
5862

@@ -154,12 +158,3 @@ def on_pre_compile(self, ctx: PreCompileContext) -> None:
154158

155159
except Exception as e:
156160
log.log_warn(f"Error during cleaning: {e}")
157-
158-
159-
# Auto-register plugin in BCASL
160-
PLUGIN = Cleaner()
161-
162-
163-
def bcasl_register(manager):
164-
"""Register the Cleaner plugin with the BCASL manager."""
165-
manager.add_plugin(PLUGIN)

bcasl/Base.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"PluginMeta",
2929
"PreCompileContext",
3030
"ExecutionReport",
31+
"bc_register",
3132
]
3233

3334
# Configuration logger par défaut (faible verbosité pour embarqué)
@@ -580,5 +581,241 @@ def __init__(self, plugin: BcPluginBase, insert_idx: int) -> None:
580581

581582

582583
def register_plugin(cls: Any) -> Any:
584+
"""Marque une classe comme plugin BCASL (legacy).
585+
586+
Args:
587+
cls: Classe à marquer
588+
589+
Returns:
590+
La classe inchangée, avec l'attribut __bcasl_plugin__ = True
591+
"""
583592
setattr(cls, "__bcasl_plugin__", True)
584593
return cls
594+
595+
596+
def bc_register(
597+
cls: Optional[type] = None,
598+
*,
599+
manager: Any = None,
600+
auto_instantiate: bool = True,
601+
priority: Optional[int] = None,
602+
) -> Any:
603+
"""Décorateur pour enregistrer un plugin BCASL.
604+
605+
Peut être utilisé de plusieurs façons:
606+
607+
1. En tant que décorateur simple (le plugin sera automatiquement instancié
608+
et enregistré lorsque le module est importé par BCASL):
609+
610+
@bc_register
611+
class MyPlugin(BcPluginBase):
612+
meta = PluginMeta(
613+
id="my_plugin",
614+
name="My Plugin",
615+
version="1.0.0",
616+
)
617+
618+
def on_pre_compile(self, ctx: PreCompileContext) -> None:
619+
...
620+
621+
2. Avec un manager explicite (pour enregistrement immédiat):
622+
623+
@bc_register(manager=my_manager)
624+
class MyPlugin(BcPluginBase):
625+
meta = PluginMeta(
626+
id="my_plugin",
627+
name="My Plugin",
628+
version="1.0.0",
629+
)
630+
...
631+
632+
3. Avec des options de configuration:
633+
634+
@bc_register(priority=10)
635+
class MyPlugin(BcPluginBase):
636+
meta = PluginMeta(
637+
id="my_plugin",
638+
name="My Plugin",
639+
version="1.0.0",
640+
)
641+
...
642+
643+
Args:
644+
cls: Classe de plugin (rempli automatiquement par Python lors de l'utilisation
645+
du décorateur sans parenthèses).
646+
manager: Instance optionnelle de BCASL pour enregistrement immédiat.
647+
auto_instantiate: Si True (défaut), le plugin est instancié automatiquement.
648+
priority: Priorité optionnelle du plugin (écrase celle définie dans le meta).
649+
650+
Returns:
651+
La classe de plugin (avec instance créée si auto_instantiate=True)
652+
653+
Raises:
654+
TypeError: Si la classe décorée n'hérite pas de BcPluginBase
655+
ValueError: Si le plugin n'a pas de métadonnées valides
656+
657+
Example:
658+
# Utilisation simple
659+
from bcasl import BcPluginBase, PluginMeta, bc_register
660+
from bcasl.Base import PreCompileContext
661+
662+
@bc_register
663+
class MyLinter(BcPluginBase):
664+
meta = PluginMeta(
665+
id="my_linter",
666+
name="My Linter",
667+
version="1.0.0",
668+
tags=["lint"],
669+
)
670+
671+
def on_pre_compile(self, ctx: PreCompileContext) -> None:
672+
# Votre logique de linting ici
673+
pass
674+
675+
# Utilisation avec options
676+
@bc_register(priority=5)
677+
class EarlyPlugin(BcPluginBase):
678+
meta = PluginMeta(
679+
id="early_plugin",
680+
name="Early Plugin",
681+
version="1.0.0",
682+
tags=["prepare"],
683+
)
684+
685+
def on_pre_compile(self, ctx: PreCompileContext) -> None:
686+
pass
687+
"""
688+
def decorator_inner(cls_to_decorate: type) -> Any:
689+
# Vérifier que c'est bien une classe
690+
if not isinstance(cls_to_decorate, type):
691+
raise TypeError(
692+
f"bc_register doit être appliqué à une classe, pas à {type(cls_to_decorate)}"
693+
)
694+
695+
# Vérifier l'héritage BcPluginBase (support des deux versions)
696+
is_valid = False
697+
try:
698+
# Vérifier BcPluginBase de bcasl
699+
if issubclass(cls_to_decorate, BcPluginBase):
700+
is_valid = True
701+
except TypeError:
702+
pass
703+
704+
# Vérifier si le module Plugins_SDK est disponible
705+
try:
706+
from Plugins_SDK.BcPluginContext import BcPluginBase as BcPluginBaseSDK
707+
if issubclass(cls_to_decorate, BcPluginBaseSDK):
708+
is_valid = True
709+
except (ImportError, TypeError):
710+
pass
711+
712+
if not is_valid:
713+
raise TypeError(
714+
f"La classe décorée avec @bc_register doit hériter de BcPluginBase. "
715+
f"Classe reçue: {cls_to_decorate.__name__}"
716+
)
717+
718+
# Marquer la classe comme plugin BCASL
719+
setattr(cls_to_decorate, "__bcasl_plugin__", True)
720+
721+
# Récupérer les métadonnées depuis l'attribut de classe
722+
meta = getattr(cls_to_decorate, "meta", None)
723+
if meta is None:
724+
# Essayer d'instancier pour récupérer le meta depuis l'instance
725+
# (utile pour les plugins old-style qui passent META à super().__init__)
726+
try:
727+
if cls_to_decorate.__init__ is not BcPluginBase.__init__:
728+
# Custom __init__ - try to instantiate
729+
temp_instance = cls_to_decorate()
730+
if hasattr(temp_instance, 'meta'):
731+
meta = temp_instance.meta
732+
except Exception:
733+
pass
734+
735+
if meta is None:
736+
raise ValueError(
737+
f"La classe plugin '{cls_to_decorate.__name__}' doit avoir un attribut 'meta' "
738+
f"contenant un PluginMeta valide."
739+
)
740+
741+
# Valider les métadonnées
742+
if not hasattr(meta, "id") or not meta.id:
743+
raise ValueError(
744+
f"Le PluginMeta du plugin '{cls_to_decorate.__name__}' doit avoir un 'id' défini."
745+
)
746+
747+
# Fonction helper pour instancier le plugin
748+
def _create_plugin_instance() -> "BcPluginBase":
749+
"""Crée une instance du plugin en gérant différents styles d'initialisation."""
750+
# Stratégie 1: Si la classe a son propre __init__ (comme les plugins existants),
751+
# essayer d'instancier directement
752+
init_method = getattr(cls_to_decorate, "__init__", None)
753+
# Obtenir le __init__ original de BcPluginBase
754+
base_init = getattr(BcPluginBase, "__init__", None)
755+
756+
# Si la classe n'a pas redéfini __init__ ou si son __init__ est le même que BcPluginBase
757+
if init_method is base_init or cls_to_decorate.__init__ is BcPluginBase.__init__:
758+
# Pas de __init__ personnalisé, utiliser meta de l'attribut de classe
759+
cls_meta = getattr(cls_to_decorate, "meta", None)
760+
if cls_meta is not None:
761+
return cls_to_decorate(meta=cls_meta)
762+
# fallback: passer meta directement
763+
return cls_to_decorate(meta)
764+
else:
765+
# __init__ personnalisé (probablement comme Cleaner), instancier directement
766+
try:
767+
return cls_to_decorate()
768+
except TypeError:
769+
# Si ça échoue, essayer avec meta
770+
cls_meta = getattr(cls_to_decorate, "meta", None)
771+
if cls_meta is not None:
772+
return cls_to_decorate(meta=cls_meta)
773+
return cls_to_decorate(meta)
774+
775+
# Créer l'instance si demandé
776+
plugin_instance = None
777+
if auto_instantiate:
778+
try:
779+
plugin_instance = _create_plugin_instance()
780+
setattr(cls_to_decorate, "_bcasl_instance_", plugin_instance)
781+
except Exception as e:
782+
raise ValueError(
783+
f"Impossible d'instancier le plugin '{cls_to_decorate.__name__}': {e}"
784+
) from e
785+
786+
# Si un manager est fourni, enregistrer immédiatement
787+
if manager is not None:
788+
if plugin_instance is None:
789+
try:
790+
plugin_instance = _create_plugin_instance()
791+
except Exception as e:
792+
raise ValueError(
793+
f"Impossible d'instancier le plugin '{cls_to_decorate.__name__}': {e}"
794+
) from e
795+
796+
# Appliquer la priorité personnalisée si fournie
797+
if priority is not None and plugin_instance is not None:
798+
plugin_instance.priority = priority
799+
800+
# Ajouter au manager
801+
try:
802+
manager.add_plugin(plugin_instance)
803+
_logger.debug("Plugin '%s' enregistré avec @bc_register", meta.id)
804+
except Exception as e:
805+
raise RuntimeError(
806+
f"Échec de l'enregistrement du plugin '{meta.id}': {e}"
807+
) from e
808+
809+
return cls_to_decorate
810+
811+
# Gestion des deux syntaxes du décorateur:
812+
# - @bc_register sans parenthèses: cls est la classe, manager/priority sont None
813+
# - @bc_register() ou @bc_register(manager=x): cls est None, retourne decorator_inner
814+
815+
if cls is not None and isinstance(cls, type):
816+
# @bc_register sans parenthèses - cls est la classe à décorée
817+
return decorator_inner(cls)
818+
819+
# @bc_register() ou @bc_register(manager=x, priority=x) - retourner le décorateur
820+
# Les paramètres manager et priority ont été capturés dans la closure
821+
return decorator_inner

bcasl/Loader.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def _has_bcasl_marker(pkg_dir: Path) -> bool:
5959

6060
def _discover_bcasl_meta(api_dir: Path) -> dict[str, dict[str, Any]]:
6161
"""Découvre les plugins en important chaque package et en appelant bcasl_register(manager).
62+
Supporte également les plugins enregistrés avec le décorateur @bc_register.
6263
Retourne un mapping plugin_id -> meta dict {id, name, version, description, author, requirements}
6364
"""
6465
meta: dict[str, dict[str, Any]] = {}
@@ -82,12 +83,39 @@ def _discover_bcasl_meta(api_dir: Path) -> dict[str, dict[str, Any]]:
8283
module = _ilu.module_from_spec(spec)
8384
_sys.modules[mod_name] = module
8485
spec.loader.exec_module(module) # type: ignore[attr-defined]
85-
reg = getattr(module, "bcasl_register", None)
86-
if not callable(reg):
87-
continue
86+
8887
# Utilise un gestionnaire temporaire pour enregistrer et lire les métadonnées
8988
mgr = BCASL(api_dir, config={}, sandbox=False, plugin_timeout_s=0.0) # type: ignore[call-arg]
90-
reg(mgr)
89+
90+
# Méthode 1: chercher fonction bcasl_register
91+
reg = getattr(module, "bcasl_register", None)
92+
if callable(reg):
93+
reg(mgr)
94+
else:
95+
# Méthode 2: chercher classes marquées avec @bc_register
96+
for attr_name in dir(module):
97+
try:
98+
attr = getattr(module, attr_name, None)
99+
if attr is None:
100+
continue
101+
if not getattr(attr, "__bcasl_plugin__", False):
102+
continue
103+
if not isinstance(attr, type):
104+
continue
105+
# C'est une classe de plugin décorée avec @bc_register
106+
plugin_instance = getattr(attr, "_bcasl_instance_", None)
107+
if plugin_instance is None:
108+
try:
109+
plugin_instance = attr()
110+
except Exception:
111+
continue
112+
# Enregistrer temporairement pour récupérer les métadonnées
113+
pid = plugin_instance.meta.id
114+
if pid not in mgr._registry:
115+
mgr.add_plugin(plugin_instance)
116+
except Exception:
117+
continue
118+
91119
# Récupère les plugins enregistrés
92120
for pid, rec in getattr(mgr, "_registry", {}).items():
93121
try:

bcasl/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
BcPluginBase,
3737
PluginMeta,
3838
PreCompileContext,
39+
bc_register,
3940
register_plugin,
4041
BCASL_PLUGIN_REGISTER_FUNC,
4142
)
@@ -103,6 +104,7 @@ def parse_version(v: str) -> tuple:
103104
"PluginMeta",
104105
"PreCompileContext",
105106
"ExecutionReport",
107+
"bc_register",
106108
"register_plugin",
107109
"BCASL_PLUGIN_REGISTER_FUNC",
108110
# Loader

0 commit comments

Comments
 (0)