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
17from ..compat
import tomllib
19__all__ = [
'Config',
'UNSET',
'SchemaIssue']
21log = logging.getLogger(__name__)
25 """Class of ``False`` singelton"""
44 """Exception to store and/or raise a message from a schema issue."""
46 def __init__(self, level: typing.Literal[
'warn',
'invalid'], msg: str):
51 return f
"[cfg schema {self.level}] {self.args[0]}"
55 """Base class used for configuration"""
60 def from_toml(cls, schema_file: pathlib.Path, cfg_file: pathlib.Path, deprecated: dict) -> Config:
64 log.debug(
"load schema file: %s", schema_file)
65 cfg = cls(cfg_schema=
toml_load(schema_file), deprecated=deprecated)
66 if not cfg_file.exists():
67 log.warning(
"missing config file: %s", cfg_file)
72 log.debug(
"load config file: %s", cfg_file)
75 is_valid, issue_list = cfg.validate(upd_cfg)
76 for msg
in issue_list:
79 raise TypeError(f
"schema of {cfg_file} is invalid!")
83 def __init__(self, cfg_schema: typing.Dict, deprecated: typing.Dict[str, str]):
84 """Construtor of class Config.
86 :param cfg_schema: Schema of the configuration
87 :param deprecated: dictionary that maps deprecated configuration names to a messages
89 These values are needed for validation, see :py:obj:`validate`.
94 self.
cfg = copy.deepcopy(cfg_schema)
100 """Validation of dictionary ``cfg`` on :py:obj:`Config.SCHEMA`.
101 Validation is done by :py:obj:`validate`."""
106 """Update this configuration by ``upd_cfg``."""
111 """Returns default value of field ``name`` in ``self.cfg_schema``."""
114 def get(self, name: str, default: Any = UNSET, replace: bool =
True) -> Any:
115 """Returns the value to which ``name`` points in the configuration.
117 If there is no such ``name`` in the config and the ``default`` is
118 :py:obj:`UNSET`, a :py:obj:`KeyError` is raised.
122 val = parent.get(name.split(
'.')[-1], UNSET)
128 if replace
and isinstance(val, str):
132 def set(self, name: str, val):
133 """Set the value to which ``name`` points in the configuration.
135 If there is no such ``name`` in the config, a :py:obj:`KeyError` is
139 parent[name.split(
'.')[-1]] = val
142 parent_name =
'.'.join(name.split(
'.')[:-1])
144 parent =
value(parent_name, self.
cfg)
147 if (parent
is UNSET)
or (
not isinstance(parent, dict)):
148 raise KeyError(parent_name)
151 def path(self, name: str, default=UNSET):
152 """Get a :py:class:`pathlib.Path` object from a config string."""
154 val = self.
get(name, default)
159 return pathlib.Path(str(val))
161 def pyobj(self, name, default=UNSET):
162 """Get python object refered by full qualiffied name (FQN) in the config
165 fqn = self.
get(name, default)
170 (modulename, name) = str(fqn).rsplit(
'.', 1)
171 m = __import__(modulename, {}, {}, [name], 0)
172 return getattr(m, name)
177 with open(file_name,
"rb")
as f:
178 return tomllib.load(f)
179 except tomllib.TOMLDecodeError
as exc:
180 msg = str(exc).replace(
'\t',
'').replace(
'\n',
' ')
181 log.error(
"%s: %s", file_name, msg)
188def value(name: str, data_dict: dict):
189 """Returns the value to which ``name`` points in the ``dat_dict``.
198 >>> value('foobar', data_dict)
200 >>> value('foo.bar', data_dict)
202 >>> value('foo.bar.xxx', data_dict)
208 for part
in name.split(
'.'):
209 if isinstance(ret_val, dict):
210 ret_val = ret_val.get(part, UNSET)
217 schema_dict: typing.Dict, data_dict: typing.Dict, deprecated: typing.Dict[str, str]
218) -> typing.Tuple[bool, list]:
219 """Deep validation of dictionary in ``data_dict`` against dictionary in
220 ``schema_dict``. Argument deprecated is a dictionary that maps deprecated
221 configuration names to a messages::
224 "foo.bar" : "config 'foo.bar' is deprecated, use 'bar.foo'",
228 The function returns a python tuple ``(is_valid, issue_list)``:
231 A bool value indicating ``data_dict`` is valid or not.
234 A list of messages (:py:obj:`SchemaIssue`) from the validation::
236 [schema warn] data_dict: deprecated 'fontlib.foo': <DEPRECATED['foo.bar']>
237 [schema invalid] data_dict: key unknown 'fontlib.foo'
238 [schema invalid] data_dict: type mismatch 'fontlib.foo': expected ..., is ...
240 If ``schema_dict`` or ``data_dict`` is not a dictionary type a
241 :py:obj:`SchemaIssue` is raised.
248 if not isinstance(schema_dict, dict):
249 raise SchemaIssue(
'invalid',
"schema_dict is not a dict type")
250 if not isinstance(data_dict, dict):
251 raise SchemaIssue(
'invalid', f
"data_dict issue{'.'.join(names)} is not a dict type")
253 is_valid, issue_list =
_validate(names, issue_list, schema_dict, data_dict, deprecated)
254 return is_valid, issue_list
259 issue_list: typing.List,
260 schema_dict: typing.Dict,
261 data_dict: typing.Dict,
262 deprecated: typing.Dict[str, str],
263) -> typing.Tuple[bool, typing.List]:
267 for key, data_value
in data_dict.items():
270 name =
'.'.join(names)
272 deprecated_msg = deprecated.get(name)
275 issue_list.append(
SchemaIssue(
'warn', f
"data_dict '{name}': deprecated - {deprecated_msg}"))
277 schema_value =
value(name, schema_dict)
279 if schema_value
is UNSET:
280 if not deprecated_msg:
281 issue_list.append(
SchemaIssue(
'invalid', f
"data_dict '{name}': key unknown in schema_dict"))
284 elif type(schema_value) != type(data_value):
288 (f
"data_dict: type mismatch '{name}':" f
" expected {type(schema_value)}, is: {type(data_value)}"),
293 elif isinstance(data_value, dict):
294 _valid, _ =
_validate(names, issue_list, schema_dict, data_value, deprecated)
295 is_valid = is_valid
and _valid
298 return is_valid, issue_list
302 """Deep-update of dictionary in ``base_dict`` by dictionary in ``upd_dict``.
304 For each ``upd_key`` & ``upd_val`` pair in ``upd_dict``:
306 0. If types of ``base_dict[upd_key]`` and ``upd_val`` do not match raise a
309 1. If ``base_dict[upd_key]`` is a dict: recursively deep-update it by ``upd_val``.
311 2. If ``base_dict[upd_key]`` not exist: set ``base_dict[upd_key]`` from a
312 (deep-) copy of ``upd_val``.
314 3. If ``upd_val`` is a list, extend list in ``base_dict[upd_key]`` by the
317 4. If ``upd_val`` is a set, update set in ``base_dict[upd_key]`` by set in
321 if not isinstance(base_dict, dict):
322 raise TypeError(
"argument 'base_dict' is not a ditionary type")
323 if not isinstance(upd_dict, dict):
324 raise TypeError(
"argument 'upd_dict' is not a ditionary type")
329 for upd_key, upd_val
in upd_dict.items():
332 if isinstance(upd_val, dict):
334 if upd_key
in base_dict:
336 if not isinstance(base_dict[upd_key], dict):
337 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a dict type in base_dict")
349 base_dict[upd_key] = copy.deepcopy(upd_val)
351 elif isinstance(upd_val, list):
353 if upd_key
in base_dict:
356 if not isinstance(base_dict[upd_key], list):
357 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a list type in base_dict")
358 base_dict[upd_key].extend(upd_val)
363 base_dict[upd_key] = copy.deepcopy(upd_val)
365 elif isinstance(upd_val, set):
367 if upd_key
in base_dict:
369 if not isinstance(base_dict[upd_key], set):
370 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a set type in base_dict")
371 base_dict[upd_key].update(upd_val.copy())
376 base_dict[upd_key] = upd_val.copy()
381 base_dict[upd_key] = copy.copy(upd_val)
Any get(self, str name, Any default=UNSET, bool replace=True)
pyobj(self, name, default=UNSET)
Config from_toml(cls, pathlib.Path schema_file, pathlib.Path cfg_file, dict deprecated)
Any __getitem__(self, str key)
path(self, str name, default=UNSET)
__init__(self, typing.Dict cfg_schema, typing.Dict[str, str] deprecated)
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)
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)
typing.Tuple[bool, list] validate(typing.Dict schema_dict, typing.Dict data_dict, typing.Dict[str, str] deprecated)