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

261 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-30 09:33 +0100

1#!./runmodule.sh 

2 

3import builtins 

4import enum 

5import typing 

6from collections.abc import Iterable, Iterator, Container, Sequence, Mapping, 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 

12from . import state 

13 

14 

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

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

17 

18T_KEY = typing.TypeVar('T_KEY') 

19T = typing.TypeVar('T') 

20T_DEFAULT = typing.TypeVar('T_DEFAULT') 

21 

22class TimingError(Exception): 

23 

24 ''' 

25 Is raised when trying to instantiate :class:`~confattr.config.Config` if a :class:`~confattr.configfile.ConfigFile` has been instantiated before without passing an explicit list or set to :paramref:`~confattr.configfile.ConfigFile.config_instances` or when trying to change a :attr:`~confattr.config.Config.key` after creating any :class:`~confattr.configfile.ConfigFile` because such changes would otherwise be silently ignored by that :class:`~confattr.configfile.ConfigFile`. 

26 ''' 

27 

28 

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

30 

31 ''' 

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

33 

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

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

36 ''' 

37 

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

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

40 

41 @classmethod 

42 def iter_instances(cls) -> 'Iterator[Config[typing.Any]|DictConfig[typing.Any, typing.Any]]': 

43 ''' 

44 Yield the instances in :attr:`~confattr.config.Config.instances` but merge :class:`~confattr.config.DictConfig` items to a single :class:`~confattr.config.DictConfig` instance so that they can be sorted differently. 

45 ''' 

46 parents = set() 

47 for cfg in cls.instances.values(): 

48 if cfg.parent: 

49 if cfg.parent not in parents: 

50 yield cfg.parent 

51 parents.add(cfg.parent) 

52 else: 

53 yield cfg 

54 

55 default_config_id = ConfigId('general') 

56 

57 #: The value of this setting. 

58 value: 'T' 

59 

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

61 type: 'AbstractFormatter[T]' 

62 

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

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

65 

66 

67 _key_changer: 'list[Callable[[str], str]]' = [] 

68 

69 @classmethod 

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

71 ''' 

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

73 

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

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

76 

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

78 ''' 

79 cls._key_changer.append(callback) 

80 

81 @classmethod 

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

83 ''' 

84 Undo the last call to :meth:`~confattr.config.Config.push_key_changer`. 

85 ''' 

86 return cls._key_changer.pop() 

87 

88 

89 def __init__(self, 

90 key: str, 

91 default: T, *, 

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

93 unit: 'str|None' = None, 

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

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

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

97 ): 

98 ''' 

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

100 :param default: The default value of this setting 

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

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

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

104 :param help: A description of this setting 

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

106 

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

108 * :class:`str` 

109 * :class:`int` 

110 * :class:`float` 

111 * :class:`bool` 

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

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

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

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

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

117 

118 :raises ValueError: if key is not unique 

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

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

121 :raises TimingError: if this setting is defined after creating a :class:`~confattr.configfile.ConfigFile` object without passing a list or set of settings to :paramref:`~confattr.configfile.ConfigFile.config_instances` 

122 ''' 

123 if state.has_config_file_been_instantiated: 

124 raise TimingError("The setting %r is defined after instantiating a ConfigFile. It will not be available in the ConfigFile. If this is intentional you can avoid this Exception by explicitly passing a set or list of settings to config_instances of the ConfigFile." % key) 

125 if self._key_changer: 

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

127 

128 if type is None: 

129 if isinstance(default, list): 

130 if not default: 

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

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

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

134 elif isinstance(default, set): 

135 if not default: 

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

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

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

139 elif isinstance(default, dict): 

140 if not default: 

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

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

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

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

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

146 else: 

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

148 else: 

149 if unit is not None: 

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

151 if allowed_values is not None: 

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

153 

154 type.set_config_key(key) 

155 

156 self._key = key 

157 self.value = default 

158 self.type = type 

159 self.help = help 

160 self.parent = parent 

161 

162 cls = builtins.type(self) 

163 if key in cls.instances: 

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

165 cls.instances[key] = self 

166 

167 @property 

168 def key(self) -> str: 

169 ''' 

170 The name of this setting which is used in the config file. 

171 This must be unique. 

172 You can change this attribute but only as long as no :class:`~confattr.configfile.ConfigFile` or :class:`~confattr.quickstart.ConfigManager` has been instantiated. 

173 ''' 

174 return self._key 

175 

176 @key.setter 

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

178 if state.has_any_config_file_been_instantiated: 

179 raise TimingError('ConfigFile has been instantiated already. Changing a key now would go unnoticed by that ConfigFile.') 

180 if key in self.instances: 

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

182 del self.instances[self._key] 

183 self._key = key 

184 self.type.config_key = key 

185 self.instances[key] = self 

186 

187 

188 @typing.overload 

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

190 pass 

191 

192 @typing.overload 

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

194 pass 

195 

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

197 if instance is None: 

198 return self 

199 

200 return self.value 

201 

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

203 self.value = value 

204 

205 def __repr__(self) -> str: 

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

207 

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

209 ''' 

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

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

212 ''' 

213 if config_id is None: 

214 config_id = self.default_config_id 

215 if config_id != self.default_config_id: 

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

217 self.value = value 

218 

219 def wants_to_be_exported(self) -> bool: 

220 return True 

221 

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

223 ''' 

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

225 

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

227 ''' 

228 return self.value 

229 

230 def is_value_valid(self) -> bool: 

231 ''' 

232 :return: true unless the value of an :class:`~confattr.config.ExplicitConfig` instance has not been set yet 

233 ''' 

234 return True 

235 

236 

237class ExplicitConfig(Config[T]): 

238 

239 ''' 

240 A setting without a default value which requires the user to explicitly set a value in the config file or pass it as command line argument. 

241 

242 You can use :meth:`~confattr.config.ExplicitConfig.is_value_valid` in order to check whether this config has a value or not. 

243 

244 If you try to use the value before it has been set: If you try to access the config as instance attribute (:python:`object.config`) a :class:`TypeError` is thrown. Otherwise (:python:`config.value`) :obj:`None` is returned. 

245 ''' 

246 

247 def __init__(self, 

248 key: str, 

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

250 unit: 'str|None' = None, 

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

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

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

254 ): 

255 ''' 

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

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

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

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

260 :param help: A description of this setting 

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

262 ''' 

263 if type is None: 

264 if not allowed_values: 

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

266 elif isinstance(allowed_values, dict): 

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

268 else: 

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

270 if not isinstance(type, AbstractFormatter): 

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

272 super().__init__(key, 

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

274 type = type, 

275 help = help, 

276 parent = parent, 

277 ) 

278 

279 @typing.overload 

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

281 pass 

282 

283 @typing.overload 

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

285 pass 

286 

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

288 if instance is None: 

289 return self 

290 

291 if self.value is None: 

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

293 return self.value 

294 

295 def is_value_valid(self) -> bool: 

296 return self.value is not None 

297 

298 

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

300 

301 ''' 

302 A container for several settings which belong together. 

303 Except for :meth:`~object.__eq__` and :meth:`~object.__ne__` it behaves like a normal :class:`~collections.abc.Mapping` 

304 but internally the items are stored in :class:`~confattr.config.Config` instances. 

305 

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

307 ''' 

308 

309 class Sort(enum.Enum): 

310 NAME = enum.auto() 

311 ENUM_VALUE = enum.auto() 

312 NONE = enum.auto() 

313 

314 def __init__(self, 

315 key_prefix: str, 

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

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

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

319 unit: 'str|None' = None, 

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

321 help: 'str|None' = None, 

322 sort: Sort = Sort.NAME, 

323 ) -> None: 

324 ''' 

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

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

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

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

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

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

331 :param help: A help for all items 

332 :param sort: How to sort the items of this dictionary in the config file/documentation 

333 

334 :raises ValueError: if a key is not unique 

335 ''' 

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

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

338 self.allowed_values = allowed_values 

339 self.sort = sort 

340 

341 self.key_prefix = key_prefix 

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

343 self.type = type 

344 self.unit = unit 

345 self.help = help 

346 self.ignore_keys = ignore_keys 

347 

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

349 self[key] = val 

350 

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

352 ''' 

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

354 

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

356 ''' 

357 key_str = format_primitive_value(key) 

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

359 

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

361 if key in self.ignore_keys: 

362 self._ignored_values[key] = val 

363 return 

364 

365 c = self._values.get(key) 

366 if c is None: 

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

368 else: 

369 c.value = val 

370 

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

372 ''' 

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

374 ''' 

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

376 

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

378 if key in self.ignore_keys: 

379 return self._ignored_values[key] 

380 else: 

381 return self._values[key].value 

382 

383 @typing.overload 

384 def get(self, key: T_KEY) -> 'T|None': 

385 ... 

386 

387 @typing.overload 

388 def get(self, key: T_KEY, default: T_DEFAULT) -> 'T|T_DEFAULT': 

389 ... 

390 

391 def get(self, key: T_KEY, default: 'typing.Any' = None) -> 'typing.Any': 

392 try: 

393 return self[key] 

394 except KeyError: 

395 return default 

396 

397 def __repr__(self) -> str: 

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

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

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

401 

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

403 if key in self.ignore_keys: 

404 return key in self._ignored_values 

405 else: 

406 return key in self._values 

407 

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

409 yield from self._values 

410 yield from self._ignored_values 

411 

412 def keys(self) -> 'Iterator[T_KEY]': 

413 yield from self._values.keys() 

414 yield from self._ignored_values.keys() 

415 

416 def values(self) -> 'Iterator[T]': 

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

418 yield cfg.value 

419 yield from self._ignored_values.values() 

420 

421 def items(self) -> 'Iterator[tuple[T_KEY, T]]': 

422 for key, cfg in self._values.items(): 

423 yield key, cfg.value 

424 yield from self._ignored_values.items() 

425 

426 

427 def iter_configs(self) -> 'Iterator[Config[T]]': 

428 ''' 

429 Iterate over the :class:`~confattr.config.Config` instances contained in this dict, 

430 sorted by the argument passed to :paramref:`~confattr.config.DictConfig.sort` in the constructor 

431 ''' 

432 if self.sort is self.Sort.NAME: 

433 yield from sorted(self._values.values(), key=lambda c: c.key) 

434 elif self.sort is self.Sort.NONE: 

435 yield from self._values.values() 

436 elif self.sort is self.Sort.ENUM_VALUE: 

437 #keys = typing.cast('Iterable[enum.Enum]', self._values.keys()) 

438 #keys = tuple(self._values) 

439 if is_mapping_with_enum_keys(self._values): 

440 for key in sorted(self._values.keys(), key=lambda c: c.value): 

441 yield self._values[key] 

442 else: 

443 raise TypeError("%r can only be used with enum keys" % self.sort) 

444 else: 

445 raise NotImplementedError("sort %r is not implemented" % self.sort) 

446 

447def is_mapping_with_enum_keys(m: 'Mapping[typing.Any, T]') -> 'typing.TypeGuard[Mapping[enum.Enum, T]]': 

448 return all(isinstance(key, enum.Enum) for key in m.keys()) 

449 

450 

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

452 

453class MultiConfig(Config[T]): 

454 

455 ''' 

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

457 

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

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

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

461 

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

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

464 ''' 

465 

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

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

468 

469 #: Stores the values for specific objects. 

470 values: 'dict[ConfigId, T]' 

471 

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

473 value: 'T' 

474 

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

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

477 

478 @classmethod 

479 def reset(cls) -> None: 

480 ''' 

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

482 ''' 

483 cls.config_ids.clear() 

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

485 if isinstance(cfg, MultiConfig): 

486 cfg.values.clear() 

487 

488 def __init__(self, 

489 key: str, 

490 default: T, *, 

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

492 unit: 'str|None' = None, 

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

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

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

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

497 ) -> None: 

498 ''' 

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

500 :param default: The default value of this setting 

501 :param help: A description of this setting 

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

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

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

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

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

507 ''' 

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

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

510 self.check_config_id = check_config_id 

511 

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

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

514 # But without copy-pasting this code mypy complains 

515 # "Signature of __get__ incompatible with supertype Config" 

516 @typing.overload 

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

518 pass 

519 

520 @typing.overload 

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

522 pass 

523 

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

525 if instance is None: 

526 return self 

527 

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

529 

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

531 config_id = instance.config_id 

532 self.values[config_id] = value 

533 if config_id not in self.config_ids: 

534 self.config_ids.append(config_id) 

535 

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

537 ''' 

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

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

540 

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

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

543 

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

545 

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

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

548 ''' 

549 if config_id is None: 

550 config_id = self.default_config_id 

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

552 self.check_config_id(self, config_id) 

553 if config_id == self.default_config_id: 

554 self.value = value 

555 else: 

556 self.values[config_id] = value 

557 if config_id not in self.config_ids: 

558 self.config_ids.append(config_id) 

559 

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

561 ''' 

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

563 ''' 

564 if config_id is None: 

565 config_id = self.default_config_id 

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

567 

568 

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

570 

571 ''' 

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

573 

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

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

576 ''' 

577 

578 def __init__(self, 

579 key_prefix: str, 

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

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

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

583 unit: 'str|None' = None, 

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

585 help: 'str|None' = None, 

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

587 ) -> None: 

588 ''' 

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

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

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

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

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

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

595 :param help: A help for all items 

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

597 

598 :raises ValueError: if a key is not unique 

599 ''' 

600 self.check_config_id = check_config_id 

601 super().__init__( 

602 key_prefix = key_prefix, 

603 default_values = default_values, 

604 type = type, 

605 ignore_keys = ignore_keys, 

606 unit = unit, 

607 help = help, 

608 allowed_values = allowed_values, 

609 ) 

610 

611 @typing.overload 

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

613 pass 

614 

615 @typing.overload 

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

617 pass 

618 

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

620 if instance is None: 

621 return self 

622 

623 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

624 

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

626 raise NotImplementedError() 

627 

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

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

630 

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

632 

633 ''' 

634 An intermediate instance which is returned when accsessing 

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

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

637 ''' 

638 

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

640 self.mdc = mdc 

641 self.config_id = config_id 

642 

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

644 if key in self.mdc.ignore_keys: 

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

646 

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

648 if c is None: 

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

650 else: 

651 c.__set__(self, val) 

652 

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

654 if key in self.mdc.ignore_keys: 

655 return self.mdc._ignored_values[key] 

656 else: 

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