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

216 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-14 08:57 +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 _key_changer: 'list[Callable[[str], str]]' = [] 

46 

47 @classmethod 

48 def push_key_changer(cls, callback: 'Callable[[str], str]') -> None: 

49 ''' 

50 Modify the key of all settings which will be defined after calling this method. 

51 

52 Call this before an ``import`` and :meth:`pop_key_changer` after it if you are unhappy with the keys of a third party library. 

53 If you import that library in different modules make sure you do this at the import which is executed first. 

54 

55 :param callback: A function which takes the key as argument and returns the modified key. 

56 ''' 

57 cls._key_changer.append(callback) 

58 

59 @classmethod 

60 def pop_key_changer(cls) -> 'Callable[[str], str]': 

61 ''' 

62 Undo the last call to :meth:`push_key_changer`. 

63 ''' 

64 return cls._key_changer.pop() 

65 

66 

67 def __init__(self, 

68 key: str, 

69 default: T, *, 

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

71 unit: 'str|None' = None, 

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

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

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

75 ): 

76 ''' 

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

78 :param default: The default value of this setting 

79 :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. 

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

81 :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.) 

82 :param help: A description of this setting 

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

84 

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

86 * :class:`str` 

87 * :class:`int` 

88 * :class:`float` 

89 * :class:`bool` 

90 * 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) 

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

92 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>`. \ 

93 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. 

94 * 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.) 

95 

96 :raises ValueError: if key is not unique 

97 :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 

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

99 ''' 

100 if self._key_changer: 

101 key = self._key_changer[-1](key) 

102 

103 if type is None: 

104 if isinstance(default, list): 

105 if not default: 

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

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

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

109 elif isinstance(default, set): 

110 if not default: 

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

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

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

114 elif isinstance(default, dict): 

115 if not default: 

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

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

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

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

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

121 else: 

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

123 else: 

124 if unit is not None: 

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

126 if allowed_values is not None: 

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

128 

129 type.set_config_key(key) 

130 

131 self._key = key 

132 self.value = default 

133 self.type = type 

134 self.help = help 

135 self.parent = parent 

136 

137 cls = builtins.type(self) 

138 if key in cls.instances: 

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

140 cls.instances[key] = self 

141 

142 @property 

143 def key(self) -> str: 

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

145 return self._key 

146 

147 @key.setter 

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

149 if key in self.instances: 

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

151 del self.instances[self._key] 

152 self._key = key 

153 self.type.config_key = key 

154 self.instances[key] = self 

155 

156 

157 @typing.overload 

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

159 pass 

160 

161 @typing.overload 

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

163 pass 

164 

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

166 if instance is None: 

167 return self 

168 

169 return self.value 

170 

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

172 self.value = value 

173 

174 def __repr__(self) -> str: 

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

176 

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

178 ''' 

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

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

181 ''' 

182 if config_id is None: 

183 config_id = self.default_config_id 

184 if config_id != self.default_config_id: 

185 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}') 

186 self.value = value 

187 

188 def wants_to_be_exported(self) -> bool: 

189 return True 

190 

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

192 ''' 

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

194 

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

196 ''' 

197 return self.value 

198 

199class ExplicitConfig(Config[T]): 

200 

201 ''' 

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

203 ''' 

204 

205 def __init__(self, 

206 key: str, 

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

208 unit: 'str|None' = None, 

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

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

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

212 ): 

213 ''' 

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

215 :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`. 

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

217 :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`.) 

218 :param help: A description of this setting 

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

220 ''' 

221 if type is None: 

222 if not allowed_values: 

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

224 elif isinstance(allowed_values, dict): 

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

226 else: 

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

228 if not isinstance(type, AbstractFormatter): 

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

230 super().__init__(key, 

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

232 type = type, 

233 help = help, 

234 parent = parent, 

235 ) 

236 

237 @typing.overload 

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

239 pass 

240 

241 @typing.overload 

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

243 pass 

244 

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

246 if instance is None: 

247 return self 

248 

249 if self.value is None: 

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

251 return self.value 

252 

253 

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

255 

256 ''' 

257 A container for several settings which belong together. 

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

259 

260 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. 

261 ''' 

262 

263 def __init__(self, 

264 key_prefix: str, 

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

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

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

268 unit: 'str|None' = None, 

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

270 help: 'str|None' = None, 

271 ) -> None: 

272 ''' 

273 :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 

274 :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`. 

275 :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. 

276 :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. 

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

278 :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.) 

279 :param help: A help for all items 

280 

281 :raises ValueError: if a key is not unique 

282 ''' 

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

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

285 self.allowed_values = allowed_values 

286 

287 self.key_prefix = key_prefix 

288 self.key_changer = Config._key_changer[-1] if Config._key_changer else lambda key: key 

289 self.type = type 

290 self.unit = unit 

291 self.help = help 

292 self.ignore_keys = ignore_keys 

293 

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

295 self[key] = val 

296 

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

298 ''' 

299 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. 

300 

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

302 ''' 

303 key_str = format_primitive_value(key) 

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

305 

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

307 if key in self.ignore_keys: 

308 self._ignored_values[key] = val 

309 return 

310 

311 c = self._values.get(key) 

312 if c is None: 

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

314 else: 

315 c.value = val 

316 

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

318 ''' 

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

320 ''' 

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

322 

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

324 if key in self.ignore_keys: 

325 return self._ignored_values[key] 

326 else: 

327 return self._values[key].value 

328 

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

330 try: 

331 return self[key] 

332 except KeyError: 

333 return default 

334 

335 def __repr__(self) -> str: 

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

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

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

339 

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

341 if key in self.ignore_keys: 

342 return key in self._ignored_values 

343 else: 

344 return key in self._values 

345 

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

347 yield from self._values 

348 yield from self._ignored_values 

349 

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

351 ''' 

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

353 ''' 

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

355 yield cfg.key 

356 

357 

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

359 

360class MultiConfig(Config[T]): 

361 

362 ''' 

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

364 

365 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. 

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

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

368 

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

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

371 ''' 

372 

373 #: 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`. 

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

375 

376 #: Stores the values for specific objects. 

377 values: 'dict[ConfigId, T]' 

378 

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

380 value: 'T' 

381 

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

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

384 

385 @classmethod 

386 def reset(cls) -> None: 

387 ''' 

388 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>` 

389 ''' 

390 cls.config_ids.clear() 

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

392 if isinstance(cfg, MultiConfig): 

393 cfg.values.clear() 

394 

395 def __init__(self, 

396 key: str, 

397 default: T, *, 

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

399 unit: 'str|None' = None, 

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

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

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

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

404 ) -> None: 

405 ''' 

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

407 :param default: The default value of this setting 

408 :param help: A description of this setting 

409 :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. 

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

411 :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.) 

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

413 :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. 

414 ''' 

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

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

417 self.check_config_id = check_config_id 

418 

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

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

421 # But without copy-pasting this code mypy complains 

422 # "Signature of __get__ incompatible with supertype Config" 

423 @typing.overload 

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

425 pass 

426 

427 @typing.overload 

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

429 pass 

430 

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

432 if instance is None: 

433 return self 

434 

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

436 

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

438 config_id = instance.config_id 

439 self.values[config_id] = value 

440 if config_id not in self.config_ids: 

441 self.config_ids.append(config_id) 

442 

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

444 ''' 

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

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

447 

448 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. 

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

450 

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

452 

453 :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`. 

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

455 ''' 

456 if config_id is None: 

457 config_id = self.default_config_id 

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

459 self.check_config_id(self, config_id) 

460 if config_id == self.default_config_id: 

461 self.value = value 

462 else: 

463 self.values[config_id] = value 

464 if config_id not in self.config_ids: 

465 self.config_ids.append(config_id) 

466 

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

468 ''' 

469 :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 

470 ''' 

471 if config_id is None: 

472 config_id = self.default_config_id 

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

474 

475 

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

477 

478 ''' 

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

480 

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

482 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. 

483 ''' 

484 

485 def __init__(self, 

486 key_prefix: str, 

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

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

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

490 unit: 'str|None' = None, 

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

492 help: 'str|None' = None, 

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

494 ) -> None: 

495 ''' 

496 :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 

497 :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`. 

498 :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. 

499 :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. 

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

501 :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.) 

502 :param help: A help for all items 

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

504 

505 :raises ValueError: if a key is not unique 

506 ''' 

507 self.check_config_id = check_config_id 

508 super().__init__( 

509 key_prefix = key_prefix, 

510 default_values = default_values, 

511 type = type, 

512 ignore_keys = ignore_keys, 

513 unit = unit, 

514 help = help, 

515 allowed_values = allowed_values, 

516 ) 

517 

518 @typing.overload 

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

520 pass 

521 

522 @typing.overload 

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

524 pass 

525 

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

527 if instance is None: 

528 return self 

529 

530 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

531 

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

533 raise NotImplementedError() 

534 

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

536 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) 

537 

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

539 

540 ''' 

541 An intermediate instance which is returned when accsessing 

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

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

544 ''' 

545 

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

547 self.mdc = mdc 

548 self.config_id = config_id 

549 

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

551 if key in self.mdc.ignore_keys: 

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

553 

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

555 if c is None: 

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

557 else: 

558 c.__set__(self, val) 

559 

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

561 if key in self.mdc.ignore_keys: 

562 return self.mdc._ignored_values[key] 

563 else: 

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