@@ -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,7 +216,7 @@ 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 return self ._local_path
189222 elif configfile == ConfigFile .SYSTEM :
@@ -374,13 +407,14 @@ def _copy_to_configparser(self, cp: configparser.ConfigParser) -> None:
374407 # function-and-global-state APIs.
375408
376409 def load (cf : _InternalCF ):
377- for section , contents in cf .cp .items ():
378- if section == 'DEFAULT' :
379- continue
380- if section not in cp :
381- cp .add_section (section )
382- for key , value in contents .items ():
383- cp [section ][key ] = value
410+ for cp in [cf .dropin_cp , cf .cp ]:
411+ for section , contents in cp .items ():
412+ if section == 'DEFAULT' :
413+ continue
414+ if section not in cp :
415+ cp .add_section (section )
416+ for key , value in contents .items ():
417+ cp [section ][key ] = value
384418
385419 if self ._system :
386420 load (self ._system )
@@ -426,11 +460,12 @@ def _cf_to_dict(cf: _InternalCF | None) -> dict[str, Any]:
426460 ret : dict [str , Any ] = {}
427461 if cf is None :
428462 return ret
429- for section , contents in cf .cp .items ():
430- if section == 'DEFAULT' :
431- continue
432- for key , value in contents .items ():
433- ret [f'{ section } .{ key } ' ] = value
463+ for cp in [cf .dropin_cp , cf .cp ]:
464+ for section , contents in cp .items ():
465+ if section == 'DEFAULT' :
466+ continue
467+ for key , value in contents .items ():
468+ ret [f'{ section } .{ key } ' ] = value
434469 return ret
435470
436471
0 commit comments