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.
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.")
39 """Class of ``False`` singleton"""
59 """Exception to store and/or raise a message from a schema issue."""
61 def __init__(self, level: typing.Literal[
'warn',
'invalid'], msg: str):
66 return f
"[cfg schema {self.level}] {self.args[0]}"
70 """Base class used for configuration"""
75 def from_toml(cls, schema_file: pathlib.Path, cfg_file: pathlib.Path, deprecated: dict[str, str]) ->
"Config":
79 log.debug(
"load schema file: %s", schema_file)
80 cfg = cls(cfg_schema=
toml_load(schema_file), deprecated=deprecated)
81 if not cfg_file.exists():
82 log.warning(
"missing config file: %s", cfg_file)
87 log.debug(
"load config file: %s", cfg_file)
90 is_valid, issue_list = cfg.validate(upd_cfg)
91 for msg
in issue_list:
94 raise TypeError(f
"schema of {cfg_file} is invalid!")
98 def __init__(self, cfg_schema: dict[str, typing.Any], deprecated: dict[str, str]):
99 """Constructor of class Config.
101 :param cfg_schema: Schema of the configuration
102 :param deprecated: dictionary that maps deprecated configuration names to a messages
104 These values are needed for validation, see :py:obj:`validate`.
109 self.
cfg: dict[str, typing.Any] = copy.deepcopy(cfg_schema)
115 """Validation of dictionary ``cfg`` on :py:obj:`Config.SCHEMA`.
116 Validation is done by :py:obj:`validate`."""
120 def update(self, upd_cfg: dict[str, typing.Any]):
121 """Update this configuration by ``upd_cfg``."""
126 """Returns default value of field ``name`` in ``self.cfg_schema``."""
129 def get(self, name: str, default: typing.Any = UNSET, replace: bool =
True) -> typing.Any:
130 """Returns the value to which ``name`` points in the configuration.
132 If there is no such ``name`` in the config and the ``default`` is
133 :py:obj:`UNSET`, a :py:obj:`KeyError` is raised.
137 val = parent.get(name.split(
'.')[-1], UNSET)
143 if replace
and isinstance(val, str):
147 def set(self, name: str, val: typing.Any):
148 """Set the value to which ``name`` points in the configuration.
150 If there is no such ``name`` in the config, a :py:obj:`KeyError` is
154 parent[name.split(
'.')[-1]] = val
157 parent_name =
'.'.join(name.split(
'.')[:-1])
159 parent: dict[str, typing.Any] =
value(parent_name, self.
cfg)
162 if (parent
is UNSET)
or (
not isinstance(parent, dict)):
163 raise KeyError(parent_name)
166 def path(self, name: str, default: typing.Any = UNSET):
167 """Get a :py:class:`pathlib.Path` object from a config string."""
169 val = self.
get(name, default)
174 return pathlib.Path(str(val))
176 def pyobj(self, name: str, default: typing.Any = UNSET):
177 """Get python object referred by full qualiffied name (FQN) in the config
180 fqn = self.
get(name, default)
185 (modulename, name) = str(fqn).rsplit(
'.', 1)
186 m = __import__(modulename, {}, {}, [name], 0)
187 return getattr(m, name)
192 with open(file_name,
"rb")
as f:
193 return tomllib.load(f)
194 except tomllib.TOMLDecodeError
as exc:
195 msg = str(exc).replace(
'\t',
'').replace(
'\n',
' ')
196 log.error(
"%s: %s", file_name, msg)
203def value(name: str, data_dict: dict[str, typing.Any]):
204 """Returns the value to which ``name`` points in the ``dat_dict``.
213 >>> value('foobar', data_dict)
215 >>> value('foo.bar', data_dict)
217 >>> value('foo.bar.xxx', data_dict)
223 for part
in name.split(
'.'):
224 if isinstance(ret_val, dict):
225 ret_val = ret_val.get(part, UNSET)
232 schema_dict: dict[str, typing.Any], data_dict: dict[str, typing.Any], deprecated: dict[str, str]
233) -> tuple[bool, list[SchemaIssue]]:
234 """Deep validation of dictionary in ``data_dict`` against dictionary in
235 ``schema_dict``. Argument deprecated is a dictionary that maps deprecated
236 configuration names to a messages::
239 "foo.bar" : "config 'foo.bar' is deprecated, use 'bar.foo'",
243 The function returns a python tuple ``(is_valid, issue_list)``:
246 A bool value indicating ``data_dict`` is valid or not.
249 A list of messages (:py:obj:`SchemaIssue`) from the validation::
251 [schema warn] data_dict: deprecated 'fontlib.foo': <DEPRECATED['foo.bar']>
252 [schema invalid] data_dict: key unknown 'fontlib.foo'
253 [schema invalid] data_dict: type mismatch 'fontlib.foo': expected ..., is ...
255 If ``schema_dict`` or ``data_dict`` is not a dictionary type a
256 :py:obj:`SchemaIssue` is raised.
259 names: list[str] = []
260 is_valid: bool =
True
261 issue_list: list[SchemaIssue] = []
263 if not isinstance(schema_dict, dict):
264 raise SchemaIssue(
'invalid',
"schema_dict is not a dict type")
265 if not isinstance(data_dict, dict):
266 raise SchemaIssue(
'invalid', f
"data_dict issue{'.'.join(names)} is not a dict type")
268 is_valid, issue_list =
_validate(names, issue_list, schema_dict, data_dict, deprecated)
269 return is_valid, issue_list
274 issue_list: list[SchemaIssue],
275 schema_dict: dict[str, typing.Any],
276 data_dict: dict[str, typing.Any],
277 deprecated: dict[str, str],
278) -> tuple[bool, list[SchemaIssue]]:
282 data_value: dict[str, typing.Any]
283 for key, data_value
in data_dict.items():
286 name =
'.'.join(names)
288 deprecated_msg = deprecated.get(name)
291 issue_list.append(
SchemaIssue(
'warn', f
"data_dict '{name}': deprecated - {deprecated_msg}"))
293 schema_value =
value(name, schema_dict)
295 if schema_value
is UNSET:
296 if not deprecated_msg:
297 issue_list.append(
SchemaIssue(
'invalid', f
"data_dict '{name}': key unknown in schema_dict"))
300 elif type(schema_value) != type(data_value):
304 (f
"data_dict: type mismatch '{name}':" f
" expected {type(schema_value)}, is: {type(data_value)}"),
309 elif isinstance(data_value, dict):
310 _valid, _ =
_validate(names, issue_list, schema_dict, data_value, deprecated)
311 is_valid = is_valid
and _valid
314 return is_valid, issue_list
317def dict_deepupdate(base_dict: dict[str, typing.Any], upd_dict: dict[str, typing.Any], names: list[str] |
None =
None):
318 """Deep-update of dictionary in ``base_dict`` by dictionary in ``upd_dict``.
320 For each ``upd_key`` & ``upd_val`` pair in ``upd_dict``:
322 0. If types of ``base_dict[upd_key]`` and ``upd_val`` do not match raise a
325 1. If ``base_dict[upd_key]`` is a dict: recursively deep-update it by ``upd_val``.
327 2. If ``base_dict[upd_key]`` not exist: set ``base_dict[upd_key]`` from a
328 (deep-) copy of ``upd_val``.
330 3. If ``upd_val`` is a list, extend list in ``base_dict[upd_key]`` by the
333 4. If ``upd_val`` is a set, update set in ``base_dict[upd_key]`` by set in
337 if not isinstance(base_dict, dict):
338 raise TypeError(
"argument 'base_dict' is not a dictionary type")
339 if not isinstance(upd_dict, dict):
340 raise TypeError(
"argument 'upd_dict' is not a dictionary type")
345 for upd_key, upd_val
in upd_dict.items():
348 if isinstance(upd_val, dict):
350 if upd_key
in base_dict:
352 if not isinstance(base_dict[upd_key], dict):
353 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a dict type in base_dict")
365 base_dict[upd_key] = copy.deepcopy(upd_val)
367 elif isinstance(upd_val, list):
369 if upd_key
in base_dict:
372 if not isinstance(base_dict[upd_key], list):
373 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a list type in base_dict")
374 base_dict[upd_key].extend(upd_val)
379 base_dict[upd_key] = copy.deepcopy(upd_val)
381 elif isinstance(upd_val, set):
383 if upd_key
in base_dict:
385 if not isinstance(base_dict[upd_key], set):
386 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a set type in base_dict")
387 base_dict[upd_key].update(upd_val.copy())
392 base_dict[upd_key] = upd_val.copy()
397 base_dict[upd_key] = copy.copy(upd_val)
pyobj(self, str name, typing.Any default=UNSET)
"Config" from_toml(cls, pathlib.Path schema_file, pathlib.Path cfg_file, dict[str, str] deprecated)
path(self, str name, typing.Any default=UNSET)
dict[str, typing.Any] _get_parent_dict(self, str name)
update(self, dict[str, typing.Any] upd_cfg)
__init__(self, dict[str, typing.Any] cfg_schema, dict[str, str] deprecated)
set(self, str name, typing.Any val)
typing.Any __getitem__(self, str key)
typing.Any get(self, str name, typing.Any default=UNSET, bool replace=True)
dict[str, str] deprecated
validate(self, dict[str, typing.Any] cfg)
dict[str, typing.Any] cfg_schema
dict[str, typing.Any] cfg
__init__(self, typing.Literal['warn', 'invalid'] level, str msg)
set_global_cfg("Config" cfg)
tuple[bool, list[SchemaIssue]] validate(dict[str, typing.Any] schema_dict, dict[str, typing.Any] data_dict, dict[str, str] deprecated)
toml_load(str|pathlib.Path file_name)
tuple[bool, list[SchemaIssue]] _validate(list[str] names, list[SchemaIssue] issue_list, dict[str, typing.Any] schema_dict, dict[str, typing.Any] data_dict, dict[str, str] deprecated)
dict_deepupdate(dict[str, typing.Any] base_dict, dict[str, typing.Any] upd_dict, list[str]|None names=None)
"Config" get_global_cfg()
value(str name, dict[str, typing.Any] data_dict)