Skip to content

Commit f624d2f

Browse files
support config dropin directories
config files in acccording dropin directories are considered automatically. The dropin directory is named as the config itself but with a '.d' suffix. For example for local config the dropin directory is './west/config.d' and for global config it is '~/.westconfig.d' support config dropin directories config files in acccording dropin directories are considered automatically. The dropin directory is named as the config itself but with a '.d' suffix. For example for local config the dropin directory is './west/config.d' and for global config it is '~/.westconfig.d' Boolean option `config.dropins` must be set to `true` so that dropin configs for the according config type are considered. So if the option config.dropins=true in enabled in local config, only local dropin configs are used (but no system or global ones).
1 parent 1faa42c commit f624d2f

File tree

3 files changed

+307
-60
lines changed

3 files changed

+307
-60
lines changed

src/west/app/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@
7474
To delete <name> everywhere it's set, including the system file:
7575
west config -D <name>
7676
77+
For each configuration type (local, global, and system), an additional
78+
drop-in config directory is supported. This directory is named as the
79+
according config file, but with a '.d' suffix, whereby all '.conf' and
80+
'.ini' files are loaded in alphabetical order.
81+
82+
All files inside a drop-in directory must use `.conf` extension and are
83+
loaded in **alphabetical order**.
84+
For example:
85+
.west/config.d/basics.conf
86+
87+
Note: It is not possible to modify dropin configs.via 'west config' commands.
88+
When config option values are set/appended/deleted via 'west config' commands,
89+
always the config file is modified (never the dropin config files).
90+
7791
To list the configuration files that are loaded (both the main config file
7892
and all drop-ins) in the exact order they were applied (where later values
7993
override earlier ones):

src/west/configuration.py

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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

131164
class ConfigFile(Enum):
@@ -183,20 +216,20 @@ 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
189-
elif configfile == ConfigFile.SYSTEM:
190-
return self._system_path
191222
elif configfile == ConfigFile.GLOBAL:
192223
return self._global_path
224+
elif configfile == ConfigFile.SYSTEM:
225+
return self._system_path
193226

194227
def get_paths(self, configfile: ConfigFile = ConfigFile.ALL):
195228
ret = []
196-
if self._global and configfile in [ConfigFile.GLOBAL, ConfigFile.ALL]:
197-
ret += self._global._paths()
198229
if self._system and configfile in [ConfigFile.SYSTEM, ConfigFile.ALL]:
199230
ret += self._system._paths()
231+
if self._global and configfile in [ConfigFile.GLOBAL, ConfigFile.ALL]:
232+
ret += self._global._paths()
200233
if self._local and configfile in [ConfigFile.LOCAL, ConfigFile.ALL]:
201234
ret += self._local._paths()
202235
return ret
@@ -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

Comments
 (0)