Skip to content

Commit 77e7c5c

Browse files
committed
feat: update config map
1 parent f569528 commit 77e7c5c

File tree

1 file changed

+53
-53
lines changed

1 file changed

+53
-53
lines changed

src/lsp_client/utils/config.py

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
from __future__ import annotations
22

33
import fnmatch
4+
from collections.abc import Iterable
45
from copy import deepcopy
6+
from functools import reduce
7+
from operator import getitem
58
from typing import Any, Protocol, runtime_checkable
69

10+
import asyncer
711
from attrs import define, field
8-
from loguru import logger
912

1013
from lsp_client.utils.uri import from_local_uri
1114

15+
Pattern = str
16+
"""Glob pattern"""
17+
1218

1319
def deep_merge(base: dict[str, Any], update: dict[str, Any]) -> dict[str, Any]:
1420
"""
@@ -23,13 +29,29 @@ def deep_merge(base: dict[str, Any], update: dict[str, Any]) -> dict[str, Any]:
2329
return result
2430

2531

32+
def deep_get(d: dict, keys: Iterable) -> Any:
33+
return reduce(getitem, keys, d)
34+
35+
2636
@runtime_checkable
2737
class ConfigurationChangeListener(Protocol):
2838
"""
2939
Protocol for configuration change listeners.
3040
"""
3141

32-
def __call__(self, config_map: ConfigurationMap, **kwargs: Any) -> Any: ...
42+
async def __call__(self, config_map: ConfigurationMap) -> Any: ...
43+
44+
45+
Config = dict[str, Any]
46+
GlobalConfig = Config
47+
48+
49+
class ScopeConfig(Config):
50+
"""
51+
A helper class to represent a scope-specific configuration.
52+
"""
53+
54+
pattern: Pattern
3355

3456

3557
@define
@@ -39,78 +61,56 @@ class ConfigurationMap:
3961
Supports global configuration and scope-specific overrides.
4062
"""
4163

42-
_global_config: dict[str, Any] = field(factory=dict)
43-
_scoped_configs: list[tuple[str, dict[str, Any]]] = field(factory=list)
64+
global_config: GlobalConfig = field(factory=dict)
65+
scoped_configs: list[ScopeConfig] = field(factory=list)
66+
4467
_on_change_callbacks: list[ConfigurationChangeListener] = field(
4568
factory=list, init=False
4669
)
4770

4871
def on_change(self, callback: ConfigurationChangeListener) -> None:
49-
"""
50-
Register a callback to be called when the configuration changes.
51-
"""
5272
self._on_change_callbacks.append(callback)
5373

54-
def _notify_change(self, **kwargs: Any) -> None:
55-
for callback in self._on_change_callbacks:
56-
try:
57-
callback(self, **kwargs)
58-
except Exception as e:
59-
logger.error(f"Error in configuration change callback: {e}")
74+
async def _notify_change(self) -> None:
75+
async with asyncer.create_task_group() as tg:
76+
for callback in self._on_change_callbacks:
77+
tg.soonify(callback)
6078

61-
def update_global(
62-
self, config: dict[str, Any], merge: bool = True, **kwargs: Any
79+
async def update_global(
80+
self, config: dict[str, Any], *, merge: bool = True
6381
) -> None:
64-
"""
65-
Update global configuration.
66-
"""
67-
if merge:
68-
self._global_config = deep_merge(self._global_config, config)
69-
else:
70-
self._global_config = deepcopy(config)
71-
self._notify_change(**kwargs)
72-
73-
def add_scope(self, pattern: str, config: dict[str, Any], **kwargs: Any) -> None:
82+
self.global_config = (
83+
deep_merge(self.global_config, config) #
84+
if merge
85+
else deepcopy(config)
86+
)
87+
await self._notify_change()
88+
89+
async def add_scope(self, pattern: Pattern, config: dict[str, Any]) -> None:
7490
"""
7591
Add a configuration override for a specific file pattern.
7692
7793
:param pattern: Glob pattern (e.g. "**/tests/**", "*.py")
7894
:param config: The configuration dict to merge for this scope
7995
"""
80-
self._scoped_configs.append((pattern, config))
81-
self._notify_change(**kwargs)
96+
97+
self.scoped_configs.append(ScopeConfig(pattern=pattern, **config))
98+
await self._notify_change()
8299

83100
def _get_section(self, config: Any, section: str | None) -> Any:
84101
if not section:
85102
return config
86103

87-
current = config
88-
for part in section.split("."):
89-
if isinstance(current, dict) and part in current:
90-
current = current[part]
91-
else:
92-
return None
93-
return current
104+
return deep_get(config, section.split("."))
94105

95106
def get(self, scope_uri: str | None, section: str | None) -> Any:
96-
final_config = self._global_config
97-
98-
if scope_uri:
99-
try:
100-
path_str = str(from_local_uri(scope_uri))
101-
for pattern, scoped_config in self._scoped_configs:
102-
if fnmatch.fnmatch(path_str, pattern):
103-
final_config = deep_merge(final_config, scoped_config)
104-
except Exception:
105-
logger.warning(f"Failed to parse scope URI: {scope_uri}")
107+
final_config = self.global_config
106108

107-
return self._get_section(final_config, section)
109+
if not scope_uri:
110+
return self._get_section(final_config, section)
108111

109-
def has_global_config(self) -> bool:
110-
"""
111-
Check if the configuration map has any global configuration.
112-
113-
Returns:
114-
True if global configuration is not empty, False otherwise.
115-
"""
116-
return bool(self._global_config)
112+
path_str = from_local_uri(scope_uri).as_posix()
113+
for scope_config in self.scoped_configs:
114+
if not fnmatch.fnmatch(path_str, scope_config.pattern):
115+
continue
116+
final_config = deep_merge(final_config, scope_config)

0 commit comments

Comments
 (0)