121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
"""
|
|
@package pmsco.config
|
|
infrastructure for configurable objects
|
|
|
|
@author Matthias Muntwiler
|
|
|
|
@copyright (c) 2021 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
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
"""
|
|
|
|
import collections.abc
|
|
import functools
|
|
import inspect
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def resolve_path(path, dirs):
|
|
"""
|
|
resolve a file path by replacing placeholders
|
|
|
|
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'}
|
|
@return: pathlib.Path object
|
|
"""
|
|
return Path(*(p.format(**dirs) for p in Path(path).parts))
|
|
|
|
|
|
class ConfigurableObject(object):
|
|
"""
|
|
Parent class for objects that can be configured by a run file
|
|
|
|
the run file 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.
|
|
|
|
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.
|
|
|
|
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__',
|
|
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,
|
|
and that the class inherits from this class.
|
|
|
|
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).
|
|
"""
|
|
def __init__(self):
|
|
pass
|
|
|
|
def set_properties(self, module, data_dict, project):
|
|
"""
|
|
set properties of this class.
|
|
|
|
@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.
|
|
@return: None
|
|
"""
|
|
for key in data_dict:
|
|
if key[0].isalpha():
|
|
self.set_property(module, key, data_dict[key], project)
|
|
|
|
def set_property(self, module, key, value, project):
|
|
obj = self.parse_object(module, value, project)
|
|
if hasattr(self, key):
|
|
if obj is not None:
|
|
if isinstance(obj, collections.abc.MutableSequence):
|
|
attr = getattr(self, key)
|
|
for item in obj:
|
|
attr.append(item)
|
|
elif isinstance(obj, collections.abc.Mapping):
|
|
d = getattr(self, key)
|
|
if d is not None and isinstance(d, collections.abc.MutableMapping):
|
|
d.update(obj)
|
|
else:
|
|
setattr(self, key, obj)
|
|
else:
|
|
setattr(self, key, obj)
|
|
else:
|
|
setattr(self, key, obj)
|
|
else:
|
|
logger.warning(f"class {self.__class__.__name__} does not have attribute {key}.")
|
|
|
|
def parse_object(self, module, value, project):
|
|
if isinstance(value, collections.abc.MutableMapping) and "__class__" in value:
|
|
cn = value["__class__"].split('.')
|
|
c = functools.reduce(getattr, cn, module)
|
|
s = inspect.signature(c)
|
|
if 'project' in s.parameters:
|
|
o = c(project=project)
|
|
else:
|
|
o = c()
|
|
o.set_properties(module, value, project)
|
|
elif isinstance(value, collections.abc.MutableSequence):
|
|
o = [self.parse_object(module, i, project) for i in value]
|
|
else:
|
|
o = value
|
|
return o
|