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

263 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-14 13:30 +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 set_value(self: 'Config[T]', config_id: 'ConfigId|None', value: T) -> None: 

143 ''' 

144 This method is just to provide a common interface for :class:`Config` and :class:`MultiConfig`. 

145 If you know that you are dealing with a normal :class:`Config` you can set :attr:`value` directly. 

146 ''' 

147 if config_id is None: 

148 config_id = self.default_config_id 

149 if config_id != self.default_config_id: 

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

151 self.value = value 

152 

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

154 ''' 

155 Parse a value to the data type of this setting. 

156 

157 :param value: The value to be parsed 

158 :raises ValueError: if :paramref:`value` is invalid 

159 ''' 

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

161 

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

163 ''' 

164 Parse a value to the given data type. 

165 

166 :param t: The data type to which :paramref:`value` shall be parsed 

167 :param value: The value to be parsed 

168 :raises ValueError: if :paramref:`value` is invalid 

169 ''' 

170 if t == str: 

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

172 out = typing.cast(T, value) 

173 elif t == int: 

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

175 elif t == float: 

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

177 elif t == bool: 

178 if value == VALUE_TRUE: 

179 out = typing.cast(T, True) 

180 elif value == VALUE_FALSE: 

181 out = typing.cast(T, False) 

182 else: 

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

184 elif t == list: 

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

186 elif issubclass(t, enum.Enum): 

187 for enum_item in t: 

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

189 out = typing.cast(T, enum_item) 

190 break 

191 else: 

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

193 else: 

194 try: 

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

196 except Exception as e: 

197 raise ValueError(f'invalid value for {self.key}: {value!r} ({e})') 

198 

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

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

201 return out 

202 

203 

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

205 out = self.format_allowed_values(t) 

206 if out: 

207 return 'one of ' + out 

208 

209 out = self.format_type(t) 

210 

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

212 # this also gives the possibility to omit the article 

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

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

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

216 if not article: 

217 return out 

218 assert isinstance(article, str) 

219 return article + ' ' + out 

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

221 return 'an ' + out 

222 return 'a ' + out 

223 

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

225 if t is None: 

226 t = self.type 

227 allowed_values: 'Iterable[typing.Any]' 

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

229 allowed_values = self.allowed_values 

230 elif t == bool: 

231 allowed_values = (True, False) 

232 elif issubclass(t, enum.Enum): 

233 allowed_values = t 

234 else: 

235 return '' 

236 

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

238 if self.unit: 

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

240 return out 

241 

242 

243 def wants_to_be_exported(self) -> bool: 

244 return True 

245 

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

247 if t is None: 

248 if self.type is list: 

249 t = self.item_type 

250 item_type = self.format_allowed_values(t) 

251 if not item_type: 

252 item_type = self.format_type(t) 

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

254 

255 t = self.type 

256 

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

258 if self.unit: 

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

260 return out 

261 

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

263 return self.format_any_value(self.value) 

264 

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

266 if isinstance(value, str): 

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

268 if isinstance(value, enum.Enum): 

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

270 if isinstance(value, bool): 

271 return VALUE_TRUE if value else VALUE_FALSE 

272 if isinstance(value, list): 

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

274 return str(value) 

275 

276 

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

278 

279 ''' 

280 A container for several settings which belong together. 

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

282 

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

284 ''' 

285 

286 def __init__(self, 

287 key_prefix: str, 

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

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

290 unit: 'str|None' = None, 

291 help: 'str|None' = None, 

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

293 ) -> None: 

294 ''' 

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

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

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

298 :param unit: The unit of all items 

299 :param help: A help for all items 

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

301 

302 :raises ValueError: if a key is not unique 

303 ''' 

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

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

306 self.allowed_values = allowed_values 

307 

308 self.key_prefix = key_prefix 

309 self.unit = unit 

310 self.help = help 

311 self.ignore_keys = ignore_keys 

312 

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

314 self[key] = val 

315 

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

317 ''' 

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

319 

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

321 ''' 

322 if isinstance(key, enum.Enum): 

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

324 elif isinstance(key, bool): 

325 key_str = VALUE_TRUE if key else VALUE_FALSE 

326 else: 

327 key_str = str(key) 

328 

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

330 

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

332 if key in self.ignore_keys: 

333 self._ignored_values[key] = val 

334 return 

335 

336 c = self._values.get(key) 

337 if c is None: 

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

339 else: 

340 c.value = val 

341 

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

343 ''' 

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

345 ''' 

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

347 

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

349 if key in self.ignore_keys: 

350 return self._ignored_values[key] 

351 else: 

352 return self._values[key].value 

353 

354 def __repr__(self) -> str: 

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

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

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

358 

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

360 if key in self.ignore_keys: 

361 return key in self._ignored_values 

362 else: 

363 return key in self._values 

364 

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

366 yield from self._values 

367 yield from self._ignored_values 

368 

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

370 ''' 

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

372 ''' 

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

374 yield cfg.key 

375 

376 

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

378 

379class MultiConfig(Config[T_co]): 

380 

381 ''' 

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

383 

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

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

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

387 

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

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

390 ''' 

391 

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

393 

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

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

396 

397 #: Stores the values for specific objects. 

398 values: 'dict[ConfigId, T_co]' 

399 

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

401 value: 'T_co' 

402 

403 @classmethod 

404 def reset(cls) -> None: 

405 ''' 

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

407 ''' 

408 cls.config_ids.clear() 

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

410 if isinstance(cfg, MultiConfig): 

411 cfg.values.clear() 

412 

413 def __init__(self, 

414 key: str, 

415 default: T_co, *, 

416 unit: 'str|None' = None, 

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

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

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

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

421 ) -> None: 

422 ''' 

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

424 :param default: The default value of this setting 

425 :param help: A description of this setting 

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

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

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

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

430 ''' 

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

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

433 self.check_config_id = check_config_id 

434 

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

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

437 # But without copy-pasting this code mypy complains 

438 # "Signature of __get__ incompatible with supertype Config" 

439 @typing.overload 

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

441 pass 

442 

443 @typing.overload 

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

445 pass 

446 

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

448 if instance is None: 

449 return self 

450 

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

452 

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

454 config_id = instance.config_id 

455 self.values[config_id] = value 

456 if config_id not in self.config_ids: 

457 self.config_ids.append(config_id) 

458 

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

460 ''' 

461 Check :paramref:`config_id` by calling :meth:`check_config_id` and 

462 set the value for the object(s) identified by :paramref:`config_id`. 

463 

464 If you know that :paramref:`config_id` is valid you can also change the items of :attr:`values` directly. 

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

466 

467 If you want to set the default value you can also set :attr:`value` directly. 

468 

469 :param config_id: Identifies the object(s) for which :paramref:`value` is intended. :obj:`None` is equivalent to :attr:`default_config_id`. 

470 :param value: The value to be assigned for the object(s) identified by :paramref:`config_id`. 

471 ''' 

472 if config_id is None: 

473 config_id = self.default_config_id 

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

475 self.check_config_id(self, config_id) 

476 if config_id == self.default_config_id: 

477 self.value = value 

478 else: 

479 self.values[config_id] = value 

480 if config_id not in self.config_ids: 

481 self.config_ids.append(config_id) 

482 

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

484 ''' 

485 Convert the value for the specified object(s) to a string. 

486 

487 :param config_id: Identifies the value which you want to convert. :obj:`None` is equivalent to :attr:`default_config_id`. 

488 ''' 

489 if config_id is None: 

490 config_id = self.default_config_id 

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

492 

493 

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

495 

496 ''' 

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

498 

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

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

501 ''' 

502 

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

504 

505 def __init__(self, 

506 key_prefix: str, 

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

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

509 unit: 'str|None' = None, 

510 help: 'str|None' = None, 

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

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

513 ) -> None: 

514 ''' 

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

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

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

518 :param unit: The unit of all items 

519 :param help: A help for all items 

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

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

522 

523 :raises ValueError: if a key is not unique 

524 ''' 

525 self.check_config_id = check_config_id 

526 super().__init__( 

527 key_prefix = key_prefix, 

528 default_values = default_values, 

529 ignore_keys = ignore_keys, 

530 unit = unit, 

531 help = help, 

532 allowed_values = allowed_values, 

533 ) 

534 

535 @typing.overload 

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

537 pass 

538 

539 @typing.overload 

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

541 pass 

542 

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

544 if instance is None: 

545 return self 

546 

547 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

548 

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

550 raise NotImplementedError() 

551 

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

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

554 

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

556 

557 ''' 

558 An intermediate instance which is returned when accsessing 

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

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

561 ''' 

562 

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

564 self.mdc = mdc 

565 self.config_id = config_id 

566 

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

568 if key in self.mdc.ignore_keys: 

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

570 

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

572 if c is None: 

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

574 else: 

575 c.__set__(self, val) 

576 

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

578 if key in self.mdc.ignore_keys: 

579 return self.mdc._ignored_values[key] 

580 else: 

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