11from __future__ import annotations
22
33import fnmatch
4+ from collections .abc import Iterable
45from copy import deepcopy
6+ from functools import reduce
7+ from operator import getitem
58from typing import Any , Protocol , runtime_checkable
69
10+ import asyncer
711from attrs import define , field
8- from loguru import logger
912
1013from lsp_client .utils .uri import from_local_uri
1114
15+ Pattern = str
16+ """Glob pattern"""
17+
1218
1319def 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
2737class 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