@@ -68,63 +68,86 @@ def parse_key(dotted_name: str):
6868
6969 @staticmethod
7070 def from_path (path : Path | None ) -> '_InternalCF | None' :
71- return _InternalCF (path ) if path and path .exists () else None
71+ cf = _InternalCF (path ) if path else None
72+ if not cf :
73+ return None
74+ if not cf .path and not cf .dropin_paths :
75+ return None
76+ return cf
7277
7378 def __init__ (self , path : Path ):
74- self .path = path
7579 self .cp = _configparser ()
76- read_files = self .cp .read (path , encoding = 'utf-8' )
77- if len (read_files ) != 1 :
78- raise FileNotFoundError (path )
80+ self .dropin_cp = _configparser ()
81+ self .path = path if path .exists () else None
82+ dropin_dir = Path (f'{ path } .d' )
83+ self .dropin_dir = dropin_dir if dropin_dir .exists () else None
84+ self .dropin_paths = []
85+ if self .dropin_dir :
86+ # dropin configs are applied in alphabetical order
87+ for conf in sorted (self .dropin_dir .iterdir ()):
88+ # only consider .conf files
89+ if conf .suffix .lower () == '.conf' :
90+ self .dropin_paths .append (self .dropin_dir / conf )
91+ self ._read ()
92+
93+ def _read (self ):
94+ if self .path :
95+ self .cp .read (self .path , encoding = 'utf-8' )
96+ if self .dropin_paths :
97+ self .dropin_cp .read (self .dropin_paths , encoding = 'utf-8' )
98+
99+ def _write (self ):
100+ if not self .path :
101+ raise WestNotFound ('No config file exists that can be written' )
102+ with open (self .path , 'w' , encoding = 'utf-8' ) as f :
103+ self .cp .write (f )
79104
80105 def __contains__ (self , option : str ) -> bool :
81106 section , key = _InternalCF .parse_key (option )
82107
83- return section in self .cp and key in self .cp [section ]
108+ if section in self .cp and key in self .cp [section ]:
109+ return True
110+ return section in self .dropin_cp and key in self .dropin_cp [section ]
84111
85112 def get (self , option : str ):
86- return self ._get (option , self .cp .get )
113+ return self ._get (option , self .cp .get , self . dropin_cp . get )
87114
88115 def getboolean (self , option : str ):
89- return self ._get (option , self .cp .getboolean )
116+ return self ._get (option , self .cp .getboolean , self . dropin_cp . getboolean )
90117
91118 def getint (self , option : str ):
92- return self ._get (option , self .cp .getint )
119+ return self ._get (option , self .cp .getint , self . dropin_cp . getint )
93120
94121 def getfloat (self , option : str ):
95- return self ._get (option , self .cp .getfloat )
122+ return self ._get (option , self .cp .getfloat , self . dropin_cp . getfloat )
96123
97- def _get (self , option , getter ):
124+ def _get (self , option , config_getter , dropin_getter ):
98125 section , key = _InternalCF .parse_key (option )
99-
100- try :
101- return getter (section , key )
102- except (configparser .NoOptionError , configparser .NoSectionError ) as err :
103- raise KeyError (option ) from err
126+ if section in self .cp and key in self .cp [section ]:
127+ getter = config_getter
128+ elif section in self .dropin_cp and key in self .dropin_cp [section ]:
129+ getter = dropin_getter
130+ else :
131+ raise KeyError (option )
132+ return getter (section , key )
104133
105134 def set (self , option : str , value : Any ):
106135 section , key = _InternalCF .parse_key (option )
107-
108136 if section not in self .cp :
109137 self .cp [section ] = {}
110-
111138 self .cp [section ][key ] = value
112-
113- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
114- self .cp .write (f )
139+ self ._write ()
115140
116141 def delete (self , option : str ):
117142 section , key = _InternalCF .parse_key (option )
118-
119- if section not in self .cp :
143+ if (section not in self .cp ) or (key not in self .cp [section ]):
120144 raise KeyError (option )
121145
122146 del self .cp [section ][key ]
123147 if not self .cp [section ].items ():
124148 del self .cp [section ]
125149
126- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
127- self .cp .write (f )
150+ self ._write ()
128151
129152class ConfigFile (Enum ):
130153 '''Types of west configuration file.
@@ -362,13 +385,14 @@ def _copy_to_configparser(self, cp: configparser.ConfigParser) -> None:
362385 # function-and-global-state APIs.
363386
364387 def load (cf : _InternalCF ):
365- for section , contents in cf .cp .items ():
366- if section == 'DEFAULT' :
367- continue
368- if section not in cp :
369- cp .add_section (section )
370- for key , value in contents .items ():
371- cp [section ][key ] = value
388+ for cp in [cf .dropin_cp , cf .cp ]:
389+ for section , contents in cp .items ():
390+ if section == 'DEFAULT' :
391+ continue
392+ if section not in cp :
393+ cp .add_section (section )
394+ for key , value in contents .items ():
395+ cp [section ][key ] = value
372396
373397 if self ._system :
374398 load (self ._system )
@@ -415,11 +439,12 @@ def _cf_to_dict(cf: _InternalCF | None) -> dict[str, Any]:
415439 ret : dict [str , Any ] = {}
416440 if cf is None :
417441 return ret
418- for section , contents in cf .cp .items ():
419- if section == 'DEFAULT' :
420- continue
421- for key , value in contents .items ():
422- ret [f'{ section } .{ key } ' ] = value
442+ for cp in [cf .dropin_cp , cf .cp ]:
443+ for section , contents in cp .items ():
444+ if section == 'DEFAULT' :
445+ continue
446+ for key , value in contents .items ():
447+ ret [f'{ section } .{ key } ' ] = value
423448 return ret
424449
425450
0 commit comments