Coverage for .tox/cov/lib/python3.11/site-packages/confattr/config.py: 100%

206 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-12 20:24 +0200

1#!./runmodule.sh 

2 

3import builtins 

4import enum 

5import typing 

6from collections.abc import Iterable, Iterator, Container, Sequence, Callable 

7 

8if typing.TYPE_CHECKING: 

9 from typing_extensions import Self 

10 

11from .formatters import AbstractFormatter, CopyableAbstractFormatter, Primitive, List, Set, Dict, format_primitive_value 

12 

13 

14#: An identifier to specify which value of a :class:`~confattr.config.MultiConfig` or :class:`~confattr.config.MultiDictConfig` should be used for a certain object. 

15ConfigId = typing.NewType('ConfigId', str) 

16 

17T_KEY = typing.TypeVar('T_KEY') 

18T = typing.TypeVar('T') 

19 

20 

21class Config(typing.Generic[T]): 

22 

23 ''' 

24 Each instance of this class represents a setting which can be changed in a config file. 

25 

26 This class implements the `descriptor protocol <https://docs.python.org/3/reference/datamodel.html#implementing-descriptors>`__ to return :attr:`~confattr.config.Config.value` if an instance of this class is accessed as an instance attribute. 

27 If you want to get this object you need to access it as a class attribute. 

28 ''' 

29 

30 #: A mapping of all :class:`~confattr.config.Config` instances. The key in the mapping is the :attr:`~confattr.config.Config.key` attribute. The value is the :class:`~confattr.config.Config` instance. New :class:`~confattr.config.Config` instances add themselves automatically in their constructor. 

31 instances: 'dict[str, Config[typing.Any]]' = {} 

32 

33 default_config_id = ConfigId('general') 

34 

35 #: The value of this setting. 

36 value: 'T' 

37 

38 #: Information about data type, unit and allowed values for :attr:`~confattr.config.Config.value` and methods how to parse, format and complete it. 

39 type: 'AbstractFormatter[T]' 

40 

41 #: A description of this setting or a description for each allowed value. 

42 help: 'str|dict[T, str]|None' 

43 

44 

45 def __init__(self, 

46 key: str, 

47 default: T, *, 

48 type: 'AbstractFormatter[T]|None' = None, 

49 unit: 'str|None' = None, 

50 allowed_values: 'Sequence[T]|dict[str, T]|None' = None, 

51 help: 'str|dict[T, str]|None' = None, 

52 parent: 'DictConfig[typing.Any, T]|None' = None, 

53 ): 

54 ''' 

55 :param key: The name of this setting in the config file 

56 :param default: The default value of this setting 

57 :param type: How to parse, format and complete a value. Usually this is determined automatically based on :paramref:`~confattr.config.Config.default`. But if :paramref:`~confattr.config.Config.default` is an empty list the item type cannot be determined automatically so that this argument must be passed explicitly. This also gives the possibility to format a standard type differently e.g. as :class:`~confattr.formatters.Hex`. It is not permissible to reuse the same object for different settings, otherwise :meth:`AbstractFormatter.set_config_key() <confattr.formatters.AbstractFormatter.set_config_key>` will throw an exception. 

58 :param unit: The unit of an int or float value (only if type is None) 

59 :param allowed_values: The possible values this setting can have. Values read from a config file or an environment variable are checked against this. The :paramref:`~confattr.config.Config.default` value is *not* checked. (Only if type is None.) 

60 :param help: A description of this setting 

61 :param parent: Applies only if this is part of a :class:`~confattr.config.DictConfig` 

62 

63 :obj:`~confattr.config.T` can be one of: 

64 * :class:`str` 

65 * :class:`int` 

66 * :class:`float` 

67 * :class:`bool` 

68 * a subclass of :class:`enum.Enum` (the value used in the config file is the name in lower case letters with hyphens instead of underscores) 

69 * a class where :meth:`~object.__str__` returns a string representation which can be passed to the constructor to create an equal object. \ 

70 A help which is written to the config file must be provided as a str in the class attribute :attr:`~confattr.types.AbstractType.help` or by adding it to :attr:`Primitive.help_dict <confattr.formatters.Primitive.help_dict>`. \ 

71 If that class has a str attribute :attr:`~confattr.types.AbstractType.type_name` this is used instead of the class name inside of config file. 

72 * a :class:`list` of any of the afore mentioned data types. The list may not be empty when it is passed to this constructor so that the item type can be derived but it can be emptied immediately afterwards. (The type of the items is not dynamically enforced—that's the job of a static type checker—but the type is mentioned in the help.) 

73 

74 :raises ValueError: if key is not unique 

75 :raises TypeError: if :paramref:`~confattr.config.Config.default` is an empty list/set because the first element is used to infer the data type to which a value given in a config file is converted 

76 :raises TypeError: if this setting is a number or a list of numbers and :paramref:`~confattr.config.Config.unit` is not given 

77 ''' 

78 if type is None: 

79 if isinstance(default, list): 

80 if not default: 

81 raise TypeError('I cannot infer the item type from an empty list. Please pass an argument to the type parameter.') 

82 item_type: 'builtins.type[T]' = builtins.type(default[0]) 

83 type = typing.cast('AbstractFormatter[T]', List(item_type=Primitive(item_type, allowed_values=allowed_values, unit=unit))) 

84 elif isinstance(default, set): 

85 if not default: 

86 raise TypeError('I cannot infer the item type from an empty set. Please pass an argument to the type parameter.') 

87 item_type = builtins.type(next(iter(default))) 

88 type = typing.cast('AbstractFormatter[T]', Set(item_type=Primitive(item_type, allowed_values=allowed_values, unit=unit))) 

89 elif isinstance(default, dict): 

90 if not default: 

91 raise TypeError('I cannot infer the key and value types from an empty dict. Please pass an argument to the type parameter.') 

92 some_key, some_value = next(iter(default.items())) 

93 key_type = Primitive(builtins.type(some_key)) 

94 val_type = Primitive(builtins.type(some_value), allowed_values=allowed_values, unit=unit) 

95 type = typing.cast('AbstractFormatter[T]', Dict(key_type, val_type)) 

96 else: 

97 type = Primitive(builtins.type(default), allowed_values=allowed_values, unit=unit) 

98 else: 

99 if unit is not None: 

100 raise TypeError("The keyword argument 'unit' is not supported if 'type' is given. Pass it to the type instead.") 

101 if allowed_values is not None: 

102 raise TypeError("The keyword argument 'allowed_values' is not supported if 'type' is given. Pass it to the type instead.") 

103 

104 type.set_config_key(key) 

105 

106 self._key = key 

107 self.value = default 

108 self.type = type 

109 self.help = help 

110 self.parent = parent 

111 

112 cls = builtins.type(self) 

113 if key in cls.instances: 

114 raise ValueError(f'duplicate config key {key!r}') 

115 cls.instances[key] = self 

116 

117 @property 

118 def key(self) -> str: 

119 '''The name of this setting which is used in the config file. This must be unique.''' 

120 return self._key 

121 

122 @key.setter 

123 def key(self, key: str) -> None: 

124 if key in self.instances: 

125 raise ValueError(f'duplicate config key {key!r}') 

126 del self.instances[self._key] 

127 self._key = key 

128 self.type.config_key = key 

129 self.instances[key] = self 

130 

131 

132 @typing.overload 

133 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self': 

134 pass 

135 

136 @typing.overload 

137 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> T: 

138 pass 

139 

140 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'T|Self': 

141 if instance is None: 

142 return self 

143 

144 return self.value 

145 

146 def __set__(self: 'Config[T]', instance: typing.Any, value: T) -> None: 

147 self.value = value 

148 

149 def __repr__(self) -> str: 

150 return '%s(%s, ...)' % (type(self).__name__, ', '.join(repr(a) for a in (self.key, self.value))) 

151 

152 def set_value(self: 'Config[T]', config_id: 'ConfigId|None', value: T) -> None: 

153 ''' 

154 This method is just to provide a common interface for :class:`~confattr.config.Config` and :class:`~confattr.config.MultiConfig`. 

155 If you know that you are dealing with a normal :class:`~confattr.config.Config` you can set :attr:`~confattr.config.Config.value` directly. 

156 ''' 

157 if config_id is None: 

158 config_id = self.default_config_id 

159 if config_id != self.default_config_id: 

160 raise ValueError(f'{self.key} cannot be set for specific groups, config_id must be the default {self.default_config_id!r} not {config_id!r}') 

161 self.value = value 

162 

163 def wants_to_be_exported(self) -> bool: 

164 return True 

165 

166 def get_value(self, config_id: 'ConfigId|None') -> T: 

167 ''' 

168 :return: :attr:`~confattr.config.Config.value` 

169 

170 This getter is only to have a common interface for :class:`~confattr.config.Config` and :class:`~confattr.config.MultiConfig` 

171 ''' 

172 return self.value 

173 

174class ExplicitConfig(Config[T]): 

175 

176 ''' 

177 A setting without a default value which requires the user to explicitly set a value in the config file. 

178 ''' 

179 

180 def __init__(self, 

181 key: str, 

182 type: 'AbstractFormatter[T]|type[T]|None' = None, *, 

183 unit: 'str|None' = None, 

184 allowed_values: 'Sequence[T]|dict[str, T]|None' = None, 

185 help: 'str|dict[T, str]|None' = None, 

186 parent: 'DictConfig[typing.Any, T]|None' = None, 

187 ): 

188 ''' 

189 :param key: The name of this setting in the config file 

190 :param type: How to parse, format and complete a value. Any class which can be passed to :class:`~confattr.formatters.Primitive` or an object of a subclass of :class:`~confattr.formatters.AbstractFormatter`. 

191 :param unit: The unit of an int or float value (only if type is not an :class:`~confattr.formatters.AbstractFormatter`) 

192 :param allowed_values: The possible values this setting can have. Values read from a config file or an environment variable are checked against this. The :paramref:`~confattr.config.Config.default` value is *not* checked. (Only if type is not an :class:`~confattr.formatters.AbstractFormatter`.) 

193 :param help: A description of this setting 

194 :param parent: Applies only if this is part of a :class:`~confattr.config.DictConfig` 

195 ''' 

196 if type is None: 

197 if not allowed_values: 

198 raise TypeError("missing required positional argument: 'type'") 

199 elif isinstance(allowed_values, dict): 

200 type = builtins.type(tuple(allowed_values.values())[0]) 

201 else: 

202 type = builtins.type(allowed_values[0]) 

203 if not isinstance(type, AbstractFormatter): 

204 type = Primitive(type, unit=unit, allowed_values=allowed_values) 

205 super().__init__(key, 

206 default = None, # type: ignore [arg-type] 

207 type = type, 

208 help = help, 

209 parent = parent, 

210 ) 

211 

212 @typing.overload 

213 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self': 

214 pass 

215 

216 @typing.overload 

217 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> T: 

218 pass 

219 

220 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'T|Self': 

221 if instance is None: 

222 return self 

223 

224 if self.value is None: 

225 raise TypeError(f"value for {self.key!r} has not been set") 

226 return self.value 

227 

228 

229class DictConfig(typing.Generic[T_KEY, T]): 

230 

231 ''' 

232 A container for several settings which belong together. 

233 It can be indexed like a normal :class:`dict` but internally the items are stored in :class:`~confattr.config.Config` instances. 

234 

235 In contrast to a :class:`~confattr.config.Config` instance it does *not* make a difference whether an instance of this class is accessed as a type or instance attribute. 

236 ''' 

237 

238 def __init__(self, 

239 key_prefix: str, 

240 default_values: 'dict[T_KEY, T]', *, 

241 type: 'CopyableAbstractFormatter[T]|None' = None, 

242 ignore_keys: 'Container[T_KEY]' = set(), 

243 unit: 'str|None' = None, 

244 allowed_values: 'Sequence[T]|dict[str, T]|None' = None, 

245 help: 'str|None' = None, 

246 ) -> None: 

247 ''' 

248 :param key_prefix: A common prefix which is used by :meth:`~confattr.config.DictConfig.format_key` to generate the :attr:`~confattr.config.Config.key` by which the setting is identified in the config file 

249 :param default_values: The content of this container. A :class:`~confattr.config.Config` instance is created for each of these values (except if the key is contained in :paramref:`~confattr.config.DictConfig.ignore_keys`). See :meth:`~confattr.config.DictConfig.format_key`. 

250 :param type: How to parse, format and complete a value. Usually this is determined automatically based on :paramref:`~confattr.config.DictConfig.default_values`. But if you want more control you can implement your own class and pass it to this parameter. 

251 :param ignore_keys: All items which have one of these keys are *not* stored in a :class:`~confattr.config.Config` instance, i.e. cannot be set in the config file. 

252 :param unit: The unit of all items (only if type is None) 

253 :param allowed_values: The possible values these settings can have. Values read from a config file or an environment variable are checked against this. The :paramref:`~confattr.config.DictConfig.default_values` are *not* checked. (Only if type is None.) 

254 :param help: A help for all items 

255 

256 :raises ValueError: if a key is not unique 

257 ''' 

258 self._values: 'dict[T_KEY, Config[T]]' = {} 

259 self._ignored_values: 'dict[T_KEY, T]' = {} 

260 self.allowed_values = allowed_values 

261 

262 self.key_prefix = key_prefix 

263 self.type = type 

264 self.unit = unit 

265 self.help = help 

266 self.ignore_keys = ignore_keys 

267 

268 for key, val in default_values.items(): 

269 self[key] = val 

270 

271 def format_key(self, key: T_KEY) -> str: 

272 ''' 

273 Generate a key by which the setting can be identified in the config file based on the dict key by which the value is accessed in the python code. 

274 

275 :return: :paramref:`~confattr.config.DictConfig.key_prefix` + dot + :paramref:`~confattr.config.DictConfig.format_key.key` 

276 ''' 

277 key_str = format_primitive_value(key) 

278 return '%s.%s' % (self.key_prefix, key_str) 

279 

280 def __setitem__(self: 'DictConfig[T_KEY, T]', key: T_KEY, val: T) -> None: 

281 if key in self.ignore_keys: 

282 self._ignored_values[key] = val 

283 return 

284 

285 c = self._values.get(key) 

286 if c is None: 

287 self._values[key] = self.new_config(self.format_key(key), val, unit=self.unit, help=self.help) 

288 else: 

289 c.value = val 

290 

291 def new_config(self: 'DictConfig[T_KEY, T]', key: str, default: T, *, unit: 'str|None', help: 'str|dict[T, str]|None') -> Config[T]: 

292 ''' 

293 Create a new :class:`~confattr.config.Config` instance to be used internally 

294 ''' 

295 return Config(key, default, type=self.type.copy() if self.type else None, unit=unit, help=help, parent=self, allowed_values=self.allowed_values) 

296 

297 def __getitem__(self, key: T_KEY) -> T: 

298 if key in self.ignore_keys: 

299 return self._ignored_values[key] 

300 else: 

301 return self._values[key].value 

302 

303 def get(self, key: T_KEY, default: 'T|None' = None) -> 'T|None': 

304 try: 

305 return self[key] 

306 except KeyError: 

307 return default 

308 

309 def __repr__(self) -> str: 

310 values = {key:val.value for key,val in self._values.items()} 

311 values.update({key:val for key,val in self._ignored_values.items()}) 

312 return '%s(%r, ignore_keys=%r, ...)' % (type(self).__name__, values, self.ignore_keys) 

313 

314 def __contains__(self, key: T_KEY) -> bool: 

315 if key in self.ignore_keys: 

316 return key in self._ignored_values 

317 else: 

318 return key in self._values 

319 

320 def __iter__(self) -> 'Iterator[T_KEY]': 

321 yield from self._values 

322 yield from self._ignored_values 

323 

324 def iter_keys(self) -> 'Iterator[str]': 

325 ''' 

326 Iterate over the keys by which the settings can be identified in the config file 

327 ''' 

328 for cfg in self._values.values(): 

329 yield cfg.key 

330 

331 

332# ========== settings which can have different values for different groups ========== 

333 

334class MultiConfig(Config[T]): 

335 

336 ''' 

337 A setting which can have different values for different objects. 

338 

339 This class implements the `descriptor protocol <https://docs.python.org/3/reference/datamodel.html#implementing-descriptors>`__ to return one of the values in :attr:`~confattr.config.MultiConfig.values` depending on a ``config_id`` attribute of the owning object if an instance of this class is accessed as an instance attribute. 

340 If there is no value for the ``config_id`` in :attr:`~confattr.config.MultiConfig.values` :attr:`~confattr.config.MultiConfig.value` is returned instead. 

341 If the owning instance does not have a ``config_id`` attribute an :class:`AttributeError` is raised. 

342 

343 In the config file a group can be opened with ``[config-id]``. 

344 Then all following ``set`` commands set the value for the specified config id. 

345 ''' 

346 

347 #: A list of all config ids for which a value has been set in any instance of this class (regardless of via code or in a config file and regardless of whether the value has been deleted later on). This list is cleared by :meth:`~confattr.config.MultiConfig.reset`. 

348 config_ids: 'list[ConfigId]' = [] 

349 

350 #: Stores the values for specific objects. 

351 values: 'dict[ConfigId, T]' 

352 

353 #: Stores the default value which is used if no value for the object is defined in :attr:`~confattr.config.MultiConfig.values`. 

354 value: 'T' 

355 

356 #: The callable which has been passed to the constructor as :paramref:`~confattr.config.MultiConfig.check_config_id` 

357 check_config_id: 'Callable[[MultiConfig[T], ConfigId], None]|None' 

358 

359 @classmethod 

360 def reset(cls) -> None: 

361 ''' 

362 Clear :attr:`~confattr.config.MultiConfig.config_ids` and clear :attr:`~confattr.config.MultiConfig.values` for all instances in :attr:`Config.instances <confattr.config.Config.instances>` 

363 ''' 

364 cls.config_ids.clear() 

365 for cfg in Config.instances.values(): 

366 if isinstance(cfg, MultiConfig): 

367 cfg.values.clear() 

368 

369 def __init__(self, 

370 key: str, 

371 default: T, *, 

372 type: 'AbstractFormatter[T]|None' = None, 

373 unit: 'str|None' = None, 

374 allowed_values: 'Sequence[T]|dict[str, T]|None' = None, 

375 help: 'str|dict[T, str]|None' = None, 

376 parent: 'MultiDictConfig[typing.Any, T]|None' = None, 

377 check_config_id: 'Callable[[MultiConfig[T], ConfigId], None]|None' = None, 

378 ) -> None: 

379 ''' 

380 :param key: The name of this setting in the config file 

381 :param default: The default value of this setting 

382 :param help: A description of this setting 

383 :param type: How to parse, format and complete a value. Usually this is determined automatically based on :paramref:`~confattr.config.MultiConfig.default`. But if :paramref:`~confattr.config.MultiConfig.default` is an empty list the item type cannot be determined automatically so that this argument must be passed explicitly. This also gives the possibility to format a standard type differently e.g. as :class:`~confattr.formatters.Hex`. It is not permissible to reuse the same object for different settings, otherwise :meth:`AbstractFormatter.set_config_key() <confattr.formatters.AbstractFormatter.set_config_key>` will throw an exception. 

384 :param unit: The unit of an int or float value (only if type is None) 

385 :param allowed_values: The possible values this setting can have. Values read from a config file or an environment variable are checked against this. The :paramref:`~confattr.config.MultiConfig.default` value is *not* checked. (Only if type is None.) 

386 :param parent: Applies only if this is part of a :class:`~confattr.config.MultiDictConfig` 

387 :param check_config_id: Is called every time a value is set in the config file (except if the config id is :attr:`~confattr.config.Config.default_config_id`—that is always allowed). The callback should raise a :class:`~confattr.configfile.ParseException` if the config id is invalid. 

388 ''' 

389 super().__init__(key, default, type=type, unit=unit, help=help, parent=parent, allowed_values=allowed_values) 

390 self.values: 'dict[ConfigId, T]' = {} 

391 self.check_config_id = check_config_id 

392 

393 # I don't know why this code duplication is necessary, 

394 # I have declared the overloads in the parent class already. 

395 # But without copy-pasting this code mypy complains 

396 # "Signature of __get__ incompatible with supertype Config" 

397 @typing.overload 

398 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self': 

399 pass 

400 

401 @typing.overload 

402 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> T: 

403 pass 

404 

405 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'T|Self': 

406 if instance is None: 

407 return self 

408 

409 return self.values.get(instance.config_id, self.value) 

410 

411 def __set__(self: 'MultiConfig[T]', instance: typing.Any, value: T) -> None: 

412 config_id = instance.config_id 

413 self.values[config_id] = value 

414 if config_id not in self.config_ids: 

415 self.config_ids.append(config_id) 

416 

417 def set_value(self: 'MultiConfig[T]', config_id: 'ConfigId|None', value: T) -> None: 

418 ''' 

419 Check :paramref:`~confattr.config.MultiConfig.set_value.config_id` by calling :meth:`~confattr.config.MultiConfig.check_config_id` and 

420 set the value for the object(s) identified by :paramref:`~confattr.config.MultiConfig.set_value.config_id`. 

421 

422 If you know that :paramref:`~confattr.config.MultiConfig.set_value.config_id` is valid you can also change the items of :attr:`~confattr.config.MultiConfig.values` directly. 

423 That is especially useful in test automation with :meth:`pytest.MonkeyPatch.setitem`. 

424 

425 If you want to set the default value you can also set :attr:`~confattr.config.MultiConfig.value` directly. 

426 

427 :param config_id: Identifies the object(s) for which :paramref:`~confattr.config.MultiConfig.set_value.value` is intended. :obj:`None` is equivalent to :attr:`~confattr.config.MultiConfig.default_config_id`. 

428 :param value: The value to be assigned for the object(s) identified by :paramref:`~confattr.config.MultiConfig.set_value.config_id`. 

429 ''' 

430 if config_id is None: 

431 config_id = self.default_config_id 

432 if self.check_config_id and config_id != self.default_config_id: 

433 self.check_config_id(self, config_id) 

434 if config_id == self.default_config_id: 

435 self.value = value 

436 else: 

437 self.values[config_id] = value 

438 if config_id not in self.config_ids: 

439 self.config_ids.append(config_id) 

440 

441 def get_value(self, config_id: 'ConfigId|None') -> T: 

442 ''' 

443 :return: The corresponding value from :attr:`~confattr.config.MultiConfig.values` if :paramref:`~confattr.config.MultiConfig.get_value.config_id` is contained or :attr:`~confattr.config.MultiConfig.value` otherwise 

444 ''' 

445 if config_id is None: 

446 config_id = self.default_config_id 

447 return self.values.get(config_id, self.value) 

448 

449 

450class MultiDictConfig(DictConfig[T_KEY, T]): 

451 

452 ''' 

453 A container for several settings which can have different values for different objects. 

454 

455 This is essentially a :class:`~confattr.config.DictConfig` using :class:`~confattr.config.MultiConfig` instead of normal :class:`~confattr.config.Config`. 

456 However, in order to return different values depending on the ``config_id`` of the owning instance, it implements the `descriptor protocol <https://docs.python.org/3/reference/datamodel.html#implementing-descriptors>`__ to return an :class:`~confattr.config.InstanceSpecificDictMultiConfig` if it is accessed as an instance attribute. 

457 ''' 

458 

459 def __init__(self, 

460 key_prefix: str, 

461 default_values: 'dict[T_KEY, T]', *, 

462 type: 'CopyableAbstractFormatter[T]|None' = None, 

463 ignore_keys: 'Container[T_KEY]' = set(), 

464 unit: 'str|None' = None, 

465 allowed_values: 'Sequence[T]|dict[str, T]|None' = None, 

466 help: 'str|None' = None, 

467 check_config_id: 'Callable[[MultiConfig[T], ConfigId], None]|None' = None, 

468 ) -> None: 

469 ''' 

470 :param key_prefix: A common prefix which is used by :meth:`~confattr.config.MultiDictConfig.format_key` to generate the :attr:`~confattr.config.Config.key` by which the setting is identified in the config file 

471 :param default_values: The content of this container. A :class:`~confattr.config.Config` instance is created for each of these values (except if the key is contained in :paramref:`~confattr.config.MultiDictConfig.ignore_keys`). See :meth:`~confattr.config.MultiDictConfig.format_key`. 

472 :param type: How to parse, format and complete a value. Usually this is determined automatically based on :paramref:`~confattr.config.MultiDictConfig.default_values`. But if you want more control you can implement your own class and pass it to this parameter. 

473 :param ignore_keys: All items which have one of these keys are *not* stored in a :class:`~confattr.config.Config` instance, i.e. cannot be set in the config file. 

474 :param unit: The unit of all items (only if type is None) 

475 :param allowed_values: The possible values these settings can have. Values read from a config file or an environment variable are checked against this. The :paramref:`~confattr.config.MultiDictConfig.default_values` are *not* checked. (Only if type is None.) 

476 :param help: A help for all items 

477 :param check_config_id: Is passed through to :class:`~confattr.config.MultiConfig` 

478 

479 :raises ValueError: if a key is not unique 

480 ''' 

481 self.check_config_id = check_config_id 

482 super().__init__( 

483 key_prefix = key_prefix, 

484 default_values = default_values, 

485 type = type, 

486 ignore_keys = ignore_keys, 

487 unit = unit, 

488 help = help, 

489 allowed_values = allowed_values, 

490 ) 

491 

492 @typing.overload 

493 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self': 

494 pass 

495 

496 @typing.overload 

497 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'InstanceSpecificDictMultiConfig[T_KEY, T]': 

498 pass 

499 

500 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'InstanceSpecificDictMultiConfig[T_KEY, T]|Self': 

501 if instance is None: 

502 return self 

503 

504 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

505 

506 def __set__(self: 'MultiDictConfig[T_KEY, T]', instance: typing.Any, value: 'InstanceSpecificDictMultiConfig[T_KEY, T]') -> typing.NoReturn: 

507 raise NotImplementedError() 

508 

509 def new_config(self: 'MultiDictConfig[T_KEY, T]', key: str, default: T, *, unit: 'str|None', help: 'str|dict[T, str]|None') -> MultiConfig[T]: 

510 return MultiConfig(key, default, type=self.type.copy() if self.type else None, unit=unit, help=help, parent=self, allowed_values=self.allowed_values, check_config_id=self.check_config_id) 

511 

512class InstanceSpecificDictMultiConfig(typing.Generic[T_KEY, T]): 

513 

514 ''' 

515 An intermediate instance which is returned when accsessing 

516 a :class:`~confattr.config.MultiDictConfig` as an instance attribute. 

517 Can be indexed like a normal :class:`dict`. 

518 ''' 

519 

520 def __init__(self, mdc: 'MultiDictConfig[T_KEY, T]', config_id: ConfigId) -> None: 

521 self.mdc = mdc 

522 self.config_id = config_id 

523 

524 def __setitem__(self: 'InstanceSpecificDictMultiConfig[T_KEY, T]', key: T_KEY, val: T) -> None: 

525 if key in self.mdc.ignore_keys: 

526 raise TypeError('cannot set value of ignored key %r' % key) 

527 

528 c = self.mdc._values.get(key) 

529 if c is None: 

530 self.mdc._values[key] = MultiConfig(self.mdc.format_key(key), val, help=self.mdc.help) 

531 else: 

532 c.__set__(self, val) 

533 

534 def __getitem__(self, key: T_KEY) -> T: 

535 if key in self.mdc.ignore_keys: 

536 return self.mdc._ignored_values[key] 

537 else: 

538 return self.mdc._values[key].__get__(self)