ps-plugin-core 0.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ps-plugin-core
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary:
|
|
5
|
+
Requires-Python: >=3.10,<3.14
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ps-plugin-core"
|
|
3
|
+
description = ""
|
|
4
|
+
requires-python = ">=3.10,<3.14"
|
|
5
|
+
version = "0.2.8"
|
|
6
|
+
|
|
7
|
+
[project.entry-points."poetry.application.plugin"]
|
|
8
|
+
entry = "ps.plugin.core._plugin:Plugin"
|
|
9
|
+
|
|
10
|
+
[tool.poetry.group.ps.dependencies]
|
|
11
|
+
ps-di = ">=0.2.8,<0.3.0"
|
|
12
|
+
ps-plugin-sdk = ">=0.2.8,<0.3.0"
|
|
13
|
+
|
|
14
|
+
[tool.poetry]
|
|
15
|
+
packages = [ { include = "ps/plugin/core", from = "src" } ]
|
|
16
|
+
|
|
17
|
+
[tool.ps-plugin]
|
|
18
|
+
host-project = ".."
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=1.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
import traceback
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from importlib import metadata
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from cleo.io.io import IO
|
|
9
|
+
|
|
10
|
+
from ps.di import DI
|
|
11
|
+
from ps.plugin.sdk.logging import log_debug, log_verbose
|
|
12
|
+
from ps.plugin.sdk.settings import PluginSettings
|
|
13
|
+
|
|
14
|
+
_HANDLER_PATTERN = re.compile(r"^poetry_(activate|command|error|terminate|signal)(_\w+)?$")
|
|
15
|
+
_EVENT_TYPES = ("activate", "command", "error", "terminate", "signal")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class _ModuleInfo:
|
|
20
|
+
name: str
|
|
21
|
+
handlers: dict[str, Callable] = field(default_factory=dict)
|
|
22
|
+
distribution: Optional[str] = None
|
|
23
|
+
instance: Optional[object] = None
|
|
24
|
+
path: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_module_path(obj: Any) -> Optional[str]:
|
|
28
|
+
try:
|
|
29
|
+
return inspect.getfile(obj)
|
|
30
|
+
except (TypeError, OSError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_distribution(entry_point: metadata.EntryPoint) -> Optional[str]:
|
|
35
|
+
try:
|
|
36
|
+
return entry_point.dist.name if entry_point.dist else None
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_class_name(cls: type) -> str:
|
|
42
|
+
name_attr = getattr(cls, "name", None)
|
|
43
|
+
return name_attr if isinstance(name_attr, str) else cls.__name__
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_static_or_classmethod(cls: type, name: str) -> bool:
|
|
47
|
+
for klass in cls.__mro__:
|
|
48
|
+
if name in klass.__dict__:
|
|
49
|
+
val = klass.__dict__[name]
|
|
50
|
+
return isinstance(val, (staticmethod, classmethod))
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _scan_class(cls: type, distribution: Optional[str]) -> list[_ModuleInfo]:
|
|
55
|
+
instance_handlers: dict[str, Callable] = {}
|
|
56
|
+
static_handlers: dict[str, tuple[str, Callable]] = {}
|
|
57
|
+
|
|
58
|
+
for name in dir(cls):
|
|
59
|
+
match = _HANDLER_PATTERN.match(name)
|
|
60
|
+
if not match:
|
|
61
|
+
continue
|
|
62
|
+
event_type = match.group(1)
|
|
63
|
+
suffix = (match.group(2) or "")[1:] # strip leading _
|
|
64
|
+
|
|
65
|
+
if _is_static_or_classmethod(cls, name):
|
|
66
|
+
if not suffix:
|
|
67
|
+
continue # static/class methods MUST have suffix
|
|
68
|
+
fn = getattr(cls, name)
|
|
69
|
+
static_handlers[name] = (suffix, fn)
|
|
70
|
+
else:
|
|
71
|
+
instance_handlers[event_type] = getattr(cls, name)
|
|
72
|
+
|
|
73
|
+
modules: list[_ModuleInfo] = []
|
|
74
|
+
|
|
75
|
+
cls_path = _get_module_path(cls)
|
|
76
|
+
|
|
77
|
+
if instance_handlers:
|
|
78
|
+
modules.append(_ModuleInfo(
|
|
79
|
+
name=_get_class_name(cls),
|
|
80
|
+
handlers=instance_handlers,
|
|
81
|
+
distribution=distribution,
|
|
82
|
+
path=cls_path,
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
# Group static methods by suffix
|
|
86
|
+
suffix_groups: dict[str, dict[str, Callable]] = {}
|
|
87
|
+
for name, (suffix, fn) in static_handlers.items():
|
|
88
|
+
match = _HANDLER_PATTERN.match(name)
|
|
89
|
+
if match:
|
|
90
|
+
event_type = match.group(1)
|
|
91
|
+
suffix_groups.setdefault(suffix, {})[event_type] = fn
|
|
92
|
+
|
|
93
|
+
for suffix, handlers in suffix_groups.items():
|
|
94
|
+
modules.append(_ModuleInfo(
|
|
95
|
+
name=suffix,
|
|
96
|
+
handlers=handlers,
|
|
97
|
+
distribution=distribution,
|
|
98
|
+
path=cls_path,
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
return modules
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _scan_function(fn: Callable, distribution: Optional[str]) -> Optional[_ModuleInfo]:
|
|
105
|
+
name = getattr(fn, "__name__", "")
|
|
106
|
+
match = _HANDLER_PATTERN.match(name)
|
|
107
|
+
if not match:
|
|
108
|
+
return None
|
|
109
|
+
event_type = match.group(1)
|
|
110
|
+
suffix = (match.group(2) or "")[1:]
|
|
111
|
+
if not suffix:
|
|
112
|
+
return None # global functions MUST have suffix
|
|
113
|
+
return _ModuleInfo(name=suffix, handlers={event_type: fn}, distribution=distribution, path=_get_module_path(fn))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _load_module_infos(io: IO) -> list[_ModuleInfo]:
|
|
117
|
+
all_modules: list[_ModuleInfo] = []
|
|
118
|
+
|
|
119
|
+
for entry_point in metadata.entry_points(group="ps.module"):
|
|
120
|
+
ep_name = f"{entry_point.group}:{entry_point.name}"
|
|
121
|
+
try:
|
|
122
|
+
loaded = entry_point.load()
|
|
123
|
+
dist = _get_distribution(entry_point)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
log_verbose(io, f" <fg=yellow>Warning: failed to load entry point '{ep_name}': {e}</>")
|
|
126
|
+
log_debug(io, f" <fg=dark_gray>{traceback.format_exc().strip()}</>")
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if inspect.isclass(loaded):
|
|
130
|
+
all_modules.extend(_scan_class(loaded, dist))
|
|
131
|
+
elif inspect.ismodule(loaded):
|
|
132
|
+
for _, obj in inspect.getmembers(loaded):
|
|
133
|
+
if inspect.isclass(obj) and obj.__module__.startswith(loaded.__name__):
|
|
134
|
+
all_modules.extend(_scan_class(obj, dist))
|
|
135
|
+
elif inspect.isfunction(obj) and obj.__module__ == loaded.__name__:
|
|
136
|
+
info = _scan_function(obj, dist)
|
|
137
|
+
if info:
|
|
138
|
+
all_modules.append(info)
|
|
139
|
+
elif inspect.isfunction(loaded):
|
|
140
|
+
info = _scan_function(loaded, dist)
|
|
141
|
+
if info:
|
|
142
|
+
all_modules.append(info)
|
|
143
|
+
else:
|
|
144
|
+
log_verbose(io, f" <fg=yellow>Warning: entry point '{ep_name}' loaded unsupported type {type(loaded).__name__}, skipping.</>")
|
|
145
|
+
|
|
146
|
+
return all_modules
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _detect_collisions(modules: list[_ModuleInfo], io: IO) -> list[_ModuleInfo]:
|
|
150
|
+
name_groups: dict[str, list[_ModuleInfo]] = {}
|
|
151
|
+
for mod in modules:
|
|
152
|
+
name_groups.setdefault(mod.name, []).append(mod)
|
|
153
|
+
|
|
154
|
+
result: list[_ModuleInfo] = []
|
|
155
|
+
for name, group in name_groups.items():
|
|
156
|
+
if len(group) == 1:
|
|
157
|
+
result.append(group[0])
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
paths = {m.path for m in group}
|
|
161
|
+
if len(paths) == 1 and None not in paths:
|
|
162
|
+
log_debug(io, f"<fg=dark_gray>Module '<fg=cyan>{name}</>' discovered via multiple entry points, using single instance</>")
|
|
163
|
+
result.append(group[0])
|
|
164
|
+
else:
|
|
165
|
+
dist_list = ", ".join(
|
|
166
|
+
f"<fg=yellow>{m.distribution or 'unknown'}</>" for m in group
|
|
167
|
+
)
|
|
168
|
+
log_verbose(
|
|
169
|
+
io,
|
|
170
|
+
f" <fg=yellow>Warning: module name collision: '<fg=cyan>{name}</>' found in [{dist_list}]. None will be loaded.</>",
|
|
171
|
+
)
|
|
172
|
+
if io.is_debug():
|
|
173
|
+
for m in group:
|
|
174
|
+
path_hint = f" — {m.path}" if m.path else ""
|
|
175
|
+
io.write_line(f" <fg=dark_gray> {m.distribution or 'unknown'}{path_hint}</>")
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class _ModulesHandler:
|
|
180
|
+
def __init__(self, di: DI, io: IO, plugin_settings: PluginSettings) -> None:
|
|
181
|
+
self._di = di
|
|
182
|
+
self._io = io
|
|
183
|
+
self._plugin_settings = plugin_settings
|
|
184
|
+
self._modules: list[_ModuleInfo] = []
|
|
185
|
+
self._disabled: set[str] = set()
|
|
186
|
+
|
|
187
|
+
def discover_and_instantiate(self) -> None:
|
|
188
|
+
io = self._io
|
|
189
|
+
all_modules = _load_module_infos(io)
|
|
190
|
+
modules = _detect_collisions(all_modules, io)
|
|
191
|
+
|
|
192
|
+
specified = self._plugin_settings.modules
|
|
193
|
+
if specified is not None:
|
|
194
|
+
name_map = {m.name: m for m in modules}
|
|
195
|
+
modules = [name_map[n] for n in specified if n in name_map]
|
|
196
|
+
selected_names = {m.name for m in modules}
|
|
197
|
+
else:
|
|
198
|
+
modules = []
|
|
199
|
+
selected_names = set()
|
|
200
|
+
|
|
201
|
+
if io.is_verbose():
|
|
202
|
+
available_not_selected = [m for m in all_modules if m.name not in selected_names]
|
|
203
|
+
|
|
204
|
+
io.write_line("<fg=magenta>Selected modules:</>")
|
|
205
|
+
for idx, mod in enumerate(modules, start=1):
|
|
206
|
+
dist_hint = f" <fg=dark_gray>[{mod.distribution}]</>" if mod.distribution else ""
|
|
207
|
+
io.write_line(f" {idx}. <fg=cyan>{mod.name}</>{dist_hint}")
|
|
208
|
+
if io.is_debug() and mod.path:
|
|
209
|
+
io.write_line(f" <fg=dark_gray>{mod.path}</>")
|
|
210
|
+
|
|
211
|
+
if available_not_selected:
|
|
212
|
+
io.write_line("<fg=magenta>Discovered but not selected:</>")
|
|
213
|
+
for mod in available_not_selected:
|
|
214
|
+
dist_hint = f" <fg=dark_gray>[{mod.distribution}]</>" if mod.distribution else ""
|
|
215
|
+
io.write_line(f" - <fg=dark_gray>{mod.name}</>{dist_hint}")
|
|
216
|
+
if io.is_debug() and mod.path:
|
|
217
|
+
io.write_line(f" <fg=dark_gray>{mod.path}</>")
|
|
218
|
+
|
|
219
|
+
# Instantiate class-based modules
|
|
220
|
+
for mod in modules:
|
|
221
|
+
handlers = mod.handlers
|
|
222
|
+
# Check if any handler is an unbound method (needs instance)
|
|
223
|
+
needs_instance = any(
|
|
224
|
+
_is_unbound_method(fn) for fn in handlers.values()
|
|
225
|
+
)
|
|
226
|
+
if needs_instance:
|
|
227
|
+
# Find the class from first unbound method
|
|
228
|
+
cls = _get_defining_class(next(iter(handlers.values())))
|
|
229
|
+
if cls:
|
|
230
|
+
instance = self._di.spawn(cls)
|
|
231
|
+
mod.instance = instance
|
|
232
|
+
# Bind methods to instance
|
|
233
|
+
mod.handlers = {
|
|
234
|
+
event_type: getattr(instance, _event_to_method_name(event_type, mod.name, cls))
|
|
235
|
+
for event_type, fn in handlers.items()
|
|
236
|
+
}
|
|
237
|
+
log_debug(io, f"<fg=dark_gray>Instantiated module <comment>{mod.name}</comment> ({cls.__module__}.{cls.__name__})</>")
|
|
238
|
+
|
|
239
|
+
event_types = ", ".join(sorted(mod.handlers.keys()))
|
|
240
|
+
log_debug(io, f"<fg=dark_gray>Module <comment>{mod.name}</comment> handles: {event_types}</>")
|
|
241
|
+
|
|
242
|
+
self._modules = modules
|
|
243
|
+
|
|
244
|
+
def activate(self) -> None:
|
|
245
|
+
io = self._io
|
|
246
|
+
activate_modules = [m for m in self._modules if "activate" in m.handlers]
|
|
247
|
+
log_verbose(io, f"<info>Activating {len(activate_modules)} module(s)</info>")
|
|
248
|
+
|
|
249
|
+
for mod in activate_modules:
|
|
250
|
+
fn = mod.handlers["activate"]
|
|
251
|
+
log_debug(io, f"<fg=dark_gray>Executing activate for module <comment>{mod.name}</comment></>")
|
|
252
|
+
try:
|
|
253
|
+
result = self._di.satisfy(fn)()
|
|
254
|
+
if result is False:
|
|
255
|
+
self._disabled.add(mod.name)
|
|
256
|
+
log_debug(io, f"<fg=dark_gray>Module <comment>{mod.name}</comment> disabled itself during activation</>")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
io.write_error_line(f"<error>Error during activation of module {mod.name}: {e}</error>")
|
|
259
|
+
raise
|
|
260
|
+
|
|
261
|
+
def get_event_handlers(self, event_type: str) -> list[Callable[..., Any]]:
|
|
262
|
+
return [
|
|
263
|
+
mod.handlers[event_type]
|
|
264
|
+
for mod in self._modules
|
|
265
|
+
if event_type in mod.handlers and mod.name not in self._disabled
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
def get_module_names(self) -> list[str]:
|
|
269
|
+
return [m.name for m in self._modules if m.name not in self._disabled]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _is_unbound_method(fn: Callable) -> bool:
|
|
273
|
+
return inspect.isfunction(fn) and "." in getattr(fn, "__qualname__", "")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _get_defining_class(fn: Callable) -> Optional[type]:
|
|
277
|
+
qualname = getattr(fn, "__qualname__", "")
|
|
278
|
+
parts = qualname.rsplit(".", 1)
|
|
279
|
+
if len(parts) < 2:
|
|
280
|
+
return None
|
|
281
|
+
module = inspect.getmodule(fn)
|
|
282
|
+
if module is None:
|
|
283
|
+
return None
|
|
284
|
+
return getattr(module, parts[0], None)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _event_to_method_name(event_type: str, module_name: str, cls: type) -> str:
|
|
288
|
+
# Try without suffix first (instance methods can omit suffix)
|
|
289
|
+
base = f"poetry_{event_type}"
|
|
290
|
+
if hasattr(cls, base):
|
|
291
|
+
return base
|
|
292
|
+
# Try with module name as suffix
|
|
293
|
+
return f"poetry_{event_type}_{module_name}"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import site
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import cleo.events.console_events
|
|
6
|
+
from cleo.events.console_command_event import ConsoleCommandEvent
|
|
7
|
+
from cleo.events.event import Event
|
|
8
|
+
from cleo.events.event_dispatcher import EventDispatcher
|
|
9
|
+
from cleo.io.inputs.argv_input import ArgvInput
|
|
10
|
+
from cleo.io.io import IO
|
|
11
|
+
from cleo.io.outputs.output import Verbosity
|
|
12
|
+
from cleo.io.outputs.stream_output import StreamOutput
|
|
13
|
+
from poetry.console.application import Application
|
|
14
|
+
from poetry.plugins.application_plugin import ApplicationPlugin
|
|
15
|
+
from poetry.utils.env import EnvManager
|
|
16
|
+
|
|
17
|
+
from ps.di import DI
|
|
18
|
+
from ps.plugin.sdk.logging import log_debug, log_verbose
|
|
19
|
+
from ps.plugin.sdk.project import Environment
|
|
20
|
+
from ps.plugin.sdk.settings import PluginSettings, parse_plugin_settings_from_document
|
|
21
|
+
|
|
22
|
+
from ._modules_handler import _ModulesHandler
|
|
23
|
+
|
|
24
|
+
_EVENT_LISTENERS = {
|
|
25
|
+
"command": cleo.events.console_events.COMMAND,
|
|
26
|
+
"terminate": cleo.events.console_events.TERMINATE,
|
|
27
|
+
"error": cleo.events.console_events.ERROR,
|
|
28
|
+
"signal": cleo.events.console_events.SIGNAL,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _create_standard_io() -> IO:
|
|
33
|
+
output = StreamOutput(sys.stdout, verbosity=Verbosity.DEBUG) # type: ignore
|
|
34
|
+
error_output = StreamOutput(sys.stderr, verbosity=Verbosity.DEBUG) # type: ignore
|
|
35
|
+
return IO(ArgvInput(), output, error_output)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _activate_project_venv(application: Application, io: IO) -> None:
|
|
39
|
+
env_prefix = os.environ.get("VIRTUAL_ENV", os.environ.get("CONDA_PREFIX"))
|
|
40
|
+
conda_env_name = os.environ.get("CONDA_DEFAULT_ENV")
|
|
41
|
+
if env_prefix is not None and conda_env_name != "base":
|
|
42
|
+
log_debug(io, f"Already in virtual environment <comment>{env_prefix}</comment>, skipping venv activation")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
env = EnvManager(application.poetry).get()
|
|
47
|
+
venv_path = env.path
|
|
48
|
+
except Exception as e:
|
|
49
|
+
log_debug(io, f"Could not resolve project environment: <comment>{e}</comment>, skipping venv activation")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if not venv_path.exists():
|
|
53
|
+
log_debug(io, "Project environment does not exist, skipping venv activation")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if sys.platform == "win32":
|
|
57
|
+
site_pkgs = venv_path / "Lib" / "site-packages"
|
|
58
|
+
else:
|
|
59
|
+
py_ver = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
60
|
+
site_pkgs = venv_path / "lib" / py_ver / "site-packages"
|
|
61
|
+
|
|
62
|
+
if not site_pkgs.exists():
|
|
63
|
+
log_debug(io, f"Site-packages not found at <comment>{site_pkgs}</comment>, skipping venv activation")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
site.addsitedir(str(site_pkgs))
|
|
67
|
+
|
|
68
|
+
bin_dir = venv_path / ("Scripts" if sys.platform == "win32" else "bin")
|
|
69
|
+
if bin_dir.exists():
|
|
70
|
+
os.environ["PATH"] = str(bin_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
71
|
+
|
|
72
|
+
log_verbose(io, f"Activated project venv from <comment>{venv_path}</comment>")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_settings(environment: Environment) -> PluginSettings:
|
|
76
|
+
return environment.host_project.plugin_settings
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Plugin(ApplicationPlugin):
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
super().__init__()
|
|
82
|
+
self._di = DI()
|
|
83
|
+
|
|
84
|
+
def activate(self, application: Application) -> None:
|
|
85
|
+
assert application.event_dispatcher is not None
|
|
86
|
+
event_dispatcher = application.event_dispatcher
|
|
87
|
+
|
|
88
|
+
io = self._ensure_io(application)
|
|
89
|
+
project_toml = application.poetry.pyproject.data
|
|
90
|
+
try:
|
|
91
|
+
settings: PluginSettings = parse_plugin_settings_from_document(project_toml)
|
|
92
|
+
if not settings.enabled:
|
|
93
|
+
log_verbose(io, f"<fg=yellow>ps-plugin not enabled or disabled in configuration in {application.poetry.pyproject.file}</>")
|
|
94
|
+
return
|
|
95
|
+
except Exception as e:
|
|
96
|
+
log_debug(io, f"<error>Not in a valid poetry project or configuration error: {e}</error>")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
log_verbose(io, "<info>Starting activation</info>")
|
|
100
|
+
|
|
101
|
+
_activate_project_venv(application, io)
|
|
102
|
+
|
|
103
|
+
di = self._di
|
|
104
|
+
di.register(IO).factory(lambda: io)
|
|
105
|
+
di.register(Application).factory(lambda: application)
|
|
106
|
+
di.register(Environment).factory(lambda path: Environment(path), application.poetry.pyproject_path)
|
|
107
|
+
di.register(PluginSettings).factory(_resolve_settings)
|
|
108
|
+
di.register(EventDispatcher).factory(lambda: event_dispatcher)
|
|
109
|
+
|
|
110
|
+
handler = di.spawn(_ModulesHandler)
|
|
111
|
+
handler.discover_and_instantiate()
|
|
112
|
+
handler.activate()
|
|
113
|
+
|
|
114
|
+
for event_type, event_constant in _EVENT_LISTENERS.items():
|
|
115
|
+
fns = handler.get_event_handlers(event_type)
|
|
116
|
+
if not fns:
|
|
117
|
+
log_debug(io, f"No handlers for <comment>{event_type}</comment>; skipping listener")
|
|
118
|
+
continue
|
|
119
|
+
log_verbose(io, f"Registering <fg=yellow>{len(fns)}</> handler(s) for <comment>{event_type}</comment>")
|
|
120
|
+
self._register_listener(event_dispatcher, di, io, event_type, event_constant, fns)
|
|
121
|
+
|
|
122
|
+
self.poetry = application.poetry
|
|
123
|
+
log_verbose(io, "<info>Activation complete</info>")
|
|
124
|
+
|
|
125
|
+
def _register_listener(
|
|
126
|
+
self,
|
|
127
|
+
event_dispatcher: EventDispatcher,
|
|
128
|
+
di: DI,
|
|
129
|
+
io: IO,
|
|
130
|
+
event_type: str,
|
|
131
|
+
event_constant: str,
|
|
132
|
+
fns: list,
|
|
133
|
+
) -> None:
|
|
134
|
+
def _listener(event: Event, event_name: str, dispatcher: EventDispatcher) -> None: # noqa: ARG001
|
|
135
|
+
with di.scope() as scope:
|
|
136
|
+
scope.register(type(event)).factory(lambda: event)
|
|
137
|
+
log_debug(io, f"Processing <comment>{event_name}</comment> event")
|
|
138
|
+
for fn in fns:
|
|
139
|
+
scope.satisfy(fn)()
|
|
140
|
+
if isinstance(event, ConsoleCommandEvent) and not event.command_should_run():
|
|
141
|
+
log_debug(io, f"Command execution stopped after <comment>{event_type}</comment> handler")
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
event_dispatcher.add_listener(event_constant, _listener)
|
|
145
|
+
|
|
146
|
+
def _ensure_io(self, application: Application) -> IO:
|
|
147
|
+
io = getattr(application, "_io", None)
|
|
148
|
+
if io is None:
|
|
149
|
+
io = _create_standard_io()
|
|
150
|
+
application._io = io
|
|
151
|
+
return io
|