Skip to content

Commit 45cdc51

Browse files
feat(core): allow to dynamically inject external (private or public) plugins extending Python SDK dynamically
1 parent 878cbb0 commit 45cdc51

3 files changed

Lines changed: 373 additions & 19 deletions

File tree

src/aignostics/utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
__version__,
2323
__version_full__,
2424
)
25-
from ._di import load_modules, locate_implementations, locate_subclasses
25+
from ._di import discover_plugin_packages, load_modules, locate_implementations, locate_subclasses
2626
from ._fs import get_user_data_directory, open_user_data_directory, sanitize_path, sanitize_path_component
2727
from ._health import Health
2828
from ._log import LogSettings
@@ -60,6 +60,7 @@
6060
"__version_full__",
6161
"boot",
6262
"console",
63+
"discover_plugin_packages",
6364
"get_process_info",
6465
"get_user_data_directory",
6566
"load_modules",

src/aignostics/utils/_di.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import importlib
44
import pkgutil
5+
from functools import lru_cache
6+
from importlib.metadata import entry_points
57
from inspect import isclass
68
from typing import Any
79

@@ -10,6 +12,28 @@
1012
_implementation_cache: dict[Any, list[Any]] = {}
1113
_subclass_cache: dict[Any, list[Any]] = {}
1214

15+
# Entry point group name for aignostics plugins
16+
PLUGIN_ENTRY_POINT_GROUP = "aignostics.plugins"
17+
18+
19+
@lru_cache(maxsize=1)
20+
def discover_plugin_packages() -> tuple[str, ...]:
21+
"""
22+
Discover plugin packages using entry points.
23+
24+
Plugins register themselves in their pyproject.toml:
25+
26+
[project.entry-points."aignostics.plugins"]
27+
my_plugin = "my_plugin"
28+
29+
Results are cached after the first call.
30+
31+
Returns:
32+
tuple[str, ...]: Tuple of discovered plugin package names.
33+
"""
34+
eps = entry_points(group=PLUGIN_ENTRY_POINT_GROUP)
35+
return tuple(ep.value for ep in eps)
36+
1337

1438
def load_modules() -> None:
1539
package = importlib.import_module(__project_name__)
@@ -21,6 +45,8 @@ def locate_implementations(_class: type[Any]) -> list[Any]:
2145
"""
2246
Dynamically discover all instances of some class.
2347
48+
Searches in the main project and all plugins registered via entry points.
49+
2450
Args:
2551
_class (type[Any]): Class to search for.
2652
@@ -30,16 +56,19 @@ def locate_implementations(_class: type[Any]) -> list[Any]:
3056
if _class in _implementation_cache:
3157
return _implementation_cache[_class]
3258

59+
plugin_packages = discover_plugin_packages()
60+
3361
implementations = []
34-
package = importlib.import_module(__project_name__)
62+
for package_name in [*plugin_packages, __project_name__]:
63+
package = importlib.import_module(package_name)
3564

36-
for _, name, _ in pkgutil.iter_modules(package.__path__):
37-
module = importlib.import_module(f"{__project_name__}.{name}")
38-
# Check all members of the module
39-
for member_name in dir(module):
40-
member = getattr(module, member_name)
41-
if isinstance(member, _class):
42-
implementations.append(member)
65+
for _, name, _ in pkgutil.iter_modules(package.__path__):
66+
module = importlib.import_module(f"{package_name}.{name}")
67+
# Check all members of the module
68+
for member_name in dir(module):
69+
member = getattr(module, member_name)
70+
if isinstance(member, _class):
71+
implementations.append(member)
4372

4473
_implementation_cache[_class] = implementations
4574
return implementations
@@ -49,6 +78,8 @@ def locate_subclasses(_class: type[Any]) -> list[Any]:
4978
"""
5079
Dynamically discover all classes that are subclasses of some type.
5180
81+
Searches in the main project and all plugins registered via entry points.
82+
5283
Args:
5384
_class (type[Any]): Parent class of subclasses to search for.
5485
@@ -58,19 +89,25 @@ def locate_subclasses(_class: type[Any]) -> list[Any]:
5889
if _class in _subclass_cache:
5990
return _subclass_cache[_class]
6091

61-
subclasses = []
62-
package = importlib.import_module(__project_name__)
92+
plugin_packages = discover_plugin_packages()
6393

64-
for _, name, _ in pkgutil.iter_modules(package.__path__):
94+
subclasses = []
95+
for package_name in [*plugin_packages, __project_name__]:
6596
try:
66-
module = importlib.import_module(f"{__project_name__}.{name}")
67-
# Check all members of the module
68-
for member_name in dir(module):
69-
member = getattr(module, member_name)
70-
if isclass(member) and issubclass(member, _class) and member != _class:
71-
subclasses.append(member)
97+
package = importlib.import_module(package_name)
7298
except ImportError:
7399
continue
74100

101+
for _, name, _ in pkgutil.iter_modules(package.__path__):
102+
try:
103+
module = importlib.import_module(f"{package_name}.{name}")
104+
# Check all members of the module
105+
for member_name in dir(module):
106+
member = getattr(module, member_name)
107+
if isclass(member) and issubclass(member, _class) and member != _class:
108+
subclasses.append(member)
109+
except ImportError:
110+
continue
111+
75112
_subclass_cache[_class] = subclasses
76113
return subclasses

0 commit comments

Comments
 (0)