Files
frappy/frappy/config.py
Markus Zolliker fd43687465 equipment_id for merged configs and routed nodes
Add a new custom module property 'original_id' indicating
the equipment_id the modules originally belongs to.
This property is only given, when distinct from the equipment_id
of the SEC node.
It happens when multiple config files are given, for all modules
but the ones given in the first file, and for routed modules,
when  multiple nodes are routed or own modules are given.

+ fix an issue in router: additional modules were ignore in case
of a single node.

+ small cosmetic changes in config.py reducing IDE complains

Change-Id: If846c47a06158629cef807d22b91f69e4f416563
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35396
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2025-01-17 15:00:20 +01:00

203 lines
6.1 KiB
Python

# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Alexander Zaft <a.zaft@fz-juelich.de>
#
# *****************************************************************************
import os
from pathlib import Path
import re
from collections import Counter
from frappy.errors import ConfigError
from frappy.lib import generalConfig
class Undef:
pass
class Node(dict):
def __init__(
self,
equipment_id,
description,
interface=None,
cls='frappy.protocol.dispatcher.Dispatcher',
**kwds
):
super().__init__(
equipment_id=equipment_id,
description=description,
interface=interface,
cls=cls,
**kwds
)
class Param(dict):
def __init__(self, value=Undef, **kwds):
if value is not Undef:
kwds['value'] = value
super().__init__(**kwds)
class Group(tuple):
def __new__(cls, *args):
return super().__new__(cls, args)
class Mod(dict):
def __init__(self, name, cls, description, **kwds):
super().__init__(
name=name,
cls=cls,
description=description
)
# matches name from spec
if not re.match(r'^[a-zA-Z]\w{0,62}$', name, re.ASCII):
raise ConfigError(f'Not a valid SECoP Module name: "{name}".'
' Does it only contain letters, numbers and underscores?')
# Make parameters out of all keywords
groups = {}
for key, val in kwds.items():
if isinstance(val, Param):
self[key] = val
elif isinstance(val, Group):
groups[key] = val
else:
# shortcut to only set value
self[key] = Param(val)
for group, members in groups.items():
for member in members:
self[member]['group'] = group
class Collector:
def __init__(self, cls):
self.list = []
self.cls = cls
def add(self, *args, **kwds):
self.list.append(self.cls(*args, **kwds))
def append(self, mod):
self.list.append(mod)
class NodeCollector:
def __init__(self):
self.node = None
def add(self, *args, **kwds):
if self.node is None:
self.node = Node(*args, **kwds)
else:
raise ConfigError('Only one Node is allowed per file!')
class Config(dict):
def __init__(self, node, modules):
super().__init__(
node=node.node,
**{mod['name']: mod for mod in modules.list}
)
self.module_names = {mod.pop('name') for mod in modules.list}
self.ambiguous = set()
def merge_modules(self, other):
""" merges only the modules from 'other' into 'self'"""
self.ambiguous |= self.module_names & other.module_names
equipment_id = other['node']['equipment_id']
for name, mod in other.items():
if name == 'node':
continue
if name not in self.module_names:
self.module_names.add(name)
self[name] = mod
mod['original_id'] = equipment_id
def process_file(filename, log):
config_text = filename.read_bytes()
node = NodeCollector()
mods = Collector(Mod)
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group}
# pylint: disable=exec-used
exec(compile(config_text, filename, 'exec'), ns)
# check for duplicates in the file itself. Between files comes later
duplicates = [name for name, count in Counter([mod['name']
for mod in mods.list]).items() if count > 1]
if duplicates:
log.warning('Duplicate module name in file \'%s\': %s',
filename, ','.join(duplicates))
return Config(node, mods)
def to_config_path(cfgfile, log):
candidates = [cfgfile + e for e in ['_cfg.py', '.py', '']]
if os.sep in cfgfile: # specified as full path
file = Path(cfgfile) if Path(cfgfile).exists() else None
else:
for file in [Path(d) / candidate
for d in generalConfig.confdir
for candidate in candidates]:
if file.exists():
break
else:
file = None
if file is None:
raise ConfigError(f"Couldn't find cfg file {cfgfile!r} in {generalConfig.confdir}")
if not file.name.endswith('_cfg.py'):
log.warning("Config files should end in '_cfg.py': %s", file.name)
log.debug('Using config file %s for %s', file, cfgfile)
return file
def load_config(cfgfiles, log):
"""Load config files.
Only the node-section of the first config file will be returned.
The others will be discarded.
Arguments
- cfgfiles : list
List of config file paths
- log : frappy.logging.Mainlogger
Logger aquired from frappy.logging
Returns
- config: Config
merged configuration
"""
config = None
for cfgfile in cfgfiles:
filename = to_config_path(str(cfgfile), log)
log.debug('Parsing config file %s...', filename)
cfg = process_file(filename, log)
if config:
config.merge_modules(cfg)
else:
config = cfg
if config.ambiguous:
log.warning('ambiguous sections in %s: %r',
cfgfiles, list(config.ambiguous))
return config