Skip to content

Commit b9ee7fc

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 eac7ccc commit b9ee7fc

File tree

3 files changed

+307
-58
lines changed

3 files changed

+307
-58
lines changed

src/west/app/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@
8282
To delete <name> everywhere it's set, including the system file:
8383
west config -D <name>
8484
85+
For each configuration type (local, global, and system), an additional
86+
drop-in config directory is supported. This directory is named as the
87+
according config file, but with a '.d' suffix, whereby all '.conf' and
88+
'.ini' files are loaded in alphabetical order.
89+
90+
All files inside a drop-in directory must use `.conf` extension and are
91+
loaded in **alphabetical order**.
92+
For example:
93+
.west/config.d/basics.conf
94+
95+
Note: It is not possible to modify dropin configs.via 'west config' commands.
96+
When config option values are set/appended/deleted via 'west config' commands,
97+
always the config file is modified (never the dropin config files).
98+
8599
To list the configuration files that are loaded (both the main config file
86100
and all drop-ins) in the exact order they were applied (where later values
87101
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,22 +216,22 @@ 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
if not self._local_path:
189222
raise MalformedConfig('local configuration cannot be determined')
190223
return self._local_path
191-
elif configfile == ConfigFile.SYSTEM:
192-
return self._system_path
193224
elif configfile == ConfigFile.GLOBAL:
194225
return self._global_path
226+
elif configfile == ConfigFile.SYSTEM:
227+
return self._system_path
195228

196229
def get_paths(self, configfile: ConfigFile = ConfigFile.ALL):
197230
ret = []
198-
if self._global and configfile in [ConfigFile.GLOBAL, ConfigFile.ALL]:
199-
ret += self._global._paths()
200231
if self._system and configfile in [ConfigFile.SYSTEM, ConfigFile.ALL]:
201232
ret += self._system._paths()
233+
if self._global and configfile in [ConfigFile.GLOBAL, ConfigFile.ALL]:
234+
ret += self._global._paths()
202235
if self._local and configfile in [ConfigFile.LOCAL, ConfigFile.ALL]:
203236
ret += self._local._paths()
204237
return ret
@@ -376,13 +409,14 @@ def _copy_to_configparser(self, cp: configparser.ConfigParser) -> None:
376409
# function-and-global-state APIs.
377410

378411
def load(cf: _InternalCF):
379-
for section, contents in cf.cp.items():
380-
if section == 'DEFAULT':
381-
continue
382-
if section not in cp:
383-
cp.add_section(section)
384-
for key, value in contents.items():
385-
cp[section][key] = value
412+
for cp in [cf.dropin_cp, cf.cp]:
413+
for section, contents in cp.items():
414+
if section == 'DEFAULT':
415+
continue
416+
if section not in cp:
417+
cp.add_section(section)
418+
for key, value in contents.items():
419+
cp[section][key] = value
386420

387421
if self._system:
388422
load(self._system)
@@ -428,11 +462,12 @@ def _cf_to_dict(cf: _InternalCF | None) -> dict[str, Any]:
428462
ret: dict[str, Any] = {}
429463
if cf is None:
430464
return ret
431-
for section, contents in cf.cp.items():
432-
if section == 'DEFAULT':
433-
continue
434-
for key, value in contents.items():
435-
ret[f'{section}.{key}'] = value
465+
for cp in [cf.dropin_cp, cf.cp]:
466+
for section, contents in cp.items():
467+
if section == 'DEFAULT':
468+
continue
469+
for key, value in contents.items():
470+
ret[f'{section}.{key}'] = value
436471
return ret
437472

438473

0 commit comments

Comments
 (0)