2"""Configuration class :py:class:`Config` with deep-update, schema validation
5The :py:class:`Config` class implements a configuration that is based on
6structured dictionaries. The configuration schema is defined in a dictionary
7structure and the configuration data is given in a dictionary structure.
9from __future__
import annotations
16from ..compat
import tomllib
18__all__ = [
'Config',
'UNSET',
'SchemaIssue',
'set_global_cfg',
'get_global_cfg']
20log = logging.getLogger(__name__)
22CFG: Config |
None =
None
23"""Global config of the botdetection."""
33 raise ValueError(
"Botdetection's config is not yet initialized.")
38 """Class of ``False`` singleton"""
57 """Exception to store and/or raise a message from a schema issue."""
59 def __init__(self, level: typing.Literal[
'warn',
'invalid'], msg: str):
64 return f
"[cfg schema {self.level}] {self.args[0]}"
68 """Base class used for configuration"""
73 def from_toml(cls, schema_file: pathlib.Path, cfg_file: pathlib.Path, deprecated: dict[str, str]) -> Config:
77 log.debug(
"load schema file: %s", schema_file)
78 cfg = cls(cfg_schema=
toml_load(schema_file), deprecated=deprecated)
79 if not cfg_file.exists():
80 log.warning(
"missing config file: %s", cfg_file)
85 log.debug(
"load config file: %s", cfg_file)
88 is_valid, issue_list = cfg.validate(upd_cfg)
89 for msg
in issue_list:
92 raise TypeError(f
"schema of {cfg_file} is invalid!")
96 def __init__(self, cfg_schema: dict[str, typing.Any], deprecated: dict[str, str]):
97 """Constructor of class Config.
99 :param cfg_schema: Schema of the configuration
100 :param deprecated: dictionary that maps deprecated configuration names to a messages
102 These values are needed for validation, see :py:obj:`validate`.
107 self.
cfg = copy.deepcopy(cfg_schema)
113 """Validation of dictionary ``cfg`` on :py:obj:`Config.SCHEMA`.
114 Validation is done by :py:obj:`validate`."""
119 """Update this configuration by ``upd_cfg``."""
124 """Returns default value of field ``name`` in ``self.cfg_schema``."""
127 def get(self, name: str, default: typing.Any = UNSET, replace: bool =
True) -> typing.Any:
128 """Returns the value to which ``name`` points in the configuration.
130 If there is no such ``name`` in the config and the ``default`` is
131 :py:obj:`UNSET`, a :py:obj:`KeyError` is raised.
135 val = parent.get(name.split(
'.')[-1], UNSET)
141 if replace
and isinstance(val, str):
145 def set(self, name: str, val):
146 """Set the value to which ``name`` points in the configuration.
148 If there is no such ``name`` in the config, a :py:obj:`KeyError` is
152 parent[name.split(
'.')[-1]] = val
155 parent_name =
'.'.join(name.split(
'.')[:-1])
157 parent =
value(parent_name, self.
cfg)
160 if (parent
is UNSET)
or (
not isinstance(parent, dict)):
161 raise KeyError(parent_name)
164 def path(self, name: str, default=UNSET):
165 """Get a :py:class:`pathlib.Path` object from a config string."""
167 val = self.
get(name, default)
172 return pathlib.Path(str(val))
174 def pyobj(self, name, default=UNSET):
175 """Get python object referred by full qualiffied name (FQN) in the config
178 fqn = self.
get(name, default)
183 (modulename, name) = str(fqn).rsplit(
'.', 1)
184 m = __import__(modulename, {}, {}, [name], 0)
185 return getattr(m, name)
190 with open(file_name,
"rb")
as f:
191 return tomllib.load(f)
192 except tomllib.TOMLDecodeError
as exc:
193 msg = str(exc).replace(
'\t',
'').replace(
'\n',
' ')
194 log.error(
"%s: %s", file_name, msg)
201def value(name: str, data_dict: dict):
202 """Returns the value to which ``name`` points in the ``dat_dict``.
211 >>> value('foobar', data_dict)
213 >>> value('foo.bar', data_dict)
215 >>> value('foo.bar.xxx', data_dict)
221 for part
in name.split(
'.'):
222 if isinstance(ret_val, dict):
223 ret_val = ret_val.get(part, UNSET)
230 schema_dict: dict[str, typing.Any], data_dict: dict[str, typing.Any], deprecated: dict[str, str]
231) -> tuple[bool, list[str]]:
232 """Deep validation of dictionary in ``data_dict`` against dictionary in
233 ``schema_dict``. Argument deprecated is a dictionary that maps deprecated
234 configuration names to a messages::
237 "foo.bar" : "config 'foo.bar' is deprecated, use 'bar.foo'",
241 The function returns a python tuple ``(is_valid, issue_list)``:
244 A bool value indicating ``data_dict`` is valid or not.
247 A list of messages (:py:obj:`SchemaIssue`) from the validation::
249 [schema warn] data_dict: deprecated 'fontlib.foo': <DEPRECATED['foo.bar']>
250 [schema invalid] data_dict: key unknown 'fontlib.foo'
251 [schema invalid] data_dict: type mismatch 'fontlib.foo': expected ..., is ...
253 If ``schema_dict`` or ``data_dict`` is not a dictionary type a
254 :py:obj:`SchemaIssue` is raised.
261 if not isinstance(schema_dict, dict):
262 raise SchemaIssue(
'invalid',
"schema_dict is not a dict type")
263 if not isinstance(data_dict, dict):
264 raise SchemaIssue(
'invalid', f
"data_dict issue{'.'.join(names)} is not a dict type")
266 is_valid, issue_list =
_validate(names, issue_list, schema_dict, data_dict, deprecated)
267 return is_valid, issue_list
272 issue_list: typing.List,
273 schema_dict: typing.Dict,
274 data_dict: typing.Dict,
275 deprecated: typing.Dict[str, str],
276) -> typing.Tuple[bool, typing.List]:
280 for key, data_value
in data_dict.items():
283 name =
'.'.join(names)
285 deprecated_msg = deprecated.get(name)
288 issue_list.append(
SchemaIssue(
'warn', f
"data_dict '{name}': deprecated - {deprecated_msg}"))
290 schema_value =
value(name, schema_dict)
292 if schema_value
is UNSET:
293 if not deprecated_msg:
294 issue_list.append(
SchemaIssue(
'invalid', f
"data_dict '{name}': key unknown in schema_dict"))
297 elif type(schema_value) != type(data_value):
301 (f
"data_dict: type mismatch '{name}':" f
" expected {type(schema_value)}, is: {type(data_value)}"),
306 elif isinstance(data_value, dict):
307 _valid, _ =
_validate(names, issue_list, schema_dict, data_value, deprecated)
308 is_valid = is_valid
and _valid
311 return is_valid, issue_list
315 """Deep-update of dictionary in ``base_dict`` by dictionary in ``upd_dict``.
317 For each ``upd_key`` & ``upd_val`` pair in ``upd_dict``:
319 0. If types of ``base_dict[upd_key]`` and ``upd_val`` do not match raise a
322 1. If ``base_dict[upd_key]`` is a dict: recursively deep-update it by ``upd_val``.
324 2. If ``base_dict[upd_key]`` not exist: set ``base_dict[upd_key]`` from a
325 (deep-) copy of ``upd_val``.
327 3. If ``upd_val`` is a list, extend list in ``base_dict[upd_key]`` by the
330 4. If ``upd_val`` is a set, update set in ``base_dict[upd_key]`` by set in
334 if not isinstance(base_dict, dict):
335 raise TypeError(
"argument 'base_dict' is not a dictionary type")
336 if not isinstance(upd_dict, dict):
337 raise TypeError(
"argument 'upd_dict' is not a dictionary type")
342 for upd_key, upd_val
in upd_dict.items():
345 if isinstance(upd_val, dict):
347 if upd_key
in base_dict:
349 if not isinstance(base_dict[upd_key], dict):
350 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a dict type in base_dict")
362 base_dict[upd_key] = copy.deepcopy(upd_val)
364 elif isinstance(upd_val, list):
366 if upd_key
in base_dict:
369 if not isinstance(base_dict[upd_key], list):
370 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a list type in base_dict")
371 base_dict[upd_key].extend(upd_val)
376 base_dict[upd_key] = copy.deepcopy(upd_val)
378 elif isinstance(upd_val, set):
380 if upd_key
in base_dict:
382 if not isinstance(base_dict[upd_key], set):
383 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a set type in base_dict")
384 base_dict[upd_key].update(upd_val.copy())
389 base_dict[upd_key] = upd_val.copy()
394 base_dict[upd_key] = copy.copy(upd_val)
pyobj(self, name, default=UNSET)
Config from_toml(cls, pathlib.Path schema_file, pathlib.Path cfg_file, dict[str, str] deprecated)
__init__(self, dict[str, typing.Any] cfg_schema, dict[str, str] deprecated)
path(self, str name, default=UNSET)
typing.Any __getitem__(self, str key)
typing.Any get(self, str name, typing.Any default=UNSET, bool replace=True)
validate(self, dict[str, typing.Any] cfg)
update(self, dict upd_cfg)
_get_parent_dict(self, name)
__init__(self, typing.Literal['warn', 'invalid'] level, str msg)
dict_deepupdate(dict base_dict, dict upd_dict, names=None)
tuple[bool, list[str]] validate(dict[str, typing.Any] schema_dict, dict[str, typing.Any] data_dict, dict[str, str] deprecated)
typing.Tuple[bool, typing.List] _validate(typing.List names, typing.List issue_list, typing.Dict schema_dict, typing.Dict data_dict, typing.Dict[str, str] deprecated)
value(str name, dict data_dict)
set_global_cfg(Config cfg)