Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Bliss
bliss
Commits
a14b481b
Commit
a14b481b
authored
Jun 28, 2021
by
Perceval Guillou
Browse files
split BlissController and ItemContainer
parent
068c26e6
Changes
8
Hide whitespace changes
Inline
Side-by-side
bliss/common/soft_axis.py
View file @
a14b481b
...
...
@@ -32,7 +32,7 @@ def SoftAxis(
config
[
"unit"
]
=
unit
controller
=
SoftController
(
name
,
obj
,
config
,
position
,
move
,
stop
,
state
)
controller
.
_
controller_init
()
controller
.
_
initialize_config
()
axis
=
controller
.
get_axis
(
name
)
axis
.
_positioner
=
False
...
...
bliss/config/plugins/bliss_controller.py
View file @
a14b481b
...
...
@@ -5,19 +5,438 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from
bliss.config.plugins.utils
import
find_top_class_and_node
from
bliss.controllers.bliss_controller
import
BlissController
from
importlib
import
import_module
from
bliss.config.plugins.utils
import
find_top_class_and_node
,
find_class_and_node
from
bliss.config.static
import
ConfigReference
,
ConfigNode
,
ConfigList
from
bliss.common.utils
import
autocomplete_property
def
find_sub_names_config
(
config
,
selection
=
None
,
level
=
0
,
parent_key
=
None
):
""" Recursively search in a config the sub-sections where the key 'name' is found.
Returns a dict of tuples (sub_config, parent_key) indexed by level (0 is the top level).
- sub_config: the sub-config containing the 'name' key
- parent_key: key under which the sub_config was found (None for level 0)
args:
config: the config that should be explored
selection: a list containing the info of the subnames already found (for recursion)
level: an integer describing at which level the subname was found (level=0 is the top/upper level) (for recursion)
parent_key: key under which the sub_config was found (None for level 0) (for recursion)
"""
assert
isinstance
(
config
,
(
ConfigNode
,
dict
))
if
selection
is
None
:
selection
=
{}
if
selection
.
get
(
level
)
is
None
:
selection
[
level
]
=
[]
if
isinstance
(
config
,
ConfigNode
):
name
=
config
.
raw_get
(
"name"
)
else
:
name
=
config
.
get
(
"name"
)
if
name
is
not
None
:
selection
[
level
].
append
((
config
,
parent_key
))
if
isinstance
(
config
,
ConfigNode
):
cfg_items
=
(
config
.
raw_items
()
)
# !!! raw_items to avoid cyclic import while resloving reference !!!
else
:
cfg_items
=
config
.
items
()
for
k
,
v
in
cfg_items
:
if
isinstance
(
v
,
(
ConfigNode
,
dict
)):
find_sub_names_config
(
v
,
selection
,
level
+
1
,
k
)
elif
isinstance
(
v
,
(
ConfigList
,
list
)):
for
i
in
v
:
if
isinstance
(
i
,
(
ConfigNode
,
dict
)):
find_sub_names_config
(
i
,
selection
,
level
+
1
,
k
)
return
selection
class
ConfigItemContainer
:
"""
Base class designed to ease the management of sub-objects that declared under a top object configuration.
Sub-objects are declared in the top object yml configuration under dedicated sub-sections.
A sub-object is considered as a subitem if it has a name (key 'name' in a sub-section of the config).
Usually subitems are counters and axes but could be anything else (known by the top object).
# --- Plugin ---
ConfigItemContainer objects are created from the yml config using the bliss plugin.
Any subitem with a name can be imported in a Bliss session with config.get('name').
The plugin ensures that the top object and its subitems are only created once.
The ConfigItemContainer itself can have a name (optional) and can be imported in the session.
The plugin resolves dependencies between the ConfigItemContainer and its subitems.
It looks for the top 'class' key in the config to instantiate the ConfigItemContainer.
While importing any subitem in the session, the ConfigItemContainer is instantiated first (if not alive already).
The effective creation of the subitems is performed by the ConfigItemContainer itself and the plugin just ensures
that the container is always created before subitems and only once.
Example: config.get(top_name) or config.get(item_name) with config = bliss.config.static.get_config()
# --- Items and sub-controllers ---
An ConfigItemContainer can have sub-controllers. In that case there are two ways to create the sub-containers:
- The most simple way to do this is to declare the sub-containers as an independant object with its own yml config
and use a reference to this object into the top-object config.
- If a sub-container has no reason to exist independently from the top-container, then the top-container
will create and manage its sub-containers from the knowledge of the top-container config only.
In that case, some items declared in the top-container are, in fact, managed by one of the sub-containers.
In that case, the author of the top container class must overload the '_get_item_owner' method and specify
which is the sub-container that manages which items.
Example: Consider a top container which manages a motors controller internally. The top container config
declares the axes subitems but those items are in fact managed by the motors controller.
In that case, '_get_item_owner' should specify that the axes subitems are managed by 'self.motor_controller'
instead of 'self'. The method receives the item name and the parent_key. So 'self.motor_controller' can be
associated to all subitems under the 'axes' parent_key (instead of doing it for each subitem name).
# --- From config dict ---
A ConfigItemContainer can be instantiated directly (i.e. not via plugin) providing a config as a dictionary.
In that case, users must call the method 'self._initialize_config()' just after the controller instantiation
to ensure that the controller is initialized in the same way as the plugin does.
The config dictionary should be structured like a YML file (i.e: nested dict and list) and
references replaced by their corresponding object instances.
Example: bctrl = ConfigItemContainer( config_dict ) => bctrl._initialize_config()
# --- yml config example ---
- plugin: bliss <== use the dedicated bliss plugin
module: custom_module <== module of the custom bliss controller
class: BCMockup <== class of the custom bliss controller
name: bcmock <== name of the custom bliss controller (optional)
com: <== communication config for associated hardware (optional)
tcp:
url: bcmock
custom_param_1: value <== a parameter for the custom bliss controller creation (optional)
custom_param_2: $ref1 <== a referenced object for the controller (optional/authorized)
sub-section-1: <== a sub-section where subitems can be declared (optional) (ex: 'counters')
- name: sub_item_1 <== name of the subitem (and its config)
tag : item_tag_1 <== a tag for this item (known and interpreted by the custom bliss controller) (optional)
sub_param_1: value <== a custom parameter for the item creation (optional)
device: $ref2 <== an external reference for this subitem (optional/authorized)
sub-section-2: <== another sub-section where subitems can be declared (optional) (ex: 'axes')
- name: sub_item_2 <== name of the subitem (and its config)
tag : item_tag_2 <== a tag for this item (known and interpreted by the custom bliss controller) (optional)
input: $sub_item_1 <== an internal reference to another subitem owned by the same controller (optional/authorized)
sub-section-2-1: <== nested sub-sections are possible (optional)
- name: sub_item_21
tag : item_tag_21
sub-section-3 : <== a third sub-section
- name: $ref3 <== a subitem as an external reference is possible (optional/authorized)
something: value
"""
def
__init__
(
self
,
config
):
self
.
__subitems_configs_ready
=
False
self
.
__initialization_done
=
False
self
.
_subitems_config
=
{}
# stores items info (cfg, pkey) (filled by self._prepare_subitems_configs)
self
.
_subitems
=
{}
# stores items instances (filled by self.__build_subitem_from_config)
# generate generic name for the container if not found in config
self
.
_name
=
config
.
get
(
"name"
)
if
self
.
_name
is
None
:
if
isinstance
(
config
,
ConfigNode
):
self
.
_name
=
f
"
{
self
.
__class__
.
__name__
}
_
{
config
.
md5hash
()
}
"
else
:
self
.
_name
=
f
"
{
self
.
__class__
.
__name__
}
_
{
id
(
self
)
}
"
# config can be a ConfigNode or a dict
self
.
_config
=
config
# ========== STANDARD METHODS ============================
@
property
def
name
(
self
):
return
self
.
_name
@
property
def
config
(
self
):
return
self
.
_config
# ========== INTERNAL METHODS (PRIVATE) ============================
@
property
def
_is_initialized
(
self
):
return
self
.
__initialization_done
def
__build_subitem_from_config
(
self
,
name
):
"""
Standard method to create an item from its config.
This method is called:
- by the plugin, via config.get(item_name) => create_object_from_cache => name is exported in session
- directly, via self._get_subitem(item_name) => name is NOT exported in session
"""
# print(f"=== Build item {name} from {self.name}")
if
not
self
.
__initialization_done
:
raise
RuntimeError
(
f
"Item container not initialized:
{
self
}
\n
Call 'self._initialize_config()'"
)
if
name
not
in
self
.
_subitems_config
:
raise
ValueError
(
f
"Cannot find item with name:
{
name
}
"
)
cfg
,
pkey
=
self
.
_subitems_config
[
name
]
cfg_name
=
cfg
.
get
(
"name"
)
if
isinstance
(
cfg_name
,
str
):
item_class
=
self
.
__find_item_class
(
cfg
,
pkey
)
item_obj
=
None
else
:
# its a referenced object (cfg_name contains the object instance)
item_class
=
None
item_obj
=
cfg_name
cfg_name
=
item_obj
.
name
item
=
self
.
_create_subitem_from_config
(
cfg_name
,
cfg
,
pkey
,
item_class
,
item_obj
)
if
item
is
None
:
msg
=
f
"
\n
Unable to obtain item
{
cfg_name
}
from
{
self
.
name
}
with:
\n
"
msg
+=
f
" class:
{
item_class
}
\n
"
msg
+=
f
" parent_key: '
{
pkey
}
'
\n
"
msg
+=
f
" config:
{
cfg
}
\n
"
msg
+=
f
"Check that item config is supported by
{
self
}
"
raise
RuntimeError
(
msg
)
self
.
_subitems
[
name
]
=
item
def
__find_item_class
(
self
,
cfg
,
pkey
):
"""
Return a suitable class for an item.
It tries to find a class_name in the item's config or ask the container for a default.
The class_name could be an absolute path, else the class is searched in the container
module first. If not found, ask the container the path of the module where the class should be found.
args:
- cfg: item config node
- pkey: item parent key
"""
class_name
=
cfg
.
get
(
"class"
)
if
class_name
is
None
:
# ask default class name to the container
class_name
=
self
.
_get_subitem_default_class_name
(
cfg
,
pkey
)
if
class_name
is
None
:
msg
=
f
"
\n
Unable to obtain default_class_name from
{
self
.
name
}
with:
\n
"
msg
+=
f
" parent_key: '
{
pkey
}
'
\n
"
msg
+=
f
" config:
{
cfg
}
\n
"
msg
+=
f
"Check item config is supported by
{
self
}
\n
"
raise
RuntimeError
(
msg
)
if
"."
in
class_name
:
# from absolute path
idx
=
class_name
.
rfind
(
"."
)
module_name
,
cname
=
class_name
[:
idx
],
class_name
[
idx
+
1
:]
module
=
__import__
(
module_name
,
fromlist
=
[
""
])
return
getattr
(
module
,
cname
)
else
:
module
=
import_module
(
self
.
__module__
)
# try at the container module level first
if
hasattr
(
module
,
class_name
):
return
getattr
(
module
,
class_name
)
else
:
# ask the container the module where the class should be found
module_name
=
self
.
_get_subitem_default_module
(
class_name
,
cfg
,
pkey
)
if
module_name
is
None
:
msg
=
f
"
\n
Unable to obtain default_module from
{
self
.
name
}
with:
\n
"
msg
+=
f
" class_name:
{
class_name
}
\n
"
msg
+=
f
" parent_key: '
{
pkey
}
'
\n
"
msg
+=
f
" config:
{
cfg
}
\n
"
msg
+=
f
"Check item config is supported by
{
self
}
\n
"
raise
RuntimeError
(
msg
)
module
=
import_module
(
module_name
)
if
hasattr
(
module
,
class_name
):
return
getattr
(
module
,
class_name
)
else
:
raise
ModuleNotFoundError
(
f
"cannot find class
{
class_name
}
in
{
module
}
"
)
def
_get_item_owner
(
self
,
name
,
cfg
,
pkey
):
""" Return the container that owns the items declared in the config.
By default, this container is the owner of all items.
However if this container has sub-containers, use this method to specify
the items owners.
"""
# === Example with one sub-container (self.foo_container) ===
# === owning all items found under the 'foo' section of the config ===
#
# if pkey == 'foo':
# return self.foo_container
# else:
# return self
return
self
def
_prepare_subitems_configs
(
self
):
""" Find all sub objects with a name in the container config (i.e. items).
Store the items config info (cfg, pkey) in the container (including referenced items).
Return the list of found items (excluding referenced items).
"""
cacheditemnames2ctrl
=
{}
sub_cfgs
=
find_sub_names_config
(
self
.
_config
)
for
level
in
sorted
(
sub_cfgs
.
keys
()):
if
level
!=
0
:
# ignore the container itself
for
cfg
,
pkey
in
sub_cfgs
[
level
]:
if
isinstance
(
cfg
,
ConfigNode
):
name
=
cfg
.
raw_get
(
"name"
)
else
:
name
=
cfg
.
get
(
"name"
)
if
isinstance
(
name
,
str
):
# only store in items_list the subitems with a name as a string
# because items_list is used by the plugin to cache subitem's container.
# (i.e exclude referenced names as they are not owned by this container)
cacheditemnames2ctrl
[
name
]
=
self
.
_get_item_owner
(
name
,
cfg
,
pkey
)
elif
isinstance
(
name
,
ConfigReference
):
name
=
name
.
object_name
else
:
name
=
name
.
name
self
.
_subitems_config
[
name
]
=
(
cfg
,
pkey
)
self
.
__subitems_configs_ready
=
True
return
cacheditemnames2ctrl
def
_get_subitem
(
self
,
name
):
""" return an item (create it if not alive) """
if
name
not
in
self
.
_subitems
:
self
.
__build_subitem_from_config
(
name
)
return
self
.
_subitems
[
name
]
def
_initialize_config
(
self
):
""" Standard method to fully initialize this container object.
This method must be called if the container has been directly
instantiated with a config dictionary (i.e without going through the plugin and YML config).
"""
if
not
self
.
__initialization_done
:
if
not
self
.
__subitems_configs_ready
:
self
.
_prepare_subitems_configs
()
self
.
__initialization_done
=
True
try
:
self
.
_load_config
()
self
.
_init
()
except
BaseException
:
self
.
__initialization_done
=
False
raise
# ========== ABSTRACT METHODS ====================
def
_get_subitem_default_class_name
(
self
,
cfg
,
parent_key
):
# Called when the class key cannot be found in the item_config.
# Then a default class must be returned. The choice of the item_class is usually made from the parent_key value.
# Elements of the item_config may also by used to make the choice of the item_class.
"""
Return the appropriate default class name (as a string) for a given item.
args:
- cfg: item config node
- parent_key: the key under which item config was found
"""
raise
NotImplementedError
def
_get_subitem_default_module
(
self
,
class_name
,
cfg
,
parent_key
):
# Called when the given class_name (found in cfg) cannot be found at the container module level.
# Then a default module path must be returned. The choice of the item module is usually made from the parent_key value.
# Elements of the item_config may also by used to make the choice of the item module.
"""
Return the path (str) of the default module where the given class_name should be found.
args:
- class_name: item class name
- cfg: item config node
- parent_key: the key under which item config was found
"""
raise
NotImplementedError
def
_create_subitem_from_config
(
self
,
name
,
cfg
,
parent_key
,
item_class
,
item_obj
=
None
):
# Called when a new subitem is created (i.e accessed for the first time via self._get_subitem)
"""
Return the instance of a new item owned by this container.
args:
name: item name
cfg : item config
parent_key: the config key under which the item was found (ex: 'counters').
item_class: a class to instantiate the item (None if item is a reference)
item_obj: the item instance (None if item is NOT a reference)
return: item instance
"""
# === Example ===
#
# if item_obj is not None:
# return item_obj
#
# if pkey == 'foo':
# return item_class(name, cfg)
# else:
# return item_class(cfg)
raise
NotImplementedError
def
_load_config
(
self
):
# Called by the plugin via self._initialize_config
# Called after self._subitems_config has_been filled.
"""
Read and apply the YML configuration of this container.
"""
raise
NotImplementedError
def
_init
(
self
):
# Called by the plugin via self._initialize_config
# Called just after self._load_config
"""
Place holder for any action to perform after the configuration has been loaded.
"""
pass
def
create_objects_from_config_node
(
cfg_obj
,
cfg_node
):
"""
Create an object from the config with a given name (unique).
It ensures that
the controll
er and sub-objects are only created once.
It ensures that
a ConfigItemContain
er and
its
sub-objects are only created once.
This function resolves dependencies between
the BlissControll
er and its sub-objects with a name.
It looks for the 'class' key in 'cfg_node' (or at upper levels) to instantiate the
BlissControll
er.
All
sub-
configs
of named sub-objects owned by the
cont
roll
er are st
o
red as cached items for later instantiation
via config.get
.
This function resolves dependencies between
a ConfigItemContain
er and its sub-objects with a name
(items)
.
It looks for the 'class' key in 'cfg_node' (or at upper levels) to instantiate the
ConfigItemContain
er.
All
items
configs
found under a
cont
ain
er are
regi
st
e
red as cached items for
a
later instantiation
(see 'create_object_from_cache')
.
args:
cfg_obj: a Config object (from config.static)
...
...
@@ -30,55 +449,85 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
# search the 'class' key in cfg_node or at a upper node level
# then return the class and the associated config node
klass
,
ctrl_node
=
find_top_class_and_node
(
cfg_node
)
ctrl_name
=
ctrl_node
.
get
(
"name"
)
# c
trl
could have a name in config
ctrl_name
=
ctrl_node
.
get
(
"name"
)
# c
ontainer
could have a name in config
item_name
=
cfg_node
[
"name"
]
# name of the item that should be created and returned
# always create the bliss controller first
bctrl
=
klass
(
ctrl_node
)
# print(f"\n=== From config: {item_name} from {bctrl.name}")
if
issubclass
(
klass
,
ConfigItemContainer
):
if
isinstance
(
bctrl
,
BlissController
):
# always create the container first
bctrl
=
klass
(
ctrl_node
)
# print(f"\n=== From config: {item_name} from {bctrl.name}")
# prepare subitems configs and cache item's cont
roll
er.
# the cont
roll
er decides which item
s
should be cached and which cont
roll
er
# is associated to the cached item (in case the cached item is owned by a sub-cont
roll
er of this cont
roll
er)
# prepare subitems configs and cache item's cont
ain
er.
# the cont
ain
er decides which item should be cached and which cont
ain
er
# is associated to the cached item (in case the cached item is owned by a sub-cont
ain
er of this cont
ain
er)
cacheditemnames2ctrl
=
bctrl
.
_prepare_subitems_configs
()
# print(f"\n=== Caching: {list(cacheditemnames2ctrl.keys())} from {bctrl.name}")
# --- add the cont
roll
er to registered items (if it has a name)
# --- add the cont
ain
er to registered items (if it has a name)
name2items
=
{}
if
ctrl_name
:
name2items
[
ctrl_name
]
=
bctrl
# update the config cache dict now to avoid cyclic instantiation with internal references
# an internal reference occurs when a subitem config uses a reference to another subitem owned by the same cont
roll
er.
# an internal reference occurs when a subitem config uses a reference to another subitem owned by the same cont
ain
er.
yield
name2items
,
cacheditemnames2ctrl
# load config and init
controller
# load config and init
ialize
try
:
bctrl
.
_
controller_init
()
bctrl
.
_
initialize_config
()
except
BaseException
:
# remove cached obj if cont
roll
er initialization fails (to avoid items with different instances of the same cont
roll
er)
# remove cached obj if cont
ain
er initialization fails (to avoid items with different instances of the same cont
ain
er)
for
iname
in
cacheditemnames2ctrl
.
keys
():
cfg_obj
.
_name2cache
.
pop
(
iname
,
None
)
raise
# --- don't forget to instantiate the object for which this function has been called (if not a cont
roll
er)
# --- don't forget to instantiate the object for which this function has been called (if not a cont
ain
er)
if
item_name
!=
ctrl_name
:
obj
=
cfg_obj
.
get
(
item_name
)
yield
{
item_name
:
obj
}
# --- Now any new object_name going through 'config.get( obj_name )' should call 'create_object_from_cache' only.
# --- 'create_objects_from_config_node' should never be called again for any object related to the cont
roll
er instantiated here (see config.get code)
# --- 'create_objects_from_config_node' should never be called again for any object related to the cont
ain
er instantiated here (see config.get code)
elif
(
item_name
==
ctrl_name
):
# allow instantiation of top object which is not a BlissController
yield
{
ctrl_name
:
bctrl
}
item_name
!=
ctrl_name
):
# prevent instantiation of an item comming from a top object that is not a ConfigItemContainer
raise
TypeError
(
f
"Object with subitems in config must be a ConfigItemContainer object"
)
else
:
# act as the older Bliss plugin
klass
,
node
=
find_class_and_node
(
cfg_node
)
if
node
.
get
(
"name"
)
!=
item_name
:
cfg_node
=
ConfigNode
.
indexed_nodes
[
item_name
]
else
:
cfg_node
=
node
o
=
klass
(
item_name
,
cfg_node
.
clone
())
for
key
,
value
in
cfg_node
.
items
():
if
isinstance
(
cfg_node
.
raw_get
(
key
),
ConfigReference
):
if
hasattr
(
o
,
key
):
continue
else
:
setattr
(
o
,
key
,
value
)
yield
{
item_name
:
o
}
return
else
:
# prevent instantiation of an item comming from a top controller which is not a BlissController
raise
TypeError
(
f
"
{
bctrl
}
is not a BlissController object!"
)
# else:
# bctrl = klass(ctrl_node)
# # print(f"\n=== From config: {item_name} from {bctrl.name}")
# if (
# item_name == ctrl_name
# ): # allow instantiation of top object which is not a ConfigItemContainer
# yield {ctrl_name: bctrl}
# return
# else: # prevent instantiation of an item comming from a top object that is not a ConfigItemContainer
# raise TypeError(f"{bctrl} is not a ConfigItemContainer object!")
def
create_object_from_cache
(
config
,
name
,
bctrl
):
...
...
bliss/controllers/bliss_controller.py
View file @
a14b481b
...
...
@@ -5,74 +5,15 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from
importlib
import
import_module
from
bliss.common.protocols
import
CounterContainer
from
bliss.common.utils
import
autocomplete_property
from
bliss.config.
static
import
ConfigReference
,
ConfigNode
,
ConfigList
from
bliss.config.
plugins.bliss_controller
import
ConfigItemContainer
def
find_sub_names_config
(
config
,
selection
=
None
,
level
=
0
,
parent_key
=
None
):
""" Recursively search in a config the sub-sections where the key 'name' is found.
Returns a dict of tuples (sub_config, parent_key) indexed by level (0 is the top level).
- sub_config: the sub-config containing the 'name' key
- parent_key: key under which the sub_config was found (None for level 0)
args:
config: the config that should be explored
selection: a list containing the info of the subnames already found (for recursion)
level: an integer describing at which level the subname was found (level=0 is the top/upper level) (for recursion)
parent_key: key under which the sub_config was found (None for level 0) (for recursion)
"""
assert
isinstance
(
config
,
(
ConfigNode
,
dict
))
if
selection
is
None
:
selection
=
{}
if
selection
.
get
(
level
)
is
None
:
selection
[
level
]
=
[]
if
isinstance
(
config
,
ConfigNode
):
name
=
config
.
raw_get
(
"name"
)
else
:
name
=
config
.
get
(
"name"
)
if
name
is
not
None
:
selection
[
level
].
append
((
config
,
parent_key
))
if
isinstance
(
config
,
ConfigNode
):
cfg_items
=
(
config
.
raw_items
()
)
# !!! raw_items to avoid cyclic import while resloving reference !!!