Skip to content

Commit 075d42d

Browse files
committed
Add possibility to define custom impls for services in config. Move last hardcoded services definitions from DI to config. Fix parsing of lists in CLI params.
1 parent da95aa7 commit 075d42d

13 files changed

Lines changed: 192 additions & 33 deletions

File tree

finecode_dev_common_preset/src/finecode_dev_common_preset/preset.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ fine_python_setuptools_scm = { path = "../../../extensions/fine_python_setuptool
3535
requires = ["setuptools>=64", "setuptools-scm>=8"]
3636
build-backend = "setuptools.build_meta"
3737

38+
[[tool.finecode.service]]
39+
interface = "finecode_extension_api.interfaces.ihttpclient.IHttpClient"
40+
source = "finecode_httpclient.HttpClient"
41+
env = "dev_no_runtime"
42+
dependencies = ["finecode_httpclient~=0.1.0a1"]
43+
44+
[[tool.finecode.service]]
45+
interface = "finecode_extension_api.interfaces.ijsonrpcclient.IJsonRpcClient"
46+
source = "finecode_jsonrpc.jsonrpc_client.JsonRpcClientImpl"
47+
env = "dev_no_runtime"
48+
dependencies = ["finecode_jsonrpc~=0.1.0a1"]
49+
50+
[[tool.finecode.service]]
51+
interface = "finecode_extension_api.interfaces.ilspclient.ILspClient"
52+
source = "finecode_extension_runner.impls.lsp_client.LspClientImpl"
53+
env = "dev_no_runtime"
54+
dependencies = []
55+
3856
# TODO: recognize minimal python version automatically
3957
[[tool.finecode.action_handler]]
4058
source = "fine_python_ruff.RuffLintFilesHandler"

finecode_extension_runner/src/finecode_extension_runner/di/bootstrap.py

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@
77

88
import ordered_set
99

10-
# TODO: get rid of these two tries
11-
try:
12-
import finecode_httpclient
13-
except ImportError:
14-
finecode_httpclient = None
15-
16-
try:
17-
from finecode_jsonrpc import jsonrpc_client
18-
except ImportError:
19-
jsonrpc_client = None
20-
2110
from loguru import logger
2211

2312
from finecode_extension_api.interfaces import ( # idevenvinfoprovider,
@@ -27,17 +16,15 @@
2716
iextensionrunnerinfoprovider,
2817
ifileeditor,
2918
ifilemanager,
30-
ihttpclient,
31-
ijsonrpcclient,
3219
ilogger,
33-
ilspclient,
3420
iprojectinfoprovider,
3521
irepositorycredentialsprovider,
3622
)
3723

3824
from finecode_extension_runner import domain
3925
from finecode_extension_runner._services import run_action
4026
from finecode_extension_runner.di import _state, resolver
27+
from finecode_extension_runner.run_utils import import_module_member_by_source_str
4128
from finecode_extension_runner.impls import ( # dev_env_info_provider,
4229
action_runner,
4330
command_runner,
@@ -46,13 +33,11 @@
4633
file_manager,
4734
inmemory_cache,
4835
loguru_logger,
49-
lsp_client,
5036
project_info_provider,
5137
repository_credentials_provider,
5238
service_registry,
5339
)
5440

55-
5641
def bootstrap(
5742
project_def_path_getter: Callable[[], pathlib.Path],
5843
project_raw_config_getter: Callable[
@@ -64,6 +49,7 @@ def bootstrap(
6449
action_by_name_getter: Callable[[str], domain.ActionDeclaration],
6550
current_env_name_getter: Callable[[], str],
6651
handler_packages: set[str],
52+
service_declarations: list,
6753
):
6854
# logger_instance = loguru_logger.LoguruLogger()
6955
logger_instance = loguru_logger.get_logger()
@@ -91,22 +77,10 @@ def bootstrap(
9177
_state.container[icache.ICache] = cache_instance
9278
_state.container[iactionrunner.IActionRunner] = action_runner_instance
9379

94-
if finecode_httpclient is not None:
95-
_state.container[ihttpclient.IHttpClient] = finecode_httpclient.HttpClient(
96-
logger=logger_instance
97-
)
98-
9980
_state.container[irepositorycredentialsprovider.IRepositoryCredentialsProvider] = (
10081
repository_credentials_provider.ConfigRepositoryCredentialsProvider()
10182
)
10283

103-
if jsonrpc_client is not None:
104-
json_rpc_client_instance = jsonrpc_client.JsonRpcClientImpl()
105-
_state.container[ijsonrpcclient.IJsonRpcClient] = json_rpc_client_instance
106-
_state.container[ilspclient.ILspClient] = lsp_client.LspClientImpl(
107-
json_rpc_client=json_rpc_client_instance,
108-
)
109-
11084
# _state.container[idevenvinfoprovider.IDevEnvInfoProvider] = dev_env_info_provider_instance
11185

11286
_state.factories[iprojectinfoprovider.IProjectInfoProvider] = functools.partial(
@@ -124,6 +98,7 @@ def bootstrap(
12498
)
12599

126100
_activate_extensions(handler_packages)
101+
_apply_user_service_config(service_declarations)
127102

128103

129104
def _activate_extensions(handler_packages: set[str]) -> None:
@@ -143,6 +118,18 @@ def _activate_extensions(handler_packages: set[str]) -> None:
143118
logger.error(f"Failed to activate extension '{pkg_name}': {e}")
144119

145120

121+
def _apply_user_service_config(service_declarations: list[object]) -> None:
122+
registry = service_registry.ServiceRegistry()
123+
for svc in service_declarations:
124+
try:
125+
interface = import_module_member_by_source_str(svc.interface)
126+
impl_cls = import_module_member_by_source_str(svc.source)
127+
registry.register_impl(interface, impl_cls)
128+
logger.trace(f"Configured service '{svc.source}' for '{svc.interface}'")
129+
except Exception as e:
130+
logger.error(f"Failed to configure service '{svc.source}': {e}")
131+
132+
146133
def _collect_activatable_packages(
147134
seed_packages: set[str],
148135
all_eps: dict[str, importlib.metadata.EntryPoint],

finecode_extension_runner/src/finecode_extension_runner/lsp_server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ async def update_config(
397397
for action in actions
398398
},
399399
action_handler_configs=action_handler_configs,
400+
services=[
401+
schemas.ServiceDeclaration(
402+
interface=svc["interface"],
403+
source=svc["source"],
404+
)
405+
for svc in config.get("services", [])
406+
],
400407
)
401408
response = await services.update_config(
402409
request=request,

finecode_extension_runner/src/finecode_extension_runner/schemas.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@ class Action(BaseSchema):
2626
config: dict[str, Any] | None = None
2727

2828

29+
@dataclass
30+
class ServiceDeclaration(BaseSchema):
31+
interface: str
32+
source: str
33+
34+
2935
@dataclass
3036
class UpdateConfigRequest(BaseSchema):
3137
working_dir: Path
3238
project_name: str
3339
project_def_path: Path
3440
actions: dict[str, Action]
3541
action_handler_configs: dict[str, dict[str, Any]]
42+
services: list[ServiceDeclaration] = field(default_factory=list)
3643

3744

3845
@dataclass

finecode_extension_runner/src/finecode_extension_runner/services.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ def current_env_name_getter() -> str:
106106
handler.source.split(".")[0]
107107
for action in actions.values()
108108
for handler in action.handlers
109+
} | {
110+
svc.source.split(".")[0] for svc in request.services
109111
}
110112

111113
di_bootstrap.bootstrap(
@@ -117,6 +119,7 @@ def current_env_name_getter() -> str:
117119
action_by_name_getter=action_by_name_getter,
118120
current_env_name_getter=current_env_name_getter,
119121
handler_packages=handler_packages,
122+
service_declarations=request.services,
120123
)
121124

122125
return schemas.UpdateConfigResponse()

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ dev_workspace = [
3232
"debugpy==1.8.*",
3333
]
3434
dev = [{ include-group = "runtime" }, "pytest==7.4.*", "debugpy==1.8.*"]
35-
docs = ["mkdocs==1.6.*", "mkdocs-material==9.7.*", "mkdocstrings[python]==1.0.*"]
35+
docs = [
36+
"mkdocs==1.6.*",
37+
"mkdocs-material==9.7.*",
38+
"mkdocstrings[python]==1.0.*",
39+
]
3640

3741
[build-system]
3842
requires = ["setuptools>=64", "setuptools-scm>=8"]

src/finecode/cli.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ def parse_handler_config_from_cli(
107107

108108
# Remove --config. prefix and split by =
109109
config_part = arg[len("--config.") :]
110-
key_part, value = config_part.split("=", 1)
111-
value = value.strip('"').strip("'")
110+
key_part, raw_value = config_part.split("=", 1)
111+
try:
112+
value = json.loads(raw_value)
113+
except json.JSONDecodeError:
114+
# fallback for literal string, all other types can be parsed by json.loads
115+
value = raw_value
112116

113117
# Split by . to determine if it's action-level or handler-specific
114118
parts = key_part.split(".")
@@ -228,7 +232,7 @@ async def show_user_message(message: str, message_type: str) -> None:
228232
def deserialize_action_payload(raw_payload: dict[str, str]) -> dict[str, typing.Any]:
229233
deserialized_payload = {}
230234
for key, value in raw_payload.items():
231-
if value.startswith("{") and value.endswith("}"):
235+
if (value.startswith("{") and value.endswith("}")) or (value.startswith('[') and value.endswith(']')):
232236
try:
233237
deserialized_value = json.loads(value)
234238
except json.JSONDecodeError:

src/finecode/config/collect_actions.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,49 @@ def collect_actions(
6262
return actions
6363

6464

65+
def collect_services(
66+
project_path: Path,
67+
ws_context: context.WorkspaceContext,
68+
) -> list[domain.ServiceDeclaration]:
69+
try:
70+
project = ws_context.ws_projects[project_path]
71+
except KeyError as exception:
72+
raise ValueError(
73+
f"Project {project_path} doesn't exist."
74+
+ f" Existing projects: {ws_context.ws_projects}"
75+
) from exception
76+
77+
try:
78+
config = ws_context.ws_projects_raw_configs[project_path]
79+
except KeyError as exception:
80+
raise Exception("First you need to parse config of project") from exception
81+
82+
services = _collect_services_in_config(config)
83+
project.services = services
84+
return services
85+
86+
87+
def _collect_services_in_config(
88+
config: dict[str, Any],
89+
) -> list[domain.ServiceDeclaration]:
90+
services: list[domain.ServiceDeclaration] = []
91+
for service_def_raw in config["tool"]["finecode"].get("service", []):
92+
try:
93+
service_def = config_models.ServiceDefinition(**service_def_raw)
94+
except config_models.ValidationError as exception:
95+
raise config_models.ConfigurationError(str(exception)) from exception
96+
97+
services.append(
98+
domain.ServiceDeclaration(
99+
interface=service_def.interface,
100+
source=service_def.source,
101+
env=service_def.env,
102+
dependencies=service_def.dependencies,
103+
)
104+
)
105+
return services
106+
107+
65108
def _collect_action_handler_configs_in_config(
66109
config: dict[str, Any],
67110
) -> dict[str, dict[str, Any]]:

src/finecode/config/config_models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ class ActionHandlerDefinition(BaseModel):
3030
enabled: bool = True
3131

3232

33+
class ServiceDefinition(BaseModel):
34+
interface: str
35+
source: str
36+
env: str
37+
dependencies: list[str] = []
38+
39+
3340
class ActionDefinition(BaseModel):
3441
source: str
3542
handlers: list[ActionHandlerDefinition] = []

src/finecode/config/read_configs.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ async def read_project_config(
171171
add_extension_runner_to_dependencies(project_config)
172172

173173
merge_handlers_dependencies_into_groups(project_config)
174+
merge_services_dependencies_into_groups(project_config)
174175

175176
ws_context.ws_projects_raw_configs[project.dir_path] = project_config
176177

@@ -400,6 +401,14 @@ def _merge_projects_configs(
400401
_merge_object_array_by_key(
401402
existing_handlers, new_handlers, "name"
402403
)
404+
elif key == "service":
405+
if key not in tool_finecode_config1:
406+
tool_finecode_config1[key] = []
407+
existing = tool_finecode_config1[key]
408+
if isinstance(value, list):
409+
_merge_object_array_by_key(existing, value, "interface")
410+
else:
411+
tool_finecode_config1[key] = value
403412
elif key == "action_handler":
404413
# Handle action_handler array merge by source
405414
if key not in tool_finecode_config1:
@@ -615,6 +624,35 @@ def merge_handlers_dependencies_into_groups(project_config: dict[str, Any]) -> N
615624
deps_groups[group_name] = unique_deps
616625

617626

627+
def merge_services_dependencies_into_groups(project_config: dict[str, Any]) -> None:
628+
# tool.finecode.service[x].dependencies
629+
services_list = project_config.get("tool", {}).get("finecode", {}).get("service", [])
630+
if "dependency-groups" not in project_config:
631+
project_config["dependency-groups"] = {}
632+
deps_groups = project_config["dependency-groups"]
633+
634+
for service in services_list:
635+
service_env = service.get("env", None)
636+
if service_env is None:
637+
logger.warning(f"Service {service} has no env, skip it")
638+
continue
639+
deps = service.get("dependencies", [])
640+
641+
if service_env not in deps_groups:
642+
deps_groups[service_env] = []
643+
644+
env_deps = deps_groups[service_env]
645+
env_deps += deps
646+
647+
for group_name in deps_groups.keys():
648+
deps_list = deps_groups[group_name]
649+
unique_deps = []
650+
for dep in deps_list:
651+
if dep not in unique_deps:
652+
unique_deps.append(dep)
653+
deps_groups[group_name] = unique_deps
654+
655+
618656
def add_extension_runner_to_dependencies(project_config: dict[str, Any]) -> None:
619657
try:
620658
deps_groups = project_config["dependency-groups"]

0 commit comments

Comments
 (0)