@@ -70,15 +70,30 @@ def parse_key(dotted_name: str):
7070 return section_child
7171
7272 @staticmethod
73- def from_path (path : Path | None ) -> '_InternalCF | None' :
74- return _InternalCF (path ) if path and path .exists () else None
75-
76- def __init__ (self , path : Path ):
77- self .path = path
73+ def from_paths (paths : list [Path ] | None ) -> '_InternalCF | None' :
74+ if not paths :
75+ return None
76+ paths = [p for p in paths if p .exists ()]
77+ return _InternalCF (paths ) if paths else None
78+
79+ def __init__ (self , paths : list [Path ]):
80+ # Latter elements in the list have precedence as documented by configparser.
7881 self .cp = _configparser ()
79- read_files = self .cp .read (path , encoding = 'utf-8' )
80- if len (read_files ) != 1 :
81- raise FileNotFoundError (path )
82+ self .paths = paths
83+ read_files = self .cp .read (self .paths , encoding = 'utf-8' )
84+ if len (read_files ) != len (self .paths ):
85+ readable = [str (p ) for p in paths ]
86+ raise MalformedConfig (f"Error while reading one of '{ readable } '" )
87+
88+ def _check_single_config (self ):
89+ assert self .paths
90+ if len (self .paths ) > 1 :
91+ raise ValueError (f'Cannot write if multiple configs in use: { self .paths } ' )
92+
93+ def _write (self ):
94+ assert len (self .paths ) == 1 # callers are expected to _check_single_config()
95+ with open (self .paths [0 ], 'w' , encoding = 'utf-8' ) as f :
96+ self .cp .write (f )
8297
8398 def __contains__ (self , option : str ) -> bool :
8499 section , key = _InternalCF .parse_key (option )
@@ -106,17 +121,18 @@ def _get(self, option, getter):
106121 raise KeyError (option ) from err
107122
108123 def set (self , option : str , value : Any ):
124+ self ._check_single_config ()
109125 section , key = _InternalCF .parse_key (option )
110126
111127 if section not in self .cp :
112128 self .cp [section ] = {}
113129
114130 self .cp [section ][key ] = value
115131
116- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
117- self .cp .write (f )
132+ self ._write ()
118133
119134 def delete (self , option : str ):
135+ self ._check_single_config ()
120136 section , key = _InternalCF .parse_key (option )
121137
122138 if section not in self .cp :
@@ -126,8 +142,7 @@ def delete(self, option: str):
126142 if not self .cp [section ].items ():
127143 del self .cp [section ]
128144
129- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
130- self .cp .write (f )
145+ self ._write ()
131146
132147
133148class ConfigFile (Enum ):
@@ -173,26 +188,34 @@ def __init__(self, topdir: PathType | None = None):
173188 :param topdir: workspace location; may be None
174189 '''
175190
176- local_path = _location (ConfigFile .LOCAL , topdir = topdir , find_local = False ) or None
177-
178- self ._system_path = Path (_location (ConfigFile .SYSTEM , topdir = topdir ))
179- self ._global_path = Path (_location (ConfigFile .GLOBAL , topdir = topdir ))
180- self ._local_path = Path (local_path ) if local_path is not None else None
191+ self ._local_locs = [Path (p ) for p in _location (ConfigFile .LOCAL , topdir , find_local = False )]
192+ self ._global_locs = [Path (p ) for p in _location (ConfigFile .GLOBAL , topdir )]
193+ self ._system_locs = [Path (p ) for p in _location (ConfigFile .SYSTEM , topdir )]
181194
182- self ._system = _InternalCF .from_path (self ._system_path )
183- self ._global = _InternalCF .from_path (self ._global_path )
184- self ._local = _InternalCF .from_path (self ._local_path )
195+ self ._system = _InternalCF .from_paths (self ._system_locs )
196+ self ._global = _InternalCF .from_paths (self ._global_locs )
197+ self ._local = _InternalCF .from_paths (self ._local_locs )
185198
186- def get_paths (self , location : ConfigFile = ConfigFile .ALL ) -> list [Path ]:
199+ def get_search_paths (self , location : ConfigFile = ConfigFile .ALL ) -> list [Path ]:
187200 ret = []
188- if self . _system and location in [ConfigFile .SYSTEM , ConfigFile .ALL ]:
189- ret .append (self ._system . path )
190- if self . _global and location in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
191- ret .append (self ._global . path )
192- if self . _local and location in [ConfigFile .LOCAL , ConfigFile .ALL ]:
193- ret .append (self ._local . path )
201+ if location in [ConfigFile .SYSTEM , ConfigFile .ALL ]:
202+ ret .extend (self ._system_locs )
203+ if location in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
204+ ret .extend (self ._global_locs )
205+ if location in [ConfigFile .LOCAL , ConfigFile .ALL ]:
206+ ret .extend (self ._local_locs )
194207 return ret
195208
209+ def get_existing_paths (self , location : ConfigFile = ConfigFile .ALL ) -> list [Path ]:
210+ paths = []
211+ if location in [ConfigFile .SYSTEM , ConfigFile .ALL ]:
212+ paths .extend (self ._system .paths if self ._system else [])
213+ if location in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
214+ paths .extend (self ._global .paths if self ._global else [])
215+ if location in [ConfigFile .LOCAL , ConfigFile .ALL ]:
216+ paths .extend (self ._local .paths if self ._local else [])
217+ return paths
218+
196219 def get (
197220 self , option : str , default : str | None = None , configfile : ConfigFile = ConfigFile .ALL
198221 ) -> str | None :
@@ -287,36 +310,40 @@ def set(self, option: str, value: Any, configfile: ConfigFile = ConfigFile.LOCAL
287310 # We need a real configuration file; ALL doesn't make sense here.
288311 raise ValueError (configfile )
289312 elif configfile == ConfigFile .LOCAL :
290- if self ._local_path is None :
313+ if not self ._local_locs :
291314 raise ValueError (
292315 f'{ configfile } : file not found; retry in a workspace or set WEST_CONFIG_LOCAL'
293316 )
294- if not self ._local_path .exists ():
295- self ._local = self ._create (self ._local_path )
296- if TYPE_CHECKING :
297- assert self ._local
298- self ._local .set (option , value )
317+
318+ # ensure that only one config is in use
319+ configs = self .get_search_paths (configfile )
320+ if len (configs ) > 1 :
321+ raise ValueError (f'Cannot set value if multiple configs in use: { configs } ' )
322+ assert len (configs ) == 1 , f'{ configfile } : no config file in use'
323+ config_path = configs [0 ]
324+
325+ # get internal attribute name (_local/_global/_system)
326+ assert configfile in [ConfigFile .SYSTEM , ConfigFile .GLOBAL , ConfigFile .LOCAL ]
327+ if configfile == ConfigFile .SYSTEM :
328+ attr = '_system'
299329 elif configfile == ConfigFile .GLOBAL :
300- if not self ._global_path .exists ():
301- self ._global = self ._create (self ._global_path )
302- if TYPE_CHECKING :
303- assert self ._global
304- self ._global .set (option , value )
305- elif configfile == ConfigFile .SYSTEM :
306- if not self ._system_path .exists ():
307- self ._system = self ._create (self ._system_path )
308- if TYPE_CHECKING :
309- assert self ._system
310- self ._system .set (option , value )
311- else :
312- # Shouldn't happen.
313- raise AssertionError (configfile )
330+ attr = '_global'
331+ elif configfile == ConfigFile .LOCAL :
332+ attr = '_local'
333+
334+ # create config file if it does not already exist and use it
335+ if not getattr (self , attr , None ):
336+ config = self ._create (config_path )
337+ setattr (self , attr , config )
338+
339+ # set config value
340+ getattr (self , attr ).set (option , value )
314341
315342 @staticmethod
316343 def _create (path : Path ) -> _InternalCF :
317344 path .parent .mkdir (parents = True , exist_ok = True )
318345 path .touch (exist_ok = True )
319- ret = _InternalCF .from_path ( path )
346+ ret = _InternalCF .from_paths ([ path ] )
320347 if TYPE_CHECKING :
321348 assert ret
322349 return ret
@@ -441,6 +468,13 @@ def _deprecated(old_function):
441468 )
442469
443470
471+ def _ensure_one_file_max_configs (topdir ):
472+ for loc in [ConfigFile .SYSTEM , ConfigFile .GLOBAL , ConfigFile .LOCAL ]:
473+ config = _location (loc , topdir = topdir , find_local = False )
474+ if len (config ) > 1 :
475+ raise MalformedConfig (f"{ loc } : Switch to newer API to use multiple configs: '{ config } '" )
476+
477+
444478def read_config (
445479 configfile : ConfigFile | None = None ,
446480 config : configparser .ConfigParser = config ,
@@ -471,6 +505,7 @@ def read_config(
471505 :param topdir: west workspace root to read local options from
472506 '''
473507 _deprecated ('read_config' )
508+ _ensure_one_file_max_configs (topdir )
474509
475510 if configfile is None :
476511 configfile = ConfigFile .ALL
@@ -503,6 +538,7 @@ def update_config(
503538 one is not found).
504539 '''
505540 _deprecated ('update_config' )
541+ _ensure_one_file_max_configs (topdir )
506542
507543 if configfile == ConfigFile .ALL :
508544 # Not possible to update ConfigFile.ALL, needs specific conf file here.
@@ -552,20 +588,23 @@ def delete_config(
552588 one is not found).
553589 '''
554590 _deprecated ('delete_config' )
591+ _ensure_one_file_max_configs (topdir )
555592
556593 stop = False
594+ considered_locations = []
557595 if configfile is None :
558- to_check = [_location ( x , topdir = topdir ) for x in [ ConfigFile .LOCAL , ConfigFile .GLOBAL ] ]
596+ considered_locations = [ConfigFile .LOCAL , ConfigFile .GLOBAL ]
559597 stop = True
560598 elif configfile == ConfigFile .ALL :
561- to_check = [
562- _location (x , topdir = topdir )
563- for x in [ConfigFile .SYSTEM , ConfigFile .GLOBAL , ConfigFile .LOCAL ]
564- ]
599+ considered_locations = [ConfigFile .SYSTEM , ConfigFile .GLOBAL , ConfigFile .LOCAL ]
565600 elif isinstance (configfile , ConfigFile ):
566- to_check = [_location ( configfile , topdir = topdir ) ]
601+ considered_locations = [configfile ]
567602 else :
568- to_check = [_location (x , topdir = topdir ) for x in configfile ]
603+ considered_locations = configfile
604+ assert isinstance (considered_locations , list )
605+
606+ # thanks to _ensure_one_file_max_configs() above, we don't actually have 2 levels here.
607+ to_check = [f for cfg in considered_locations for f in _location (cfg , topdir = topdir )]
569608
570609 found = False
571610 for path in to_check :
@@ -597,7 +636,9 @@ def _rel_topdir_to_abs(p: PathType, topdir: PathType | None) -> str:
597636 return str (os .path .join (topdir , p ))
598637
599638
600- def _location (cfg : ConfigFile , topdir : PathType | None = None , find_local : bool = True ) -> str :
639+ def _location (
640+ cfg : ConfigFile , topdir : PathType | None = None , find_local : bool = True
641+ ) -> list [str ]:
601642 # Return the WEST_CONFIG_x environment variable if defined, or the
602643 # OS-specific default value. Anchors relative paths to
603644 # "topdir". Does _not_ check whether the file exists or if it is
@@ -616,27 +657,34 @@ def _location(cfg: ConfigFile, topdir: PathType | None = None, find_local: bool
616657 # any cases which might run on Windows, then call fspath() on the
617658 # final result. This lets the standard library do the work of
618659 # producing a canonical string name.
660+
661+ def parse_paths (paths : str | None , sep : str = os .pathsep ) -> list [Path ]:
662+ """Split a string into a list of Path objects using the given separator."""
663+ paths = paths or ""
664+ return [Path (p ) for p in paths .split (sep ) if p ]
665+
619666 env = os .environ
620667
621668 if cfg == ConfigFile .ALL :
622669 raise ValueError ('ConfigFile.ALL has no location' )
623670 elif cfg == ConfigFile .SYSTEM :
624671 if 'WEST_CONFIG_SYSTEM' in env :
625- return _rel_topdir_to_abs (env ['WEST_CONFIG_SYSTEM' ], topdir )
672+ paths = parse_paths (env ['WEST_CONFIG_SYSTEM' ])
673+ return [_rel_topdir_to_abs (p , topdir ) for p in paths ]
626674
627675 plat = platform .system ()
628676
629677 if plat == 'Linux' :
630- return '/etc/westconfig'
678+ return [ '/etc/westconfig' ]
631679
632680 if plat == 'Darwin' :
633- return '/usr/local/etc/westconfig'
681+ return [ '/usr/local/etc/westconfig' ]
634682
635683 if plat == 'Windows' :
636- return os .path .expandvars ('%PROGRAMDATA%\\ west\\ config' )
684+ return [ os .path .expandvars ('%PROGRAMDATA%\\ west\\ config' )]
637685
638686 if 'BSD' in plat :
639- return '/etc/westconfig'
687+ return [ '/etc/westconfig' ]
640688
641689 if 'CYGWIN' in plat or 'MSYS_NT' in plat :
642690 # Cygwin can handle windows style paths, so make sure we
@@ -647,45 +695,47 @@ def _location(cfg: ConfigFile, topdir: PathType | None = None, find_local: bool
647695 # See https://github.com/zephyrproject-rtos/west/issues/300
648696 # for details.
649697 pd = PureWindowsPath (os .environ ['ProgramData' ])
650- return os .fspath (pd / 'west' / 'config' )
698+ return [ os .fspath (pd / 'west' / 'config' )]
651699
652700 raise ValueError ('unsupported platform ' + plat )
653701 elif cfg == ConfigFile .GLOBAL :
654702 if 'WEST_CONFIG_GLOBAL' in env :
655- return _rel_topdir_to_abs (env ['WEST_CONFIG_GLOBAL' ], topdir )
703+ paths = parse_paths (env ['WEST_CONFIG_GLOBAL' ])
704+ return [_rel_topdir_to_abs (p , topdir ) for p in paths ]
656705
657706 if platform .system () == 'Linux' and 'XDG_CONFIG_HOME' in env :
658- return os .path .join (env ['XDG_CONFIG_HOME' ], 'west' , 'config' )
707+ return [ os .path .join (env ['XDG_CONFIG_HOME' ], 'west' , 'config' )]
659708
660- return os .fspath (Path .home () / '.westconfig' )
709+ return [ os .fspath (Path .home () / '.westconfig' )]
661710 elif cfg == ConfigFile .LOCAL :
662711 if 'WEST_CONFIG_LOCAL' in env :
663- return _rel_topdir_to_abs (env ['WEST_CONFIG_LOCAL' ], topdir )
712+ paths = parse_paths (env ['WEST_CONFIG_LOCAL' ])
713+ return [_rel_topdir_to_abs (p , topdir ) for p in paths ]
664714
665715 if topdir :
666- return os .fspath (Path (topdir ) / WEST_DIR / 'config' )
716+ return [ os .fspath (Path (topdir ) / WEST_DIR / 'config' )]
667717
668718 if find_local :
669719 # Might raise WestNotFound!
670- return os .fspath (Path (west_dir ()) / 'config' )
720+ return [ os .fspath (Path (west_dir ()) / 'config' )]
671721 else :
672- return ''
722+ return []
673723 else :
674724 raise ValueError (f'invalid configuration file { cfg } ' )
675725
676726
677727def _gather_configs (cfg : ConfigFile , topdir : PathType | None ) -> list [str ]:
678728 # Find the paths to the given configuration files, in increasing
679729 # precedence order.
680- ret = []
730+ ret : list [ str ] = []
681731
682732 if cfg == ConfigFile .ALL or cfg == ConfigFile .SYSTEM :
683- ret .append (_location (ConfigFile .SYSTEM , topdir = topdir ))
733+ ret .extend (_location (ConfigFile .SYSTEM , topdir = topdir ))
684734 if cfg == ConfigFile .ALL or cfg == ConfigFile .GLOBAL :
685- ret .append (_location (ConfigFile .GLOBAL , topdir = topdir ))
735+ ret .extend (_location (ConfigFile .GLOBAL , topdir = topdir ))
686736 if cfg == ConfigFile .ALL or cfg == ConfigFile .LOCAL :
687737 try :
688- ret .append (_location (ConfigFile .LOCAL , topdir = topdir ))
738+ ret .extend (_location (ConfigFile .LOCAL , topdir = topdir ))
689739 except WestNotFound :
690740 pass
691741
@@ -695,11 +745,12 @@ def _gather_configs(cfg: ConfigFile, topdir: PathType | None) -> list[str]:
695745def _ensure_config (configfile : ConfigFile , topdir : PathType | None ) -> str :
696746 # Ensure the given configfile exists, returning its path. May
697747 # raise permissions errors, WestNotFound, etc.
698- loc = _location (configfile , topdir = topdir )
699- path = Path (loc )
700-
748+ configs : list [str ] = _location (configfile , topdir = topdir )
749+ assert configs , 'No configfile found'
750+ assert len (configs ) == 1 , f'Multiple config files in use: { configs } '
751+ path = Path (configs [0 ])
701752 if path .is_file ():
702- return loc
753+ return os . fspath ( path )
703754
704755 path .parent .mkdir (parents = True , exist_ok = True )
705756 path .touch (exist_ok = True )
0 commit comments