44Configuration management module.
55"""
66
7- from json import loads , dump , JSONDecodeError
7+ from json import loads , dumps
88from abc import ABC , abstractmethod
99from datetime import datetime
1010import re
1111import os
12+ import tomlkit
1213from typing import Any , Dict
1314
1415try : # use gamuLogger if available # pragma: no cover
1718 def _trace (msg : str ) -> None :
1819 Logger .trace (msg )
1920except ImportError : # pragma: no cover
20- def _trace (_ : str ) -> None :
21- pass
21+ def _trace (msg : str ) -> None :
22+ print ( msg )
2223
2324
2425
25- class BaseConfig ( ABC ) :
26+ class BaseConfig :
2627 """
2728 Base class for configuration management.
28- This class provides methods to load, save, and manage configuration settings.
29- It is designed to be subclassed for specific configuration formats (e.g., JSON, YAML).
29+ This class provides methods to manage configuration settings.
30+ Can be subclassed for specific configuration formats (e.g., JSON, YAML).
3031 """
3132 RE_REFERENCE = re .compile (r'\$\{([a-zA-Z0-9_.]+)\}' )
3233
3334 def __init__ (self ):
3435 self ._config : Dict [str , Any ] = {}
35- self ._load ()
36-
37- @abstractmethod
38- def _load (self ) -> 'BaseConfig' :
39- """
40- Load configuration
41- """
42-
43- @abstractmethod
44- def _save (self ) -> 'BaseConfig' :
45- """
46- Save configuration
47- """
48-
49- @abstractmethod
50- def _reload (self ) -> 'BaseConfig' :
51- """
52- Reload configuration
53- """
5436
5537 def get (self , key : str , / , default : Any = None , set_if_not_found : bool = False ) -> str | int | float | bool :
5638 """
@@ -61,7 +43,6 @@ def get(self, key: str, /, default: Any = None, set_if_not_found: bool = False)
6143 :return: Configuration value.
6244 """
6345 _trace (f"Getting config value for key: { key } " )
64- self ._reload ()
6546 key_tokens = key .split ('.' )
6647 config = self ._config
6748 for token in key_tokens :
@@ -92,7 +73,6 @@ def set(self, key: str, value : Any) -> 'BaseConfig':
9273 :param value: Configuration value.
9374 """
9475 _trace (f"Setting config value for key: { key } to { value } " )
95- self ._reload ()
9676 key_tokens = key .split ('.' )
9777 config = self ._config
9878 for token in key_tokens [:- 1 ]:
@@ -103,7 +83,6 @@ def set(self, key: str, value : Any) -> 'BaseConfig':
10383 config [key_tokens [- 1 ]] = value
10484 except TypeError :
10585 raise TypeError (f"Cannot set value for key '{ key } ' because key is already a non-dict type." ) from None
106- self ._save ()
10786 return self
10887
10988 def remove (self , key : str ) -> 'BaseConfig' :
@@ -113,7 +92,6 @@ def remove(self, key: str) -> 'BaseConfig':
11392 :param key: Configuration key.
11493 """
11594 _trace (f"Removing config key: { key } " )
116- self ._reload ()
11795 key_tokens = key .split ('.' )
11896 config = self ._config
11997 for token in key_tokens [:- 1 ]:
@@ -129,37 +107,52 @@ def remove(self, key: str) -> 'BaseConfig':
129107 raise KeyError (f"Key '{ key } ' not found in configuration." )
130108 return self
131109
110+ def __str__ (self ) -> str :
111+ """
112+ String representation of the configuration.
113+ """
114+ return str (self ._config )
132115
133- class JSONConfig (BaseConfig ):
116+ def __repr__ (self ) -> str :
117+ """
118+ String representation of the configuration.
119+ """
120+ return f"{ self .__class__ .__name__ } ({ self ._config } )"
121+
122+ class FileConfig (BaseConfig , ABC ):
134123 """
135- JSON configuration management class.
136- This class provides methods to load, save, and manage configuration settings in JSON format.
124+ File configuration management class.
125+
126+ Must be subclassed for specific file formats (e.g., JSON, TOML) that implement `_to_string` and `_from_string`.
137127 """
138128 def __init__ (self , file_path : str ):
139129 self .file_path = file_path
140130 self ._last_modified = datetime .now ()
141131 super ().__init__ ()
132+ self ._load ()
133+
134+ def __init_empty (self ) -> 'FileConfig' :
135+ self ._config = {}
136+ self ._save ()
137+ return self
142138
143- def _load (self ) -> 'JSONConfig ' :
139+ def _load (self ) -> 'FileConfig ' :
144140 """
145- Load configuration from a JSON file.
141+ Load configuration from a config file.
142+ the _from_string method must be implemented in subclasses.
146143 """
147144 _trace (f"Loading configuration from { self .file_path } " )
148145 if not os .path .exists (self .file_path ):
149- self ._config = {}
150- self ._save ()
151- return self
146+ return self .__init_empty ()
152147 with open (self .file_path , 'r' , encoding = "utf-8" ) as file :
153148 content = file .read ()
154149 if content .strip () == "" :
155150 _trace (f"Configuration file { self .file_path } is empty, creating empty config" )
156- self ._config = {}
157- self ._save ()
158- return self
159- self ._config = loads (content )
151+ return self .__init_empty ()
152+ self ._from_string (content )
160153 return self
161154
162- def _reload (self ) -> 'JSONConfig ' :
155+ def _reload (self ) -> 'FileConfig ' :
163156 """
164157 Reload configuration from a JSON file if the modification time has changed.
165158 """
@@ -169,20 +162,138 @@ def _reload(self) -> 'JSONConfig':
169162 return self
170163 file_modified_time = os .path .getmtime (self .file_path ) #when the file was last modified
171164 config_modified_time = self ._last_modified .timestamp () #when the config was last modified (this object)
172- if self . _last_modified is None or file_modified_time > config_modified_time :
165+ if file_modified_time > config_modified_time :
173166 _trace (f"Reloading configuration from { self .file_path } due to modification time change" )
174167 self ._load ()
175168 self ._last_modified = datetime .fromtimestamp (file_modified_time )
176169 else :
177170 _trace (f"Configuration file { self .file_path } has not changed since last load" )
178171 return self
179172
180- def _save (self ) -> 'JSONConfig ' :
173+ def _save (self ) -> 'FileConfig ' :
181174 """
182175 Save configuration to a JSON file.
183176 """
184177 _trace (f"Saving configuration to { self .file_path } " )
185178 with open (self .file_path , 'w' , encoding = "utf-8" ) as file :
186- dump (self ._config , file , indent = 4 )
179+ file . write (self ._to_string () )
187180 self ._last_modified = datetime .now ()
188181 return self
182+
183+ def get (self , key : str , / , default : Any = None , set_if_not_found : bool = False ) -> str | int | float | bool :
184+ """
185+ Get the value of a configuration key.
186+
187+ :param key: Configuration key.
188+ :param default: Default value if the key does not exist.
189+ :return: Configuration value.
190+ """
191+ self ._reload ()
192+ return super ().get (key , default , set_if_not_found )
193+
194+ def set (self , key : str , value : Any ) -> 'FileConfig' :
195+ """
196+ Set the value of a configuration key.
197+
198+ :param key: Configuration key.
199+ :param value: Configuration value.
200+ """
201+ self ._reload ()
202+ super ().set (key , value )
203+ self ._save ()
204+ return self
205+
206+ def remove (self , key : str ) -> 'FileConfig' :
207+ """
208+ Remove a configuration key.
209+
210+ :param key: Configuration key.
211+ """
212+ self ._reload ()
213+ super ().remove (key )
214+ self ._save ()
215+ return self
216+
217+ @abstractmethod
218+ def _to_string (self ) -> str :
219+ """
220+ String representation of the configuration.
221+ """
222+
223+ @abstractmethod
224+ def _from_string (self , config_string : str ) -> None :
225+ """
226+ Create a configuration object from a string.
227+
228+ :param config_string: Configuration string.
229+ :return: Configuration object.
230+ """
231+
232+ class JSONConfig (FileConfig ):
233+ """
234+ JSON configuration management class.
235+ This class provides methods to load, save, and manage configuration settings in JSON format.
236+ """
237+
238+ def _to_string (self ) -> str :
239+ """
240+ String representation of the configuration in JSON format.
241+ """
242+ return dumps (self ._config , indent = 4 )
243+
244+ def _from_string (self , config_string : str ) -> None :
245+ """
246+ Create a configuration object from a JSON string.
247+
248+ :param config_string: Configuration string.
249+ :return: Configuration object.
250+ """
251+ self ._config = loads (config_string )
252+ if not isinstance (self ._config , dict ):
253+ raise ValueError ("Invalid JSON format: expected a dictionary." )
254+
255+ class TOMLConfig (FileConfig ):
256+ """
257+ TOML configuration management class.
258+ This class provides methods to load, save, and manage configuration settings in TOML format.
259+ """
260+
261+ def _to_string (self ) -> str :
262+ """
263+ String representation of the configuration in TOML format.
264+ """
265+ return tomlkit .dumps (self ._config )
266+
267+ def _from_string (self , config_string : str ) -> None :
268+ """
269+ Create a configuration object from a TOML string.
270+
271+ :param config_string: Configuration string.
272+ :return: Configuration object.
273+ """
274+ self ._config = tomlkit .loads (config_string )
275+
276+ class MemoryConfig (BaseConfig ):
277+ """
278+ In-memory configuration management class.
279+ This class provides methods to load, save, and manage configuration settings in memory.
280+ Does not persist to a file.
281+ """
282+ def __init__ (self , initial : Dict [str , Any ] = None ):
283+ super ().__init__ ()
284+ if initial is not None :
285+ self ._config = initial
286+
287+
288+ def get_config (file_path : str ) -> FileConfig :
289+ """
290+ Get a configuration object based on the file extension.
291+
292+ :param file_path: Path to the configuration file.
293+ :return: Configuration object.
294+ """
295+ if file_path .lower ().endswith ('.json' ):
296+ return JSONConfig (file_path )
297+ if file_path .lower ().endswith ('.toml' ):
298+ return TOMLConfig (file_path )
299+ raise ValueError (f"Unsupported configuration file format: { file_path } " )
0 commit comments