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

1#!./runmodule.sh

2

3 import builtins

4 import enum

5 import typing

6 from collections.abc import Iterable, Iterator, Container, Sequence, Mapping, Callable

7

8 if typing.TYPE_CHECKING:

9 from typing_extensions import Self

10

11 from .formatters import AbstractFormatter, CopyableAbstractFormatter, Primitive, List, Set, Dict, format_primitive_value

12 from . 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.

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

17

18 T_KEY = typing.TypeVar('T_KEY')

19 T = typing.TypeVar('T')

20

21 class TimingError(Exception):

22

23 '''

24 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`.

25 '''

26

27

28 class 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 '''

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

170 This must be unique.

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

172 '''

173 return self._key

174

175 @key.setter

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

177 if state.has_any_config_file_been_instantiated:

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

179 if key in self.instances:

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

181 del self.instances[self._key]

182 self._key = key

183 self.type.config_key = key

184 self.instances[key] = self

185

186

187 @typing.overload

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

189 pass

190

191 @typing.overload

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

193 pass

194

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

196 if instance is None:

197 return self

198

199 return self.value

200

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

202 self.value = value

203

204 def __repr__(self) -> str:

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

206

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

208 '''

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

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

211 '''

212 if config_id is None:

213 config_id = self.default_config_id

214 if config_id != self.default_config_id:

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

216 self.value = value

217

218 def wants_to_be_exported(self) -> bool:

219 return True

220

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

222 '''

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

224

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

226 '''

227 return self.value

228

229 class ExplicitConfig(Config[T]):

230

231 '''

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

233 '''

234

235 def __init__(self,

236 key: str,

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

238 unit: 'str|None' = None,

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

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

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

242 ):

243 '''

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

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

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

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

248 :param help: A description of this setting

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

250 '''

251 if type is None:

252 if not allowed_values:

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

254 elif isinstance(allowed_values, dict):

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

256 else:

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

258 if not isinstance(type, AbstractFormatter):

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

260 super().__init__(key,

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

262 type = type,

263 help = help,

264 parent = parent,

265 )

266

267 @typing.overload

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

269 pass

270

271 @typing.overload

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

273 pass

274

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

276 if instance is None:

277 return self

278

279 if self.value is None:

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

281 return self.value

282

283

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

285

286 '''

287 A container for several settings which belong together.

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

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

290

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

292 '''

293

294 class Sort(enum.Enum):

295 NAME = enum.auto()

296 ENUM_VALUE = enum.auto()

297 NONE = enum.auto()

298

299 def __init__(self,

300 key_prefix: str,

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

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

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

304 unit: 'str|None' = None,

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

306 help: 'str|None' = None,

307 sort: Sort = Sort.NAME,

308 ) -> None:

309 '''

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

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

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

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

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

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

316 :param help: A help for all items

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

318

319 :raises ValueError: if a key is not unique

320 '''

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

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

323 self.allowed_values = allowed_values

324 self.sort = sort

325

326 self.key_prefix = key_prefix

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

328 self.type = type

329 self.unit = unit

330 self.help = help

331 self.ignore_keys = ignore_keys

332

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

334 self[key] = val

335

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

337 '''

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

339

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

341 '''

342 key_str = format_primitive_value(key)

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

344

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

346 if key in self.ignore_keys:

347 self._ignored_values[key] = val

348 return

349

350 c = self._values.get(key)

351 if c is None:

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

353 else:

354 c.value = val

355

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

357 '''

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

359 '''

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

361

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

363 if key in self.ignore_keys:

364 return self._ignored_values[key]

365 else:

366 return self._values[key].value

367

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

369 try:

370 return self[key]

371 except KeyError:

372 return default

373

374 def __repr__(self) -> str:

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

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

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

378

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

380 if key in self.ignore_keys:

381 return key in self._ignored_values

382 else:

383 return key in self._values

384

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

386 yield from self._values

387 yield from self._ignored_values

388

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

390 yield from self._values.keys()

391 yield from self._ignored_values.keys()

392

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

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

395 yield cfg.value

396 yield from self._ignored_values.values()

397

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

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

400 yield key, cfg.value

401 yield from self._ignored_values.items()

402

403

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

405 '''

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

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

408 '''

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

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

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

412 yield from self._values.values()

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

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

415 #keys = tuple(self._values)

416 if is_mapping_with_enum_keys(self._values):

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

418 yield self._values[key]

419 else:

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

421 else:

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

423

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

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

426

427

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

429

430 class MultiConfig(Config[T]):

431

432 '''

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

434

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

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

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

438

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

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

441 '''

442

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

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

445

446 #: Stores the values for specific objects.

447 values: 'dict[ConfigId, T]'

448

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

450 value: 'T'

451

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

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

454

455 @classmethod

456 def reset(cls) -> None:

457 '''

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

459 '''

460 cls.config_ids.clear()

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

462 if isinstance(cfg, MultiConfig):

463 cfg.values.clear()

464

465 def __init__(self,

466 key: str,

467 default: T, *,

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

469 unit: 'str|None' = None,

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

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

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

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

474 ) -> None:

475 '''

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

477 :param default: The default value of this setting

478 :param help: A description of this setting

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

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

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

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

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

484 '''

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

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

487 self.check_config_id = check_config_id

488

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

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

491 # But without copy-pasting this code mypy complains

492 # "Signature of __get__ incompatible with supertype Config"

493 @typing.overload

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

495 pass

496

497 @typing.overload

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

499 pass

500

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

502 if instance is None:

503 return self

504

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

506

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

508 config_id = instance.config_id

509 self.values[config_id] = value

510 if config_id not in self.config_ids:

511 self.config_ids.append(config_id)

512

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

514 '''

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

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

517

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

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

520

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

522

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

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

525 '''

526 if config_id is None:

527 config_id = self.default_config_id

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

529 self.check_config_id(self, config_id)

530 if config_id == self.default_config_id:

531 self.value = value

532 else:

533 self.values[config_id] = value

534 if config_id not in self.config_ids:

535 self.config_ids.append(config_id)

536

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

538 '''

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

540 '''

541 if config_id is None:

542 config_id = self.default_config_id

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

544

545

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

547

548 '''

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

550

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

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

553 '''

554

555 def __init__(self,

556 key_prefix: str,

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

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

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

560 unit: 'str|None' = None,

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

562 help: 'str|None' = None,

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

564 ) -> None:

565 '''

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

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

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

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

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

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

572 :param help: A help for all items

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

574

575 :raises ValueError: if a key is not unique

576 '''

577 self.check_config_id = check_config_id

578 super().__init__(

579 key_prefix = key_prefix,

580 default_values = default_values,

581 type = type,

582 ignore_keys = ignore_keys,

583 unit = unit,

584 help = help,

585 allowed_values = allowed_values,

586 )

587

588 @typing.overload

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

590 pass

591

592 @typing.overload

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

594 pass

595

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

597 if instance is None:

598 return self

599

600 return InstanceSpecificDictMultiConfig(self, instance.config_id)

601

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

603 raise NotImplementedError()

604

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

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

607

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

609

610 '''

611 An intermediate instance which is returned when accsessing

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

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

614 '''

615

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

617 self.mdc = mdc

618 self.config_id = config_id

619

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

621 if key in self.mdc.ignore_keys:

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

623

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

625 if c is None:

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

627 else:

628 c.__set__(self, val)

629

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

631 if key in self.mdc.ignore_keys:

632 return self.mdc._ignored_values[key]

633 else:

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