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
29__all__ = [
'Config',
'UNSET',
'SchemaIssue']
31log = logging.getLogger(__name__)
35 """Class of ``False`` singelton"""
54 """Exception to store and/or raise a message from a schema issue."""
56 def __init__(self, level: typing.Literal[
'warn',
'invalid'], msg: str):
61 return f
"[cfg schema {self.level}] {self.args[0]}"
65 """Base class used for configuration"""
70 def from_toml(cls, schema_file: pathlib.Path, cfg_file: pathlib.Path, deprecated: dict) -> Config:
74 log.debug(
"load schema file: %s", schema_file)
75 cfg = cls(cfg_schema=
toml_load(schema_file), deprecated=deprecated)
76 if not cfg_file.exists():
77 log.warning(
"missing config file: %s", cfg_file)
82 log.debug(
"load config file: %s", cfg_file)
85 is_valid, issue_list = cfg.validate(upd_cfg)
86 for msg
in issue_list:
89 raise TypeError(f
"schema of {cfg_file} is invalid!")
93 def __init__(self, cfg_schema: typing.Dict, deprecated: typing.Dict[str, str]):
94 """Construtor of class Config.
96 :param cfg_schema: Schema of the configuration
97 :param deprecated: dictionary that maps deprecated configuration names to a messages
99 These values are needed for validation, see :py:obj:`validate`.
104 self.
cfg = copy.deepcopy(cfg_schema)
110 """Validation of dictionary ``cfg`` on :py:obj:`Config.SCHEMA`.
111 Validation is done by :py:obj:`validate`."""
116 """Update this configuration by ``upd_cfg``."""
121 """Returns default value of field ``name`` in ``self.cfg_schema``."""
124 def get(self, name: str, default: Any = UNSET, replace: bool =
True) -> Any:
125 """Returns the value to which ``name`` points in the configuration.
127 If there is no such ``name`` in the config and the ``default`` is
128 :py:obj:`UNSET`, a :py:obj:`KeyError` is raised.
132 val = parent.get(name.split(
'.')[-1], UNSET)
138 if replace
and isinstance(val, str):
142 def set(self, name: str, val):
143 """Set the value to which ``name`` points in the configuration.
145 If there is no such ``name`` in the config, a :py:obj:`KeyError` is
149 parent[name.split(
'.')[-1]] = val
152 parent_name =
'.'.join(name.split(
'.')[:-1])
154 parent =
value(parent_name, self.
cfg)
157 if (parent
is UNSET)
or (
not isinstance(parent, dict)):
158 raise KeyError(parent_name)
161 def path(self, name: str, default=UNSET):
162 """Get a :py:class:`pathlib.Path` object from a config string."""
164 val = self.
get(name, default)
169 return pathlib.Path(str(val))
171 def pyobj(self, name, default=UNSET):
172 """Get python object refered by full qualiffied name (FQN) in the config
175 fqn = self.
get(name, default)
180 (modulename, name) = str(fqn).rsplit(
'.', 1)
181 m = __import__(modulename, {}, {}, [name], 0)
182 return getattr(m, name)
189 with open(file_name,
"rb")
as f:
190 return tomllib.load(f)
191 except tomllib.TOMLDecodeError
as exc:
192 msg = str(exc).replace(
'\t',
'').replace(
'\n',
' ')
193 log.error(
"%s: %s", file_name, msg)
197 return pytomlpp.load(file_name)
198 except pytomlpp.DecodeError
as exc:
199 msg = str(exc).replace(
'\t',
'').replace(
'\n',
' ')
200 log.error(
"%s: %s", file_name, msg)
207def value(name: str, data_dict: dict):
208 """Returns the value to which ``name`` points in the ``dat_dict``.
217 >>> value('foobar', data_dict)
219 >>> value('foo.bar', data_dict)
221 >>> value('foo.bar.xxx', data_dict)
227 for part
in name.split(
'.'):
228 if isinstance(ret_val, dict):
229 ret_val = ret_val.get(part, UNSET)
236 schema_dict: typing.Dict, data_dict: typing.Dict, deprecated: typing.Dict[str, str]
237) -> typing.Tuple[bool, list]:
238 """Deep validation of dictionary in ``data_dict`` against dictionary in
239 ``schema_dict``. Argument deprecated is a dictionary that maps deprecated
240 configuration names to a messages::
243 "foo.bar" : "config 'foo.bar' is deprecated, use 'bar.foo'",
247 The function returns a python tuple ``(is_valid, issue_list)``:
250 A bool value indicating ``data_dict`` is valid or not.
253 A list of messages (:py:obj:`SchemaIssue`) from the validation::
255 [schema warn] data_dict: deprecated 'fontlib.foo': <DEPRECATED['foo.bar']>
256 [schema invalid] data_dict: key unknown 'fontlib.foo'
257 [schema invalid] data_dict: type mismatch 'fontlib.foo': expected ..., is ...
259 If ``schema_dict`` or ``data_dict`` is not a dictionary type a
260 :py:obj:`SchemaIssue` is raised.
267 if not isinstance(schema_dict, dict):
268 raise SchemaIssue(
'invalid',
"schema_dict is not a dict type")
269 if not isinstance(data_dict, dict):
270 raise SchemaIssue(
'invalid', f
"data_dict issue{'.'.join(names)} is not a dict type")
272 is_valid, issue_list =
_validate(names, issue_list, schema_dict, data_dict, deprecated)
273 return is_valid, issue_list
278 issue_list: typing.List,
279 schema_dict: typing.Dict,
280 data_dict: typing.Dict,
281 deprecated: typing.Dict[str, str],
282) -> typing.Tuple[bool, typing.List]:
286 for key, data_value
in data_dict.items():
289 name =
'.'.join(names)
291 deprecated_msg = deprecated.get(name)
294 issue_list.append(
SchemaIssue(
'warn', f
"data_dict '{name}': deprecated - {deprecated_msg}"))
296 schema_value =
value(name, schema_dict)
298 if schema_value
is UNSET:
299 if not deprecated_msg:
300 issue_list.append(
SchemaIssue(
'invalid', f
"data_dict '{name}': key unknown in schema_dict"))
303 elif type(schema_value) != type(data_value):
307 (f
"data_dict: type mismatch '{name}':" f
" expected {type(schema_value)}, is: {type(data_value)}"),
312 elif isinstance(data_value, dict):
313 _valid, _ =
_validate(names, issue_list, schema_dict, data_value, deprecated)
314 is_valid = is_valid
and _valid
317 return is_valid, issue_list
321 """Deep-update of dictionary in ``base_dict`` by dictionary in ``upd_dict``.
323 For each ``upd_key`` & ``upd_val`` pair in ``upd_dict``:
325 0. If types of ``base_dict[upd_key]`` and ``upd_val`` do not match raise a
328 1. If ``base_dict[upd_key]`` is a dict: recursively deep-update it by ``upd_val``.
330 2. If ``base_dict[upd_key]`` not exist: set ``base_dict[upd_key]`` from a
331 (deep-) copy of ``upd_val``.
333 3. If ``upd_val`` is a list, extend list in ``base_dict[upd_key]`` by the
336 4. If ``upd_val`` is a set, update set in ``base_dict[upd_key]`` by set in
340 if not isinstance(base_dict, dict):
341 raise TypeError(
"argument 'base_dict' is not a ditionary type")
342 if not isinstance(upd_dict, dict):
343 raise TypeError(
"argument 'upd_dict' is not a ditionary type")
348 for upd_key, upd_val
in upd_dict.items():
351 if isinstance(upd_val, dict):
353 if upd_key
in base_dict:
355 if not isinstance(base_dict[upd_key], dict):
356 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a dict type in base_dict")
368 base_dict[upd_key] = copy.deepcopy(upd_val)
370 elif isinstance(upd_val, list):
372 if upd_key
in base_dict:
375 if not isinstance(base_dict[upd_key], list):
376 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a list type in base_dict")
377 base_dict[upd_key].extend(upd_val)
382 base_dict[upd_key] = copy.deepcopy(upd_val)
384 elif isinstance(upd_val, set):
386 if upd_key
in base_dict:
388 if not isinstance(base_dict[upd_key], set):
389 raise TypeError(f
"type mismatch {'.'.join(names)}: is not a set type in base_dict")
390 base_dict[upd_key].update(upd_val.copy())
395 base_dict[upd_key] = upd_val.copy()
400 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)