Coverage for .tox/cov/lib/python3.11/site-packages/confattr/config.py: 100%
261 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 09:33 +0100
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 09:33 +0100
1#!./runmodule.sh
3import builtins
4import enum
5import typing
6from collections.abc import Iterable, Iterator, Container, Sequence, Mapping, Callable
8if typing.TYPE_CHECKING:
9 from typing_extensions import Self
11from .formatters import AbstractFormatter, CopyableAbstractFormatter, Primitive, List, Set, Dict, format_primitive_value
12from . import state
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)
18T_KEY = typing.TypeVar('T_KEY')
19T = typing.TypeVar('T')
20T_DEFAULT = typing.TypeVar('T_DEFAULT')
22class TimingError(Exception):
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 '''
29class Config(typing.Generic[T]):
31 '''
32 Each instance of this class represents a setting which can be changed in a config file.
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 '''
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]]' = {}
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
55 default_config_id = ConfigId('general')
57 #: The value of this setting.
58 value: 'T'
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]'
63 #: A description of this setting or a description for each allowed value.
64 help: 'str|dict[T, str]|None'
67 _key_changer: 'list[Callable[[str], str]]' = []
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.
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.
77 :param callback: A function which takes the key as argument and returns the modified key.
78 '''
79 cls._key_changer.append(callback)
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()
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`
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.)
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)
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.")
154 type.set_config_key(key)
156 self._key = key
157 self.value = default
158 self.type = type
159 self.help = help
160 self.parent = parent
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
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
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
188 @typing.overload
189 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self':
190 pass
192 @typing.overload
193 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> T:
194 pass
196 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'T|Self':
197 if instance is None:
198 return self
200 return self.value
202 def __set__(self: 'Config[T]', instance: typing.Any, value: T) -> None:
203 self.value = value
205 def __repr__(self) -> str:
206 return '%s(%s, ...)' % (type(self).__name__, ', '.join(repr(a) for a in (self.key, self.value)))
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
219 def wants_to_be_exported(self) -> bool:
220 return True
222 def get_value(self, config_id: 'ConfigId|None') -> T:
223 '''
224 :return: :attr:`~confattr.config.Config.value`
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
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
237class ExplicitConfig(Config[T]):
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.
242 You can use :meth:`~confattr.config.ExplicitConfig.is_value_valid` in order to check whether this config has a value or not.
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 '''
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 )
279 @typing.overload
280 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self':
281 pass
283 @typing.overload
284 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> T:
285 pass
287 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'T|Self':
288 if instance is None:
289 return self
291 if self.value is None:
292 raise TypeError(f"value for {self.key!r} has not been set")
293 return self.value
295 def is_value_valid(self) -> bool:
296 return self.value is not None
299class DictConfig(typing.Generic[T_KEY, T]):
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.
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 '''
309 class Sort(enum.Enum):
310 NAME = enum.auto()
311 ENUM_VALUE = enum.auto()
312 NONE = enum.auto()
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
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
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
348 for key, val in default_values.items():
349 self[key] = val
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.
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)
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
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
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)
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
383 @typing.overload
384 def get(self, key: T_KEY) -> 'T|None':
385 ...
387 @typing.overload
388 def get(self, key: T_KEY, default: T_DEFAULT) -> 'T|T_DEFAULT':
389 ...
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
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)
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
408 def __iter__(self) -> 'Iterator[T_KEY]':
409 yield from self._values
410 yield from self._ignored_values
412 def keys(self) -> 'Iterator[T_KEY]':
413 yield from self._values.keys()
414 yield from self._ignored_values.keys()
416 def values(self) -> 'Iterator[T]':
417 for cfg in self._values.values():
418 yield cfg.value
419 yield from self._ignored_values.values()
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()
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)
447def is_mapping_with_enum_keys(m: 'Mapping[typing.Any, T]') -> 'typing.TypeGuard[Mapping[enum.Enum, T]]':
448 return all(isinstance(key, enum.Enum) for key in m.keys())
451# ========== settings which can have different values for different groups ==========
453class MultiConfig(Config[T]):
455 '''
456 A setting which can have different values for different objects.
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.
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 '''
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]' = []
469 #: Stores the values for specific objects.
470 values: 'dict[ConfigId, T]'
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'
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'
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()
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
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
520 @typing.overload
521 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> T:
522 pass
524 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'T|Self':
525 if instance is None:
526 return self
528 return self.values.get(instance.config_id, self.value)
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)
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`.
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`.
544 If you want to set the default value you can also set :attr:`~confattr.config.MultiConfig.value` directly.
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)
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)
569class MultiDictConfig(DictConfig[T_KEY, T]):
571 '''
572 A container for several settings which can have different values for different objects.
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 '''
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`
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 )
611 @typing.overload
612 def __get__(self, instance: None, owner: typing.Any = None) -> 'Self':
613 pass
615 @typing.overload
616 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'InstanceSpecificDictMultiConfig[T_KEY, T]':
617 pass
619 def __get__(self, instance: typing.Any, owner: typing.Any = None) -> 'InstanceSpecificDictMultiConfig[T_KEY, T]|Self':
620 if instance is None:
621 return self
623 return InstanceSpecificDictMultiConfig(self, instance.config_id)
625 def __set__(self: 'MultiDictConfig[T_KEY, T]', instance: typing.Any, value: 'InstanceSpecificDictMultiConfig[T_KEY, T]') -> typing.NoReturn:
626 raise NotImplementedError()
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)
631class InstanceSpecificDictMultiConfig(typing.Generic[T_KEY, T]):
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 '''
639 def __init__(self, mdc: 'MultiDictConfig[T_KEY, T]', config_id: ConfigId) -> None:
640 self.mdc = mdc
641 self.config_id = config_id
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)
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)
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)