4444from pathlib import Path , PureWindowsPath
4545from typing import TYPE_CHECKING , Any
4646
47- from west .util import WEST_DIR , PathType , WestNotFound , west_dir
47+ from west .util import WEST_DIR , PathType , WestNotFound , paths_to_str , str_to_paths , west_dir
4848
4949
5050class MalformedConfig (Exception ):
@@ -70,19 +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+ paths = paths or []
75+ if not paths :
76+ return None
77+ paths = [p for p in paths if p .exists ()]
78+ return _InternalCF (paths ) if paths else None
79+
80+ def __init__ (self , paths : list [Path ]):
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+ raise WestNotFound (f"Error while reading one of '{ paths_to_str (paths )} '" )
86+
87+ def _write (self ):
88+ if not self .paths :
89+ raise WestNotFound ('No config file exists that can be written' )
90+ if len (self .paths ) > 1 :
91+ raise ValueError (f'Cannot write if multiple configs in use: { paths_to_str (self .paths )} ' )
92+ with open (self .paths [0 ], 'w' , encoding = 'utf-8' ) as f :
93+ self .cp .write (f )
8294
8395 def __contains__ (self , option : str ) -> bool :
8496 section , key = _InternalCF .parse_key (option )
85-
8697 return section in self .cp and key in self .cp [section ]
8798
8899 def get (self , option : str ):
@@ -99,35 +110,28 @@ def getfloat(self, option: str):
99110
100111 def _get (self , option , getter ):
101112 section , key = _InternalCF .parse_key (option )
102-
103113 try :
104114 return getter (section , key )
105115 except (configparser .NoOptionError , configparser .NoSectionError ) as err :
106116 raise KeyError (option ) from err
107117
108118 def set (self , option : str , value : Any ):
109119 section , key = _InternalCF .parse_key (option )
110-
111120 if section not in self .cp :
112121 self .cp [section ] = {}
113-
114122 self .cp [section ][key ] = value
115-
116- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
117- self .cp .write (f )
123+ self ._write ()
118124
119125 def delete (self , option : str ):
120126 section , key = _InternalCF .parse_key (option )
121-
122- if section not in self .cp :
127+ if option not in self :
123128 raise KeyError (option )
124129
125130 del self .cp [section ][key ]
126131 if not self .cp [section ].items ():
127132 del self .cp [section ]
128133
129- with open (self .path , 'w' , encoding = 'utf-8' ) as f :
130- self .cp .write (f )
134+ self ._write ()
131135
132136
133137class ConfigFile (Enum ):
@@ -173,24 +177,26 @@ def __init__(self, topdir: PathType | None = None):
173177 :param topdir: workspace location; may be None
174178 '''
175179
176- local_path = _location (ConfigFile .LOCAL , topdir = topdir , find_local = False ) or None
180+ local_paths = _location (ConfigFile .LOCAL , topdir = topdir , find_local = False ) or None
177181
178- self ._system_path = Path (_location (ConfigFile .SYSTEM ))
179- self ._global_path = Path (_location (ConfigFile .GLOBAL ))
180- self ._local_path = Path ( local_path ) if local_path is not None else None
182+ self ._system_paths = str_to_paths (_location (ConfigFile .SYSTEM ))
183+ self ._global_paths = str_to_paths (_location (ConfigFile .GLOBAL ))
184+ self ._local_paths = str_to_paths ( local_paths )
181185
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 )
186+ self ._system = _InternalCF .from_paths (self ._system_paths )
187+ self ._global = _InternalCF .from_paths (self ._global_paths )
188+ self ._local = _InternalCF .from_paths (self ._local_paths )
185189
186- def get_paths (self , location : ConfigFile = ConfigFile .ALL ) :
190+ def get_paths (self , location : ConfigFile = ConfigFile .ALL , existing_only = False ) -> list [ Path ] :
187191 ret = []
188- if self ._global and location in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
189- ret .append (self ._global .path )
190- if self ._system and location in [ConfigFile .SYSTEM , ConfigFile .ALL ]:
191- ret .append (self ._system .path )
192- if self ._local and location in [ConfigFile .LOCAL , ConfigFile .ALL ]:
193- ret .append (self ._local .path )
192+ if location in [ConfigFile .GLOBAL , ConfigFile .ALL ]:
193+ ret .extend (self ._global_paths )
194+ if location in [ConfigFile .SYSTEM , ConfigFile .ALL ]:
195+ ret .extend (self ._system_paths )
196+ if location in [ConfigFile .LOCAL , ConfigFile .ALL ]:
197+ ret .extend (self ._local_paths )
198+ if existing_only :
199+ ret = [p for p in ret if p .exists ()]
194200 return ret
195201
196202 def get (
@@ -283,28 +289,45 @@ def set(self, option: str, value: Any, configfile: ConfigFile = ConfigFile.LOCAL
283289 :param configfile: type of config file to set the value in
284290 '''
285291
292+ def check_configfile (location : ConfigFile ) -> Path :
293+ '''
294+ check that exactly one configfile is in use (even if it not exists yet).
295+ Return its path.
296+ '''
297+ configs = self .get_paths (location )
298+ if not configs :
299+ raise WestNotFound (f'{ configfile } : Cannot determine any config file' )
300+ if len (configs ) > 1 :
301+ raise ValueError (
302+ f'Cannot set value if multiple configs in use: { paths_to_str (configs )} '
303+ )
304+ return configs [0 ]
305+
286306 if configfile == ConfigFile .ALL :
287307 # We need a real configuration file; ALL doesn't make sense here.
288308 raise ValueError (configfile )
289309 elif configfile == ConfigFile .LOCAL :
290- if self ._local_path is None :
310+ if not self ._local_paths :
291311 raise ValueError (
292312 f'{ configfile } : file not found; retry in a workspace or set WEST_CONFIG_LOCAL'
293313 )
294- if not self ._local_path .exists ():
295- self ._local = self ._create (self ._local_path )
314+ config_file = check_configfile (configfile )
315+ if not config_file .exists ():
316+ self ._local = self ._create (config_file )
296317 if TYPE_CHECKING :
297318 assert self ._local
298319 self ._local .set (option , value )
299320 elif configfile == ConfigFile .GLOBAL :
300- if not self ._global_path .exists ():
301- self ._global = self ._create (self ._global_path )
321+ config_file = check_configfile (configfile )
322+ if not config_file .exists ():
323+ self ._global = self ._create (config_file )
302324 if TYPE_CHECKING :
303325 assert self ._global
304326 self ._global .set (option , value )
305327 elif configfile == ConfigFile .SYSTEM :
306- if not self ._system_path .exists ():
307- self ._system = self ._create (self ._system_path )
328+ config_file = check_configfile (configfile )
329+ if not config_file .exists ():
330+ self ._system = self ._create (config_file )
308331 if TYPE_CHECKING :
309332 assert self ._system
310333 self ._system .set (option , value )
@@ -316,7 +339,7 @@ def set(self, option: str, value: Any, configfile: ConfigFile = ConfigFile.LOCAL
316339 def _create (path : Path ) -> _InternalCF :
317340 path .parent .mkdir (parents = True , exist_ok = True )
318341 path .touch (exist_ok = True )
319- ret = _InternalCF .from_path ( path )
342+ ret = _InternalCF .from_paths ([ path ] )
320343 if TYPE_CHECKING :
321344 assert ret
322345 return ret
@@ -554,18 +577,27 @@ def delete_config(
554577 _deprecated ('delete_config' )
555578
556579 stop = False
580+ considered_locations = []
557581 if configfile is None :
558- to_check = [_location ( x , topdir = topdir ) for x in [ ConfigFile .LOCAL , ConfigFile .GLOBAL ] ]
582+ considered_locations = [ConfigFile .LOCAL , ConfigFile .GLOBAL ]
559583 stop = True
560584 elif configfile == ConfigFile .ALL :
561- to_check = [
562- _location (x , topdir = topdir )
563- for x in [ConfigFile .SYSTEM , ConfigFile .GLOBAL , ConfigFile .LOCAL ]
564- ]
585+ considered_locations = [ConfigFile .SYSTEM , ConfigFile .GLOBAL , ConfigFile .LOCAL ]
565586 elif isinstance (configfile , ConfigFile ):
566- to_check = [_location ( configfile , topdir = topdir ) ]
587+ considered_locations = [configfile ]
567588 else :
568- to_check = [_location (x , topdir = topdir ) for x in configfile ]
589+ considered_locations = configfile
590+
591+ # ensure that only one config file is in use
592+ config_files = [str_to_paths (_location (cfg , topdir = topdir )) for cfg in considered_locations ]
593+ for config_file_list in config_files :
594+ if len (config_file_list ) > 1 :
595+ raise ValueError (
596+ f'Error: Multiple config paths in use: { paths_to_str (config_file_list )} '
597+ )
598+
599+ # determine config file paths
600+ to_check = [c for config_file_list in config_files for c in config_file_list ]
569601
570602 found = False
571603 for path in to_check :
@@ -659,32 +691,37 @@ def _location(cfg: ConfigFile, topdir: PathType | None = None, find_local: bool
659691 raise ValueError (f'invalid configuration file { cfg } ' )
660692
661693
662- def _gather_configs (cfg : ConfigFile , topdir : PathType | None ) -> list [str ]:
694+ def _gather_configs (cfg : ConfigFile , topdir : PathType | None ) -> list [Path ]:
663695 # Find the paths to the given configuration files, in increasing
664696 # precedence order.
665- ret = []
697+ ret : list [ Path ] = []
666698
667699 if cfg == ConfigFile .ALL or cfg == ConfigFile .SYSTEM :
668- ret .append (_location (ConfigFile .SYSTEM , topdir = topdir ))
700+ paths = str_to_paths (_location (ConfigFile .SYSTEM , topdir = topdir ))
701+ ret .extend (paths )
669702 if cfg == ConfigFile .ALL or cfg == ConfigFile .GLOBAL :
670- ret .append (_location (ConfigFile .GLOBAL , topdir = topdir ))
703+ paths = str_to_paths (_location (ConfigFile .GLOBAL , topdir = topdir ))
704+ ret .extend (paths )
671705 if cfg == ConfigFile .ALL or cfg == ConfigFile .LOCAL :
672706 try :
673- ret .append (_location (ConfigFile .LOCAL , topdir = topdir ))
707+ paths = str_to_paths (_location (ConfigFile .LOCAL , topdir = topdir ))
708+ ret .extend (paths )
674709 except WestNotFound :
675710 pass
676-
677711 return ret
678712
679713
680714def _ensure_config (configfile : ConfigFile , topdir : PathType | None ) -> str :
681715 # Ensure the given configfile exists, returning its path. May
682716 # raise permissions errors, WestNotFound, etc.
683- loc = _location (configfile , topdir = topdir )
684- path = Path (loc )
685-
717+ config_file = str_to_paths (_location (configfile , topdir = topdir ))
718+ if not config_file :
719+ raise ValueError (f"no config found for configfile '{ configfile } '" )
720+ if len (config_file ) > 1 :
721+ raise ValueError (f'Cannot write if multiple configs in use: { paths_to_str (config_file )} ' )
722+ path : Path = config_file [0 ]
686723 if path .is_file ():
687- return loc
724+ return os . fspath ( path )
688725
689726 path .parent .mkdir (parents = True , exist_ok = True )
690727 path .touch (exist_ok = True )
0 commit comments