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

254 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-16 15:15 +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') 

20 

21class TimingError(Exception): 

22 

23 ''' 

24 Is raised by :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` because settings defined after a :class:`~confattr.configfile.ConfigFile` would otherwise be silently ignored by that :class:`~confattr.configfile.ConfigFile`. 

25 ''' 

26 

27 

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

29 

30 ''' 

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

32 

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

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

35 ''' 

36 

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

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

39 

40 @classmethod 

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

42 ''' 

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

44 ''' 

45 parents = set() 

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

47 if cfg.parent: 

48 if cfg.parent not in parents: 

49 yield cfg.parent 

50 parents.add(cfg.parent) 

51 else: 

52 yield cfg 

53 

54 default_config_id = ConfigId('general') 

55 

56 #: The value of this setting. 

57 value: 'T' 

58 

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

60 type: 'AbstractFormatter[T]' 

61 

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

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

64 

65 

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

67 

68 @classmethod 

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

70 ''' 

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

72 

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

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

75 

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

77 ''' 

78 cls._key_changer.append(callback) 

79 

80 @classmethod 

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

82 ''' 

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

84 ''' 

85 return cls._key_changer.pop() 

86 

87 

88 def __init__(self, 

89 key: str, 

90 default: T, *, 

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

92 unit: 'str|None' = None, 

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

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

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

96 ): 

97 ''' 

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

99 :param default: The default value of this setting 

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

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

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

103 :param help: A description of this setting 

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

105 

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

107 * :class:`str` 

108 * :class:`int` 

109 * :class:`float` 

110 * :class:`bool` 

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

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

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

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

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

116 

117 :raises ValueError: if key is not unique 

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

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

120 :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` 

121 ''' 

122 if state.has_config_file_been_instantiated: 

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

124 if self._key_changer: 

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

126 

127 if type is None: 

128 if isinstance(default, list): 

129 if not default: 

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

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

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

133 elif isinstance(default, set): 

134 if not default: 

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

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

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

138 elif isinstance(default, dict): 

139 if not default: 

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

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

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

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

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

145 else: 

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

147 else: 

148 if unit is not None: 

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

150 if allowed_values is not None: 

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

152 

153 type.set_config_key(key) 

154 

155 self._key = key 

156 self.value = default 

157 self.type = type 

158 self.help = help 

159 self.parent = parent 

160 

161 cls = builtins.type(self) 

162 if key in cls.instances: 

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

164 cls.instances[key] = self 

165 

166 @property 

167 def key(self) -> str: 

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

169 return self._key 

170 

171 @key.setter 

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

173 if key in self.instances: 

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

175 del self.instances[self._key] 

176 self._key = key 

177 self.type.config_key = key 

178 self.instances[key] = self 

179 

180 

181 @typing.overload 

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

183 pass 

184 

185 @typing.overload 

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

187 pass 

188 

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

190 if instance is None: 

191 return self 

192 

193 return self.value 

194 

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

196 self.value = value 

197 

198 def __repr__(self) -> str: 

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

200 

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

202 ''' 

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

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

205 ''' 

206 if config_id is None: 

207 config_id = self.default_config_id 

208 if config_id != self.default_config_id: 

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

210 self.value = value 

211 

212 def wants_to_be_exported(self) -> bool: 

213 return True 

214 

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

216 ''' 

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

218 

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

220 ''' 

221 return self.value 

222 

223class ExplicitConfig(Config[T]): 

224 

225 ''' 

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

227 ''' 

228 

229 def __init__(self, 

230 key: str, 

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

232 unit: 'str|None' = None, 

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

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

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

236 ): 

237 ''' 

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

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

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

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

242 :param help: A description of this setting 

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

244 ''' 

245 if type is None: 

246 if not allowed_values: 

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

248 elif isinstance(allowed_values, dict): 

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

250 else: 

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

252 if not isinstance(type, AbstractFormatter): 

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

254 super().__init__(key, 

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

256 type = type, 

257 help = help, 

258 parent = parent, 

259 ) 

260 

261 @typing.overload 

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

263 pass 

264 

265 @typing.overload 

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

267 pass 

268 

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

270 if instance is None: 

271 return self 

272 

273 if self.value is None: 

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

275 return self.value 

276 

277 

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

279 

280 ''' 

281 A container for several settings which belong together. 

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

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

284 

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

286 ''' 

287 

288 class Sort(enum.Enum): 

289 NAME = enum.auto() 

290 ENUM_VALUE = enum.auto() 

291 NONE = enum.auto() 

292 

293 def __init__(self, 

294 key_prefix: str, 

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

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

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

298 unit: 'str|None' = None, 

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

300 help: 'str|None' = None, 

301 sort: Sort = Sort.NAME, 

302 ) -> None: 

303 ''' 

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

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

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

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

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

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

310 :param help: A help for all items 

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

312 

313 :raises ValueError: if a key is not unique 

314 ''' 

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

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

317 self.allowed_values = allowed_values 

318 self.sort = sort 

319 

320 self.key_prefix = key_prefix 

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

322 self.type = type 

323 self.unit = unit 

324 self.help = help 

325 self.ignore_keys = ignore_keys 

326 

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

328 self[key] = val 

329 

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

331 ''' 

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

333 

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

335 ''' 

336 key_str = format_primitive_value(key) 

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

338 

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

340 if key in self.ignore_keys: 

341 self._ignored_values[key] = val 

342 return 

343 

344 c = self._values.get(key) 

345 if c is None: 

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

347 else: 

348 c.value = val 

349 

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

351 ''' 

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

353 ''' 

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

355 

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

357 if key in self.ignore_keys: 

358 return self._ignored_values[key] 

359 else: 

360 return self._values[key].value 

361 

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

363 try: 

364 return self[key] 

365 except KeyError: 

366 return default 

367 

368 def __repr__(self) -> str: 

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

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

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

372 

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

374 if key in self.ignore_keys: 

375 return key in self._ignored_values 

376 else: 

377 return key in self._values 

378 

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

380 yield from self._values 

381 yield from self._ignored_values 

382 

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

384 yield from self._values.keys() 

385 yield from self._ignored_values.keys() 

386 

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

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

389 yield cfg.value 

390 yield from self._ignored_values.values() 

391 

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

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

394 yield key, cfg.value 

395 yield from self._ignored_values.items() 

396 

397 

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

399 ''' 

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

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

402 ''' 

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

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

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

406 yield from self._values.values() 

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

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

409 #keys = tuple(self._values) 

410 if is_mapping_with_enum_keys(self._values): 

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

412 yield self._values[key] 

413 else: 

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

415 else: 

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

417 

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

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

420 

421 

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

423 

424class MultiConfig(Config[T]): 

425 

426 ''' 

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

428 

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

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

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

432 

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

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

435 ''' 

436 

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

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

439 

440 #: Stores the values for specific objects. 

441 values: 'dict[ConfigId, T]' 

442 

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

444 value: 'T' 

445 

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

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

448 

449 @classmethod 

450 def reset(cls) -> None: 

451 ''' 

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

453 ''' 

454 cls.config_ids.clear() 

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

456 if isinstance(cfg, MultiConfig): 

457 cfg.values.clear() 

458 

459 def __init__(self, 

460 key: str, 

461 default: T, *, 

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

463 unit: 'str|None' = None, 

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

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

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

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

468 ) -> None: 

469 ''' 

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

471 :param default: The default value of this setting 

472 :param help: A description of this setting 

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

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

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

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

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

478 ''' 

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

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

481 self.check_config_id = check_config_id 

482 

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

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

485 # But without copy-pasting this code mypy complains 

486 # "Signature of __get__ incompatible with supertype Config" 

487 @typing.overload 

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

489 pass 

490 

491 @typing.overload 

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

493 pass 

494 

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

496 if instance is None: 

497 return self 

498 

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

500 

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

502 config_id = instance.config_id 

503 self.values[config_id] = value 

504 if config_id not in self.config_ids: 

505 self.config_ids.append(config_id) 

506 

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

508 ''' 

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

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

511 

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

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

514 

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

516 

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

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

519 ''' 

520 if config_id is None: 

521 config_id = self.default_config_id 

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

523 self.check_config_id(self, config_id) 

524 if config_id == self.default_config_id: 

525 self.value = value 

526 else: 

527 self.values[config_id] = value 

528 if config_id not in self.config_ids: 

529 self.config_ids.append(config_id) 

530 

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

532 ''' 

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

534 ''' 

535 if config_id is None: 

536 config_id = self.default_config_id 

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

538 

539 

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

541 

542 ''' 

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

544 

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

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

547 ''' 

548 

549 def __init__(self, 

550 key_prefix: str, 

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

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

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

554 unit: 'str|None' = None, 

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

556 help: 'str|None' = None, 

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

558 ) -> None: 

559 ''' 

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

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

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

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

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

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

566 :param help: A help for all items 

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

568 

569 :raises ValueError: if a key is not unique 

570 ''' 

571 self.check_config_id = check_config_id 

572 super().__init__( 

573 key_prefix = key_prefix, 

574 default_values = default_values, 

575 type = type, 

576 ignore_keys = ignore_keys, 

577 unit = unit, 

578 help = help, 

579 allowed_values = allowed_values, 

580 ) 

581 

582 @typing.overload 

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

584 pass 

585 

586 @typing.overload 

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

588 pass 

589 

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

591 if instance is None: 

592 return self 

593 

594 return InstanceSpecificDictMultiConfig(self, instance.config_id) 

595 

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

597 raise NotImplementedError() 

598 

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

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

601 

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

603 

604 ''' 

605 An intermediate instance which is returned when accsessing 

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

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

608 ''' 

609 

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

611 self.mdc = mdc 

612 self.config_id = config_id 

613 

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

615 if key in self.mdc.ignore_keys: 

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

617 

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

619 if c is None: 

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

621 else: 

622 c.__set__(self, val) 

623 

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

625 if key in self.mdc.ignore_keys: 

626 return self.mdc._ignored_values[key] 

627 else: 

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