@@ -59,6 +59,11 @@ class _InternalCF:
5959 # For internal use only; convenience interface for reading and
6060 # writing INI-style [section] key = value configuration files,
6161 # but presenting a west-style section.key = value style API.
62+ # The config file and the drop-in configs need separate
63+ # configparsers, since west needs to determine options that are
64+ # set in the config. E.g. if config values are updated, only those
65+ # values shall be written to the config, which are already present
66+ # (and not all values that may also come from dropin configs)
6267
6368 @staticmethod
6469 def parse_key (dotted_name : str ):
@@ -69,63 +74,91 @@ def parse_key(dotted_name: str):
6974
7075 @staticmethod
7176 def from_path (path : Path | None ) -> '_InternalCF | None' :
72- return _InternalCF (path ) if path and path .exists () else None
77+ if not path :
78+ return None
79+ cf = _InternalCF (path )
80+ if not cf .path and not cf .dropin_paths :
81+ return None
82+ return cf
7383
7484 def __init__ (self , path : Path ):
75- self .path = path
7685 self .cp = _configparser ()
77- read_files = self .cp .read (path , encoding = 'utf-8' )
78- if len (read_files ) != 1 :
79- raise FileNotFoundError (path )
86+ self .path = path if path .exists () else None
87+ if self .path :
88+ self .cp .read (self .path , encoding = 'utf-8' )
89+
90+ # consider dropin configs
91+ self .dropin_cp = _configparser ()
92+ self .dropin_dir = None
93+ self .dropin_paths = []
94+ # dropin configs must be enabled in config
95+ if self .cp .getboolean ('config' , 'dropins' , fallback = False ):
96+ # dropin dir is the config path with .d suffix
97+ dropin_dir = Path (f'{ path } .d' )
98+ self .dropin_dir = dropin_dir if dropin_dir .exists () else None
99+ if self .dropin_dir :
100+ # dropin configs are applied in alphabetical order
101+ for conf in sorted (self .dropin_dir .iterdir ()):
102+ # only consider .conf files
103+ if conf .suffix in ['.conf' , '.ini' ]:
104+ self .dropin_paths .append (self .dropin_dir / conf )
105+ if self .dropin_paths :
106+ self .dropin_cp .read (self .dropin_paths , encoding = 'utf-8' )
107+
108+ def _paths (self ) -> list [Path ]:
109+ ret = [p for p in self .dropin_paths ]
110+ if self .path :
111+ ret .append (self .path )
112+ return ret
113+
114+ def _write (self ):
115+ with open (self .path , 'w' , encoding = 'utf-8' ) as f :
116+ self .cp .write (f )
80117
81118 def __contains__ (self , option : str ) -> bool :
82119 section , key = _InternalCF .parse_key (option )
83120
84- return section in self .cp and key in self .cp [section ]
121+ if section in self .cp and key in self .cp [section ]:
122+ return True
123+ return section in self .dropin_cp and key in self .dropin_cp [section ]
85124
86125 def get (self , option : str ):
87- return self ._get (option , self .cp .get )
126+ return self ._get (option , self .cp .get , self . dropin_cp . get )
88127
89128 def getboolean (self , option : str ):
90- return self ._get (option , self .cp .getboolean )
129+ return self ._get (option , self .cp .getboolean , self . dropin_cp . getboolean )
91130
92131 def getint (self , option : str ):
93- return self ._get (option , self .cp .getint )
132+ return self ._get (option , self .cp .getint , self . dropin_cp . getint )
94133
95134 def getfloat (self , option : str ):
96- return self ._get (option , self .cp .getfloat )
135+ return self ._get (option , self .cp .getfloat , self . dropin_cp . getfloat )
97136
98- def _get (self , option , getter ):
137+ def _get (self , option , config_getter , dropin_getter ):
99138 section , key = _InternalCF .parse_key (option )
100-
101- try :
102- return getter (section , key )
103- except (configparser .NoOptionError , configparser .NoSectionError ) as err :
104- raise KeyError (option ) from err
139+ if section in self .cp and key in self .cp [section ]:
140+ getter = config_getter
141+ elif section in self .dropin_cp and key in self .dropin_cp [section ]:
142+ getter = dropin_getter
143+ else :
144+ raise KeyError (option )
145+ return getter (section , key )
105146
106147 def set (self , option : str , value : Any ):
107148 section , key = _InternalCF .parse_key (option )
108-
109149 if section not in self .cp :
110150 self .cp [section ] = {}
111-
112151 self .cp [section ][key ] = value
113-
114- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
115- self .cp .write (f )
152+ self ._write ()
116153
117154 def delete (self , option : str ):
118155 section , key = _InternalCF .parse_key (option )
119-
120- if section not in self .cp :
156+ if option not in self :
121157 raise KeyError (option )
122-
123158 del self .cp [section ][key ]
124159 if not self .cp [section ].items ():
125160 del self .cp [section ]
126-
127- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
128- self .cp .write (f )
161+ self ._write ()
129162
130163
131164class ConfigFile (Enum ):
@@ -183,22 +216,22 @@ def __init__(self, topdir: PathType | None = None):
183216
184217 def get_path (self , configfile : ConfigFile = ConfigFile .LOCAL ):
185218 if configfile == ConfigFile .ALL :
186- raise RuntimeError (f'{ configfile } not allowed for get_path ' )
219+ raise RuntimeError (f'{ configfile } not allowed for this operation ' )
187220 elif configfile == ConfigFile .LOCAL :
188221 if not self ._local_path :
189222 raise MalformedConfig ('local configuration cannot be determined' )
190223 return self ._local_path
191- elif configfile == ConfigFile .SYSTEM :
192- return self ._system_path
193224 elif configfile == ConfigFile .GLOBAL :
194225 return self ._global_path
226+ elif configfile == ConfigFile .SYSTEM :
227+ return self ._system_path
195228
196229 def get_paths (self , configfile : ConfigFile = ConfigFile .ALL ):
197230 ret = []
198- if self ._global and configfile in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
199- ret += self ._global ._paths ()
200231 if self ._system and configfile in [ConfigFile .SYSTEM , ConfigFile .ALL ]:
201232 ret += self ._system ._paths ()
233+ if self ._global and configfile in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
234+ ret += self ._global ._paths ()
202235 if self ._local and configfile in [ConfigFile .LOCAL , ConfigFile .ALL ]:
203236 ret += self ._local ._paths ()
204237 return ret
@@ -376,13 +409,14 @@ def _copy_to_configparser(self, cp: configparser.ConfigParser) -> None:
376409 # function-and-global-state APIs.
377410
378411 def load (cf : _InternalCF ):
379- for section , contents in cf .cp .items ():
380- if section == 'DEFAULT' :
381- continue
382- if section not in cp :
383- cp .add_section (section )
384- for key , value in contents .items ():
385- cp [section ][key ] = value
412+ for cp in [cf .dropin_cp , cf .cp ]:
413+ for section , contents in cp .items ():
414+ if section == 'DEFAULT' :
415+ continue
416+ if section not in cp :
417+ cp .add_section (section )
418+ for key , value in contents .items ():
419+ cp [section ][key ] = value
386420
387421 if self ._system :
388422 load (self ._system )
@@ -428,11 +462,12 @@ def _cf_to_dict(cf: _InternalCF | None) -> dict[str, Any]:
428462 ret : dict [str , Any ] = {}
429463 if cf is None :
430464 return ret
431- for section , contents in cf .cp .items ():
432- if section == 'DEFAULT' :
433- continue
434- for key , value in contents .items ():
435- ret [f'{ section } .{ key } ' ] = value
465+ for cp in [cf .dropin_cp , cf .cp ]:
466+ for section , contents in cp .items ():
467+ if section == 'DEFAULT' :
468+ continue
469+ for key , value in contents .items ():
470+ ret [f'{ section } .{ key } ' ] = value
436471 return ret
437472
438473
0 commit comments