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

280 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-06 19:52 +0100

1#!./runmodule.sh 

2 

3import enum 

4import typing 

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

6 

7 

8VALUE_TRUE = 'true' 

9VALUE_FALSE = 'false' 

10VALUE_NONE = 'none' 

11VALUE_AUTO = 'auto' 

12 

13TYPES_REQUIRING_UNIT = {int, float} 

14CONTAINER_TYPES = {list} 

15 

16 

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

18 

19T_co = typing.TypeVar('T_co', covariant=True) 

20T_KEY = typing.TypeVar('T_KEY') 

21T = typing.TypeVar('T') 

22 

23 

24class Config(typing.Generic[T_co]): 

25 

26 ''' 

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

28 

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

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

31 ''' 

32 

33 _Self = typing.TypeVar('_Self', bound='Config[T_co]') 

34 

35 LIST_SEP = ',' 

36 

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

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

39 

40 default_config_id = ConfigId('general') 

41 

42 #: The value of this setting. 

43 value: 'T_co' 

44 

45 #: The unit of :attr:`value` if :attr:`value` is a number. 

46 unit: 'str|None' 

47 

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

49 help: 'str|dict[T_co, str]|None' 

50 

51 #: The values which are allowed for this setting. Trying to set this setting to a different value in the config file is considered an error. If you set this setting in the program the value is *not* checked. 

52 allowed_values: 'Sequence[T_co]|None' 

53 

54 def __init__(self, 

55 key: str, 

56 default: T_co, *, 

57 help: 'str|dict[T_co, str]|None' = None, 

58 unit: 'str|None' = None, 

59 parent: 'DictConfig[typing.Any, T_co]|None' = None, 

60 allowed_values: 'Sequence[T_co]|None' = None, 

61 ): 

62 ''' 

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

64 :param default: The default value of this setting 

65 :param help: A description of this setting 

66 :param unit: The unit of an int or float value 

67 :param parent: Applies only if this is part of a :class:`DictConfig` 

68 :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:`default` value is *not* checked. 

69 

70 :const:`T_co` can be one of: 

71 * :class:`str` 

72 * :class:`int` 

73 * :class:`float` 

74 * :class:`bool` 

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

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

77 A help which is written to the config file must be provided as a str in the class attribute :attr:`help` or by calling :meth:`Set.set_help_for_type`. \ 

78 If that class has a str attribute :attr:`type_name` this is used instead of the class name inside of config file. 

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

80 

81 :raises ValueError: if key is not unique 

82 :raises ValueError: if :paramref:`default` is an empty list because the first element is used to infer the data type to which a value given in a config file is converted 

83 :raises TypeError: if this setting is a number or a list of numbers and :paramref:`unit` is not given 

84 ''' 

85 self._key = key 

86 self.value = default 

87 self.type = type(default) 

88 self.help = help 

89 self.unit = unit 

90 self.parent = parent 

91 self.allowed_values = allowed_values 

92 

93 if self.type == list: 

94 if not default: 

95 raise ValueError('I cannot infer the type from an empty list') 

96 self.item_type = type(default[0]) # type: ignore [index] # mypy does not understand that I just checked that default is a list 

97 needs_unit = self.item_type in TYPES_REQUIRING_UNIT 

98 else: 

99 needs_unit = self.type in TYPES_REQUIRING_UNIT 

100 if needs_unit and self.unit is None: 

101 raise TypeError(f'missing argument unit for {self.key}, pass an empty string if the number really has no unit') 

102 

103 cls = type(self) 

104 if key in cls.instances: 

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

106 cls.instances[key] = self 

107 

108 @property 

109 def key(self) -> str: 

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

111 return self._key 

112 

113 @key.setter 

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

115 if key in self.instances: 

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

117 del self.instances[self._key] 

118 self._key = key 

119 self.instances[key] = self 

120 

121 

122 @typing.overload 

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

124 pass 

125 

126 @typing.overload 

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

128 pass 

129 

130 def __get__(self: _Self, instance: typing.Any, owner: typing.Any = None) -> 'T_co|_Self': 

131 if instance is None: 

132 return self 

133 

134 return self.value 

135 

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

137 self.value = value 

138 

139 def __repr__(self) -> str: 

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

141 

142 def parse_and_set_value(self, config_id: 'ConfigId|None', value: str) -> None: 

143 if config_id is None: 

144 config_id = self.default_config_id 

145 if config_id != self.default_config_id: 

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

147 self.value = self.parse_value(value) 

148 

149 def parse_value(self, value: str) -> T_co: 

150 return self.parse_value_part(self.type, value) 

151 

152 def parse_value_part(self, t: 'type[T]', value: str) -> T: 

153 ''':raises ValueError: if value is invalid''' 

154 if t == str: 

155 value = value.replace(r'\n', '\n') 

156 out = typing.cast(T, value) 

157 elif t == int: 

158 out = typing.cast(T, int(value, base=0)) 

159 elif t == float: 

160 out = typing.cast(T, float(value)) 

161 elif t == bool: 

162 if value == VALUE_TRUE: 

163 out = typing.cast(T, True) 

164 elif value == VALUE_FALSE: 

165 out = typing.cast(T, False) 

166 else: 

167 raise ValueError(f'invalid value for {self.key}: {value!r} (should be {self.format_allowed_values_or_type(t)})') 

168 elif t == list: 

169 return typing.cast(T, [self.parse_value_part(self.item_type, v) for v in value.split(self.LIST_SEP)]) 

170 elif issubclass(t, enum.Enum): 

171 for enum_item in t: 

172 if self.format_any_value(typing.cast(T, enum_item)) == value: 

173 out = typing.cast(T, enum_item) 

174 break 

175 else: 

176 raise ValueError(f'invalid value for {self.key}: {value!r} (should be {self.format_allowed_values_or_type(t)})') 

177 else: 

178 try: 

179 out = t(value) # type: ignore [call-arg] 

180 except ValueError: 

181 raise ValueError(f'invalid value for {self.key}: {value!r} (should be {self.format_allowed_values_or_type(t)})') 

182 

183 if self.allowed_values is not None and out not in self.allowed_values: 

184 raise ValueError(f'invalid value for {self.key}: {value!r} (should be {self.format_allowed_values_or_type(t)})') 

185 return out 

186 

187 

188 def format_allowed_values_or_type(self, t: 'type[typing.Any]|None' = None) -> str: 

189 out = self.format_allowed_values(t) 

190 if out: 

191 return 'one of ' + out 

192 

193 out = self.format_type(t) 

194 

195 # getting the article right is not so easy, so a user can specify the correct article with type_article 

196 # this also gives the possibility to omit the article 

197 # https://en.wiktionary.org/wiki/Appendix:English_articles#Indefinite_singular_articles 

198 if hasattr(self.type, 'type_article'): 

199 article = getattr(self.type, 'type_article') 

200 if not article: 

201 return out 

202 assert isinstance(article, str) 

203 return article + ' ' + out 

204 if out[0].lower() in 'aeio': 

205 return 'an ' + out 

206 return 'a ' + out 

207 

208 def format_allowed_values(self, t: 'type[typing.Any]|None' = None) -> str: 

209 if t is None: 

210 t = self.type 

211 allowed_values: 'Iterable[typing.Any]' 

212 if t not in CONTAINER_TYPES and self.allowed_values is not None: 

213 allowed_values = self.allowed_values 

214 elif t == bool: 

215 allowed_values = (True, False) 

216 elif issubclass(t, enum.Enum): 

217 allowed_values = t 

218 else: 

219 return '' 

220 

221 out = ', '.join(self.format_any_value(val) for val in allowed_values) 

222 if self.unit: 

223 out += ' (unit: %s)' % self.unit 

224 return out 

225 

226 

227 def wants_to_be_exported(self) -> bool: 

228 return True 

229 

230 def format_type(self, t: 'type[typing.Any]|None' = None) -> str: 

231 if t is None: 

232 if self.type is list: 

233 t = self.item_type 

234 item_type = self.format_allowed_values(t) 

235 if not item_type: 

236 item_type = self.format_type(t) 

237 return 'comma separated list of %s' % item_type 

238 

239 t = self.type 

240 

241 out = getattr(t, 'type_name', t.__name__) 

242 if self.unit: 

243 out += ' in %s' % self.unit 

244 return out 

245 

246 def format_value(self, config_id: 'ConfigId|None') -> str: 

247 return self.format_any_value(self.value) 

248 

249 def format_any_value(self, value: typing.Any) -> str: 

250 if isinstance(value, str): 

251 value = value.replace('\n', r'\n') 

252 if isinstance(value, enum.Enum): 

253 return value.name.lower().replace('_', '-') 

254 if isinstance(value, bool): 

255 return VALUE_TRUE if value else VALUE_FALSE 

256 if isinstance(value, list): 

257 return self.LIST_SEP.join(self.format_any_value(v) for v in value) 

258 return str(value) 

259 

260 

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

262 

263 ''' 

264 A container for several settings which belong together. 

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

266 

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

268 ''' 

269 

270 def __init__(self, 

271 key_prefix: str, 

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

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

274 unit: 'str|None' = None, 

275 help: 'str|None' = None, 

276 allowed_values: 'Sequence[T]|None' = None, 

277 ) -> None: 

278 ''' 

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

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

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

282 :param unit: The unit of all items 

283 :param help: A help for all items 

284 :param allowed_values: The values which the items can have 

285 

286 :raises ValueError: if a key is not unique 

287 ''' 

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

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

290 self.allowed_values = allowed_values 

291 

292 self.key_prefix = key_prefix 

293 self.unit = unit 

294 self.help = help 

295 self.ignore_keys = ignore_keys 

296 

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

298 self[key] = val 

299 

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

301 ''' 

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

303 

304 :return: :paramref:`~DictConfig.key_prefix` + dot + :paramref:`key` 

305 ''' 

306 if isinstance(key, enum.Enum): 

307 key_str = key.name.lower().replace('_', '-') 

308 elif isinstance(key, bool): 

309 key_str = VALUE_TRUE if key else VALUE_FALSE 

310 else: 

311 key_str = str(key) 

312 

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

314 

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

316 if key in self.ignore_keys: 

317 self._ignored_values[key] = val 

318 return 

319 

320 c = self._values.get(key) 

321 if c is None: 

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

323 else: 

324 c.value = val 

325 

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

327 ''' 

328 Create a new :class:`Config` instance to be used internally 

329 ''' 

330 return Config(key, default, unit=unit, help=help, parent=self, allowed_values=self.allowed_values) 

331 

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

333 if key in self.ignore_keys: 

334 return self._ignored_values[key] 

335 else: 

336 return self._values[key].value 

337 

338 def __repr__(self) -> str: 

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

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

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

342 

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

344 if key in self.ignore_keys: 

345 return key in self._ignored_values 

346 else: 

347 return key in self._values 

348 

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

350 yield from self._values 

351 yield from self._ignored_values 

352 

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

354 ''' 

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

356 ''' 

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

358 yield cfg.key 

359 

360 

361class ConfigTrackingChanges(Config[T_co]): 

362 

363 _has_changed = False 

364 

365 @property # type: ignore [override] # This is a bug in mypy https://github.com/python/mypy/issues/4125 

366 def value(self: 'ConfigTrackingChanges[T]') -> T: 

367 ''' 

368 The value of this setting. 

369 ''' 

370 return self._value 

371 

372 @value.setter 

373 def value(self: 'ConfigTrackingChanges[T]', value: T) -> None: 

374 self._value = value 

375 self._has_changed = True 

376 

377 def save_value(self: 'ConfigTrackingChanges[T]', new_value: T) -> None: 

378 ''' 

379 Save the current :attr:`value` and assign :paramref:`new_value` to :attr:`value`. 

380 ''' 

381 self._last_value = self._value 

382 self._value = new_value 

383 self._has_changed = False 

384 

385 def restore_value(self) -> None: 

386 ''' 

387 Restore :attr:`value` to the value before the last call of :meth:`save_value`. 

388 ''' 

389 self._value = self._last_value 

390 

391 def has_changed(self) -> bool: 

392 ''' 

393 :return: :const:`True` if :attr:`value` has been changed since the last call to :meth:`save_value` 

394 ''' 

395 return self._has_changed 

396 

397 

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

399 

400class MultiConfig(Config[T_co]): 

401 

402 ''' 

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

404 

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

406 If there is no value for the ``config_id`` in :attr:`values` :attr:`value` is returned instead. 

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

408 

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

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

411 ''' 

412 

413 _Self = typing.TypeVar('_Self', bound='MultiConfig[T_co]') 

414 

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

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

417 

418 #: Stores the values for specific objects. 

419 values: 'dict[ConfigId, T_co]' 

420 

421 #: Stores the default value which is used if no value for the object is defined in :attr:`values`. 

422 value: 'T_co' 

423 

424 @classmethod 

425 def reset(cls) -> None: 

426 ''' 

427 Clear :attr:`config_ids` and clear :attr:`values` for all instances in :attr:`Config.instances` 

428 ''' 

429 cls.config_ids.clear() 

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

431 if isinstance(cfg, MultiConfig): 

432 cfg.values.clear() 

433 

434 def __init__(self, 

435 key: str, 

436 default: T_co, *, 

437 unit: 'str|None' = None, 

438 help: 'str|dict[T_co, str]|None' = None, 

439 parent: 'MultiDictConfig[typing.Any, T_co]|None' = None, 

440 allowed_values: 'Sequence[T_co]|None' = None, 

441 check_config_id: 'Callable[[MultiConfig[T_co], ConfigId], None]|None' = None, 

442 ) -> None: 

443 ''' 

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

445 :param default: The default value of this setting 

446 :param help: A description of this setting 

447 :param unit: The unit of an int or float value 

448 :param parent: Applies only if this is part of a :class:`MultiDictConfig` 

449 :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:`default` value is *not* checked. 

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

451 ''' 

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

453 self.values: 'dict[ConfigId, T_co]' = {} 

454 self.check_config_id = check_config_id 

455 

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

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

458 # But without copy-pasting this code mypy complains 

459 # "Signature of __get__ incompatible with supertype Config" 

460 @typing.overload 

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

462 pass 

463 

464 @typing.overload 

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

466 pass 

467 

468 def __get__(self: _Self, instance: typing.Any, owner: typing.Any = None) -> 'T_co|_Self': 

469 if instance is None: 

470 return self 

471 

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

473 

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

475 config_id = instance.config_id 

476 self.values[config_id] = value 

477 if config_id not in self.config_ids: 

478 self.config_ids.append(config_id) 

479 

480 def parse_and_set_value(self, config_id: 'ConfigId|None', value: str) -> None: 

481 if config_id is None: 

482 config_id = self.default_config_id 

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

484 self.check_config_id(self, config_id) 

485 if config_id == self.default_config_id: 

486 self.value = self.parse_value(value) 

487 else: 

488 self.values[config_id] = self.parse_value(value) 

489 if config_id not in self.config_ids: 

490 self.config_ids.append(config_id) 

491 

492 def format_value(self, config_id: 'ConfigId|None') -> str: 

493 if config_id is None: 

494 config_id = self.default_config_id 

495 return self.format_any_value(self.values.get(config_id, self.value)) 

496 

497 

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

499 

500 ''' 

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

502 

503 This is essentially a :class:`DictConfig` using :class:`MultiConfig` instead of normal :class:`Config`. 

504 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:`InstanceSpecificDictMultiConfig` if it is accessed as an instance attribute. 

505 ''' 

506 

507 _Self = typing.TypeVar('_Self', bound='MultiDictConfig[T_KEY, T]') 

508 

509 def __init__(self, 

510 key_prefix: str, 

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

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

513 unit: 'str|None' = None, 

514 help: 'str|None' = None, 

515 allowed_values: 'Sequence[T]|None' = None, 

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

517 ) -> None: 

518 ''' 

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

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

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

522 :param unit: The unit of all items 

523 :param help: A help for all items 

524 :param allowed_values: The values which the items can have 

525 :param check_config_id: Is passed through to :class:`MultiConfig` 

526 

527 :raises ValueError: if a key is not unique 

528 ''' 

529 self.check_config_id = check_config_id 

530 super().__init__( 

531 key_prefix = key_prefix, 

532 default_values = default_values, 

533 ignore_keys = ignore_keys, 

534 unit = unit, 

535 help = help, 

536 allowed_values = allowed_values, 

537 ) 

538 

539 @typing.overload 

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

541 pass 

542 

543 @typing.overload 

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

545 pass 

546 

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

548 if instance is None: 

549 return self 

550 

551 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

552 

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

554 raise NotImplementedError() 

555 

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

557 return MultiConfig(key, default, unit=unit, help=help, parent=self, allowed_values=self.allowed_values, check_config_id=self.check_config_id) 

558 

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

560 

561 ''' 

562 An intermediate instance which is returned when accsessing 

563 a :class:`MultiDictConfig` as an instance attribute. 

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

565 ''' 

566 

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

568 self.dmc = dmc 

569 self.config_id = config_id 

570 

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

572 if key in self.dmc.ignore_keys: 

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

574 

575 c = self.dmc._values.get(key) 

576 if c is None: 

577 self.dmc._values[key] = MultiConfig(self.dmc.format_key(key), val, help=self.dmc.help) 

578 else: 

579 c.__set__(self, val) 

580 

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

582 if key in self.dmc.ignore_keys: 

583 return self.dmc._ignored_values[key] 

584 else: 

585 return self.dmc._values[key].__get__(self)