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 T_DEFAULT = typing.TypeVar('T_DEFAULT')

21

22 class 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

29 class 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

237 class 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

299 class 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

447 def 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

453 class 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

569 class 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

631 class 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)