public release 4.2.0 - see README.md and CHANGES.md for details
This commit is contained in:
125
pmsco/config.py
125
pmsco/config.py
@@ -4,7 +4,7 @@ infrastructure for configurable objects
|
||||
|
||||
@author Matthias Muntwiler
|
||||
|
||||
@copyright (c) 2021 by Paul Scherrer Institut @n
|
||||
@copyright (c) 2021-23 by Paul Scherrer Institut @n
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); @n
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
@@ -12,78 +12,114 @@ Licensed under the Apache License, Version 2.0 (the "License"); @n
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
from typing import Any, Callable, Dict, Generator, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PathLike = Union[str, os.PathLike]
|
||||
DataDict = Mapping[str, Union[str, int, float, Iterable, Mapping]]
|
||||
|
||||
def resolve_path(path, dirs):
|
||||
|
||||
def resolve_path(path: PathLike, dirs: Mapping[str, Any]):
|
||||
"""
|
||||
resolve a file path by replacing placeholders
|
||||
Resolve a file path by replacing placeholders.
|
||||
|
||||
placeholders are enclosed in curly braces.
|
||||
values for all possible placeholders are provided in a dictionary.
|
||||
Placeholders are enclosed in curly braces.
|
||||
Values for all possible placeholders are provided in a dictionary.
|
||||
|
||||
@param path: str, Path or other path-like.
|
||||
example: '{work}/test/testfile.dat'.
|
||||
@param dirs: dictionary mapping placeholders to project paths.
|
||||
the paths can be str, Path or other path-like
|
||||
example: {'work': '/home/user/work'}
|
||||
Example: '${work}/test/testfile.dat'.
|
||||
@param dirs: Dictionary mapping placeholders to project paths.
|
||||
The paths can be str, Path or other path-like
|
||||
Example: {'work': '/home/user/work'}
|
||||
@return: pathlib.Path object
|
||||
"""
|
||||
return Path(*(p.format(**dirs) for p in Path(path).parts))
|
||||
|
||||
return Path(*(Template(p).substitute(dirs) for p in Path(path).parts))
|
||||
|
||||
|
||||
class ConfigurableObject(object):
|
||||
"""
|
||||
Parent class for objects that can be configured by a run file
|
||||
Parent class for objects that can be configured from a runfile
|
||||
|
||||
the run file is a JSON file that contains object data in a nested dictionary structure.
|
||||
The runfile is a JSON file that contains object data in a nested dictionary structure.
|
||||
|
||||
in the dictionary structure the keys are property or attribute names of the object to be initialized.
|
||||
keys starting with a non-alphabetic character (except for some special keys like __class__) are ignored.
|
||||
these can be used as comments, or they protect private attributes.
|
||||
In the dictionary structure the keys are property or attribute names of the object to be initialized.
|
||||
Keys starting with a non-alphabetic character (except for some special keys like __class__) are ignored.
|
||||
These can be used as comments, or they protect private attributes.
|
||||
|
||||
the values can be numeric values, strings, lists or dictionaries.
|
||||
The values can be numeric values, strings, lists or dictionaries.
|
||||
|
||||
simple values are simply assigned using setattr.
|
||||
this may call a property setter if defined.
|
||||
Simple values are simply assigned using setattr.
|
||||
This may call a property setter if defined.
|
||||
|
||||
lists are iterated. each item is appended to the attribute.
|
||||
the attribute must implement an append method in this case.
|
||||
Lists are iterated. Each item is appended to the attribute.
|
||||
The attribute must implement an append method in this case.
|
||||
|
||||
if an item is a dictionary and contains the special key '__class__',
|
||||
If an item is a dictionary and contains the special key '__class__',
|
||||
an object of that class is instantiated and recursively initialized with the dictionary elements.
|
||||
this requires that the class can be found in the module scope passed to the parser methods,
|
||||
This requires that the class can be found in the module scope passed to the parser methods,
|
||||
and that the class inherits from this class.
|
||||
|
||||
cases that can't be covered easily using this mechanism
|
||||
Cases that can't be covered easily using this mechanism
|
||||
should be implemented in a property setter.
|
||||
value-checking should also be done in a property setter (or the append method in sequence-like objects).
|
||||
Value-checking should also be done in a property setter (or the append method in sequence-like objects).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
||||
project_symbols: Dictionary of symbols that should be used to resolve class and function names.
|
||||
This is usually the globals() dictionary of the project module.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
super().__init__()
|
||||
self.project_symbols: Optional[Mapping[str, Any]] = None
|
||||
|
||||
def set_properties(self, symbols: Optional[Mapping[str, Any]],
|
||||
data_dict: DataDict,
|
||||
project: 'ConfigurableObject') -> None:
|
||||
|
||||
def set_properties(self, module, data_dict, project):
|
||||
"""
|
||||
set properties of this class.
|
||||
Set properties from dictionary.
|
||||
|
||||
@param module: module reference that should be used to resolve class names.
|
||||
this is usually the project module.
|
||||
@param data_dict: dictionary of properties to set.
|
||||
see the class description for details.
|
||||
@param project: reference to the project object.
|
||||
@param symbols: Dictionary of symbols that should be used to resolve class names.
|
||||
This is usually the globals() dictionary of the project module.
|
||||
Classes are resolved using the eval function.
|
||||
@param data_dict: Dictionary of properties to set.
|
||||
See the class description for details.
|
||||
@param project: Reference to the project object.
|
||||
@return: None
|
||||
"""
|
||||
|
||||
self.project_symbols = symbols
|
||||
for key in data_dict:
|
||||
if key[0].isalpha():
|
||||
self.set_property(module, key, data_dict[key], project)
|
||||
self.set_property(symbols, key, data_dict[key], project)
|
||||
|
||||
def set_property(self, module, key, value, project):
|
||||
obj = self.parse_object(module, value, project)
|
||||
def set_property(self, symbols: Optional[Mapping[str, Any]],
|
||||
key: str,
|
||||
value: DataDict,
|
||||
project: 'ConfigurableObject') -> None:
|
||||
|
||||
"""
|
||||
Set one property.
|
||||
|
||||
@param symbols: Dictionary of symbols that should be used to resolve class names.
|
||||
This is usually the globals() dictionary of the project module.
|
||||
Classes are resolved using the eval function.
|
||||
@param key: Attribute name to set.
|
||||
@param value: New value of the attribute.
|
||||
@param project: Reference to the project object.
|
||||
@return: None
|
||||
"""
|
||||
|
||||
obj = self.parse_object(symbols, value, project)
|
||||
if hasattr(self, key):
|
||||
if obj is not None:
|
||||
if isinstance(obj, collections.abc.MutableSequence):
|
||||
@@ -103,18 +139,25 @@ class ConfigurableObject(object):
|
||||
else:
|
||||
logger.warning(f"class {self.__class__.__name__} does not have attribute {key}.")
|
||||
|
||||
def parse_object(self, module, value, project):
|
||||
def parse_object(self, symbols: Optional[Mapping[str, Any]],
|
||||
value: DataDict,
|
||||
project: 'ConfigurableObject') -> object:
|
||||
|
||||
if isinstance(value, collections.abc.MutableMapping) and "__class__" in value:
|
||||
cn = value["__class__"].split('.')
|
||||
c = functools.reduce(getattr, cn, module)
|
||||
cn = value["__class__"]
|
||||
try:
|
||||
c = eval(cn, symbols)
|
||||
except (AttributeError, KeyError, NameError, ValueError):
|
||||
logger.critical(f"can't resolve class name {cn}")
|
||||
raise
|
||||
s = inspect.signature(c)
|
||||
if 'project' in s.parameters:
|
||||
o = c(project=project)
|
||||
else:
|
||||
o = c()
|
||||
o.set_properties(module, value, project)
|
||||
o.set_properties(symbols, value, project)
|
||||
elif isinstance(value, collections.abc.MutableSequence):
|
||||
o = [self.parse_object(module, i, project) for i in value]
|
||||
o = [self.parse_object(symbols, i, project) for i in value]
|
||||
else:
|
||||
o = value
|
||||
return o
|
||||
|
||||
Reference in New Issue
Block a user