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

270 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-22 14:03 +0200

1#!./runmodule.sh 

2 

3import enum 

4import typing 

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

6 

7if typing.TYPE_CHECKING: 

8 from typing_extensions import Self 

9 

10 

11VALUE_TRUE = 'true' 

12VALUE_FALSE = 'false' 

13VALUE_NONE = 'none' 

14VALUE_AUTO = 'auto' 

15 

16TYPES_REQUIRING_UNIT = {int, float} 

17CONTAINER_TYPES = {list} 

18 

19 

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

21 

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

23T_KEY = typing.TypeVar('T_KEY') 

24T = typing.TypeVar('T') 

25 

26 

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

28 

29 ''' 

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

31 

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

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

34 ''' 

35 

36 LIST_SEP = ',' 

37 

38 #: 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. 

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

40 

41 default_config_id = ConfigId('general') 

42 

43 #: The value of this setting. 

44 value: 'T_co' 

45 

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

47 unit: 'str|None' 

48 

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

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

51 

52 #: 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. 

53 allowed_values: 'Sequence[T_co]|None' 

54 

55 def __init__(self, 

56 key: str, 

57 default: T_co, *, 

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

59 unit: 'str|None' = None, 

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

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

62 ): 

63 ''' 

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

65 :param default: The default value of this setting 

66 :param help: A description of this setting 

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

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

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

70 

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

72 * :class:`str` 

73 * :class:`int` 

74 * :class:`float` 

75 * :class:`bool` 

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

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

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

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

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

81 

82 :raises ValueError: if key is not unique 

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

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

85 ''' 

86 self._key = key 

87 self.value = default 

88 self.type = type(default) 

89 self.help = help 

90 self.unit = unit 

91 self.parent = parent 

92 self.allowed_values = allowed_values 

93 

94 if self.type == list: 

95 if not default: 

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

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

98 needs_unit = self.item_type in TYPES_REQUIRING_UNIT 

99 else: 

100 needs_unit = self.type in TYPES_REQUIRING_UNIT 

101 if needs_unit and self.unit is None: 

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

103 

104 cls = type(self) 

105 if key in cls.instances: 

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

107 cls.instances[key] = self 

108 

109 @property 

110 def key(self) -> str: 

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

112 return self._key 

113 

114 @key.setter 

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

116 if key in self.instances: 

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

118 del self.instances[self._key] 

119 self._key = key 

120 self.instances[key] = self 

121 

122 

123 @typing.overload 

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

125 pass 

126 

127 @typing.overload 

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

129 pass 

130 

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

132 if instance is None: 

133 return self 

134 

135 return self.value 

136 

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

138 self.value = value 

139 

140 def __repr__(self) -> str: 

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

142 

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

144 ''' 

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

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

147 ''' 

148 if config_id is None: 

149 config_id = self.default_config_id 

150 if config_id != self.default_config_id: 

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

152 self.value = value 

153 

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

155 ''' 

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

157 

158 :param value: The value to be parsed 

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

160 ''' 

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

162 

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

164 ''' 

165 Parse a value to the given data type. 

166 

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

168 :param value: The value to be parsed 

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

170 ''' 

171 if t == str: 

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

173 out = typing.cast(T, value) 

174 elif t == int: 

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

176 elif t == float: 

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

178 elif t == bool: 

179 if value == VALUE_TRUE: 

180 out = typing.cast(T, True) 

181 elif value == VALUE_FALSE: 

182 out = typing.cast(T, False) 

183 else: 

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

185 elif t == list: 

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

187 elif issubclass(t, enum.Enum): 

188 for enum_item in t: 

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

190 out = typing.cast(T, enum_item) 

191 break 

192 else: 

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

194 else: 

195 try: 

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

197 except Exception as e: 

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

199 

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

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

202 return out 

203 

204 

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

206 out = self.format_allowed_values(t) 

207 if out: 

208 return 'one of ' + out 

209 

210 out = self.format_type(t) 

211 

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

213 # this also gives the possibility to omit the article 

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

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

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

217 if not article: 

218 return out 

219 assert isinstance(article, str) 

220 return article + ' ' + out 

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

222 return 'an ' + out 

223 return 'a ' + out 

224 

225 def get_allowed_values(self, t: 'type[typing.Any]|None' = None) -> 'Iterable[object]': 

226 if t is None: 

227 t = self.type 

228 allowed_values: 'Iterable[typing.Any]' 

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

230 allowed_values = self.allowed_values 

231 elif t == bool: 

232 allowed_values = (True, False) 

233 elif issubclass(t, enum.Enum): 

234 allowed_values = t 

235 else: 

236 allowed_values = [] 

237 return allowed_values 

238 

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

240 for val in self.get_allowed_values(t): 

241 yield self.format_any_value(val) 

242 

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

244 out = ', '.join(self.get_stringified_allowed_values(t)) 

245 if out and self.unit: 

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

247 return out 

248 

249 

250 def wants_to_be_exported(self) -> bool: 

251 return True 

252 

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

254 if t is None: 

255 if self.type is list: 

256 t = self.item_type 

257 item_type = self.format_allowed_values(t) 

258 if not item_type: 

259 item_type = self.format_type(t) 

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

261 

262 t = self.type 

263 

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

265 if self.unit: 

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

267 return out 

268 

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

270 return self.format_any_value(self.value) 

271 

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

273 if isinstance(value, str): 

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

275 if isinstance(value, enum.Enum): 

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

277 if isinstance(value, bool): 

278 return VALUE_TRUE if value else VALUE_FALSE 

279 if isinstance(value, list): 

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

281 return str(value) 

282 

283 

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

285 

286 ''' 

287 A container for several settings which belong together. 

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

289 

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

291 ''' 

292 

293 def __init__(self, 

294 key_prefix: str, 

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

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

297 unit: 'str|None' = None, 

298 help: 'str|None' = None, 

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

300 ) -> None: 

301 ''' 

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

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

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

305 :param unit: The unit of all items 

306 :param help: A help for all items 

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

308 

309 :raises ValueError: if a key is not unique 

310 ''' 

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

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

313 self.allowed_values = allowed_values 

314 

315 self.key_prefix = key_prefix 

316 self.unit = unit 

317 self.help = help 

318 self.ignore_keys = ignore_keys 

319 

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

321 self[key] = val 

322 

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

324 ''' 

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

326 

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

328 ''' 

329 if isinstance(key, enum.Enum): 

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

331 elif isinstance(key, bool): 

332 key_str = VALUE_TRUE if key else VALUE_FALSE 

333 else: 

334 key_str = str(key) 

335 

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

337 

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

339 if key in self.ignore_keys: 

340 self._ignored_values[key] = val 

341 return 

342 

343 c = self._values.get(key) 

344 if c is None: 

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

346 else: 

347 c.value = val 

348 

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

350 ''' 

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

352 ''' 

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

354 

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

356 if key in self.ignore_keys: 

357 return self._ignored_values[key] 

358 else: 

359 return self._values[key].value 

360 

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

362 try: 

363 return self[key] 

364 except KeyError: 

365 return default 

366 

367 def __repr__(self) -> str: 

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

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

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

371 

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

373 if key in self.ignore_keys: 

374 return key in self._ignored_values 

375 else: 

376 return key in self._values 

377 

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

379 yield from self._values 

380 yield from self._ignored_values 

381 

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

383 ''' 

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

385 ''' 

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

387 yield cfg.key 

388 

389 

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

391 

392class MultiConfig(Config[T_co]): 

393 

394 ''' 

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

396 

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

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

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

400 

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

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

403 ''' 

404 

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

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

407 

408 #: Stores the values for specific objects. 

409 values: 'dict[ConfigId, T_co]' 

410 

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

412 value: 'T_co' 

413 

414 @classmethod 

415 def reset(cls) -> None: 

416 ''' 

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

418 ''' 

419 cls.config_ids.clear() 

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

421 if isinstance(cfg, MultiConfig): 

422 cfg.values.clear() 

423 

424 def __init__(self, 

425 key: str, 

426 default: T_co, *, 

427 unit: 'str|None' = None, 

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

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

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

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

432 ) -> None: 

433 ''' 

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

435 :param default: The default value of this setting 

436 :param help: A description of this setting 

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

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

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

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

441 ''' 

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

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

444 self.check_config_id = check_config_id 

445 

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

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

448 # But without copy-pasting this code mypy complains 

449 # "Signature of __get__ incompatible with supertype Config" 

450 @typing.overload 

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

452 pass 

453 

454 @typing.overload 

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

456 pass 

457 

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

459 if instance is None: 

460 return self 

461 

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

463 

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

465 config_id = instance.config_id 

466 self.values[config_id] = value 

467 if config_id not in self.config_ids: 

468 self.config_ids.append(config_id) 

469 

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

471 ''' 

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

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

474 

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

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

477 

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

479 

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

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

482 ''' 

483 if config_id is None: 

484 config_id = self.default_config_id 

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

486 self.check_config_id(self, config_id) 

487 if config_id == self.default_config_id: 

488 self.value = value 

489 else: 

490 self.values[config_id] = value 

491 if config_id not in self.config_ids: 

492 self.config_ids.append(config_id) 

493 

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

495 ''' 

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

497 

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

499 ''' 

500 if config_id is None: 

501 config_id = self.default_config_id 

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

503 

504 

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

506 

507 ''' 

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

509 

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

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

512 ''' 

513 

514 def __init__(self, 

515 key_prefix: str, 

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

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

518 unit: 'str|None' = None, 

519 help: 'str|None' = None, 

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

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

522 ) -> None: 

523 ''' 

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

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

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

527 :param unit: The unit of all items 

528 :param help: A help for all items 

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

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

531 

532 :raises ValueError: if a key is not unique 

533 ''' 

534 self.check_config_id = check_config_id 

535 super().__init__( 

536 key_prefix = key_prefix, 

537 default_values = default_values, 

538 ignore_keys = ignore_keys, 

539 unit = unit, 

540 help = help, 

541 allowed_values = allowed_values, 

542 ) 

543 

544 @typing.overload 

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

546 pass 

547 

548 @typing.overload 

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

550 pass 

551 

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

553 if instance is None: 

554 return self 

555 

556 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

557 

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

559 raise NotImplementedError() 

560 

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

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

563 

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

565 

566 ''' 

567 An intermediate instance which is returned when accsessing 

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

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

570 ''' 

571 

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

573 self.mdc = mdc 

574 self.config_id = config_id 

575 

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

577 if key in self.mdc.ignore_keys: 

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

579 

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

581 if c is None: 

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

583 else: 

584 c.__set__(self, val) 

585 

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

587 if key in self.mdc.ignore_keys: 

588 return self.mdc._ignored_values[key] 

589 else: 

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