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
Expand all
Hide whitespace changes
Inline
Side-by-side
bliss/common/soft_axis.py
View file @
a14b481b
...
@@ -32,7 +32,7 @@ def SoftAxis(
...
@@ -32,7 +32,7 @@ def SoftAxis(
config
[
"unit"
]
=
unit
config
[
"unit"
]
=
unit
controller
=
SoftController
(
name
,
obj
,
config
,
position
,
move
,
stop
,
state
)
controller
=
SoftController
(
name
,
obj
,
config
,
position
,
move
,
stop
,
state
)
controller
.
_
controller_init
()
controller
.
_
initialize_config
()
axis
=
controller
.
get_axis
(
name
)
axis
=
controller
.
get_axis
(
name
)
axis
.
_positioner
=
False
axis
.
_positioner
=
False
...
...
bliss/config/plugins/bliss_controller.py
View file @
a14b481b
This diff is collapsed.
Click to expand it.
bliss/controllers/bliss_controller.py
View file @
a14b481b
...
@@ -5,74 +5,15 @@
...
@@ -5,74 +5,15 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from
importlib
import
import_module
from
bliss.common.protocols
import
CounterContainer
from
bliss.common.protocols
import
CounterContainer
from
bliss.common.utils
import
autocomplete_property
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
):
class
BlissController
(
CounterContainer
,
ConfigItemContainer
):
""" 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
def
from_config_dict
(
ctrl_class
,
cfg_dict
):
""" Helper to instanciate a BlissController object from a configuration dictionary """
if
BlissController
not
in
ctrl_class
.
mro
():
raise
TypeError
(
f
"
{
ctrl_class
}
is not a BlissController class"
)
bctrl
=
ctrl_class
(
cfg_dict
)
bctrl
.
_controller_init
()
return
bctrl
class
BlissController
(
CounterContainer
):
"""
"""
BlissController base class is made for the implementation of all Bliss controllers.
BlissController base class is made for the implementation of all Bliss controllers.
It is designed to ease the management of sub-objects that depend on a shared controller.
It is designed to ease the management of sub-objects that depend on a shared controller
(see ConfigItemContainer)
.
Sub-objects are declared in the yml configuration of the controller under dedicated sub-sections.
Sub-objects are declared in the yml configuration of the controller 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).
A sub-object is considered as a subitem if it has a name (key 'name' in a sub-section of the config).
...
@@ -120,12 +61,12 @@ class BlissController(CounterContainer):
...
@@ -120,12 +61,12 @@ class BlissController(CounterContainer):
# --- From config dict ---
# --- From config dict ---
A BlissController can be instantiated directly (i.e. not via plugin) providing a config as a dictionary.
A BlissController can be instantiated directly (i.e. not via plugin) providing a config as a dictionary.
In that case, users must call the method 'self._
controller_init
()' just after the controller instantiation
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.
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
The config dictionary should be structured like a YML file (i.e: nested dict and list) and
references replaced by their corresponding object instances.
references replaced by their corresponding object instances.
Example: bctrl = BlissController( config_dict ) => bctrl._
controller_init
()
Example: bctrl = BlissController( config_dict ) => bctrl._
initialize_config
()
# --- yml config example ---
# --- yml config example ---
...
@@ -162,27 +103,7 @@ class BlissController(CounterContainer):
...
@@ -162,27 +103,7 @@ class BlissController(CounterContainer):
something: value
something: value
"""
"""
def
__init__
(
self
,
config
):
# ========== STANDARD PROPERTIES ============================
self
.
__subitems_configs_ready
=
False
self
.
__ctrl_is_initialized
=
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)
self
.
_hw_controller
=
(
None
)
# acces the low level hardware controller interface (if any)
# generate generic name if no controller name 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 ============================
@
autocomplete_property
@
autocomplete_property
def
hardware
(
self
):
def
hardware
(
self
):
...
@@ -190,256 +111,12 @@ class BlissController(CounterContainer):
...
@@ -190,256 +111,12 @@ class BlissController(CounterContainer):
self
.
_hw_controller
=
self
.
_create_hardware
()
self
.
_hw_controller
=
self
.
_create_hardware
()
return
self
.
_hw_controller
return
self
.
_hw_controller
@
property
def
name
(
self
):
return
self
.
_name
@
property
def
config
(
self
):
return
self
.
_config
# ========== INTERNAL METHODS (PRIVATE) ============================
@
property
def
_is_initialized
(
self
):
return
self
.
__ctrl_is_initialized
def
__build_subitem_from_config
(
self
,
name
):
"""
Standard method to create an item from its config.
This method is called by either:
- the plugin, via a config.get(item_name) => create_object_from_cache => name is exported in session
- the controller, via self._get_subitem(item_name) => name is NOT exported in session
"""
# print(f"=== Build item {name} from {self.name}")
if
not
self
.
__ctrl_is_initialized
:
raise
RuntimeError
(
f
"Controller not initialized:
{
self
}
\n
Call 'ctrl._controller_init()'"
)
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
+=
"Check item config is supported by this controller"
raise
RuntimeError
(
msg
)
self
.
_subitems
[
name
]
=
item
def
__find_item_class
(
self
,
cfg
,
pkey
):
"""
Return a suitable class for an item of a bliss controller.
It tries to find a class_name in the item's config or ask the controller for a default.
The class_name could be an absolute path, else the class is searched in the controller
module first. If not found, ask the controller 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 controller
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
+=
"Check item config is supported by this controller
\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 controller module level first
if
hasattr
(
module
,
class_name
):
return
getattr
(
module
,
class_name
)
else
:
# ask the controller 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
+=
"Check item config is supported by this controller
\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 controller that owns the items declared in the config.
By default, this controller is the owner of all config items.
However if this controller has sub-controllers that are the real owners
of some items, this method should use to specify which sub-controller is
the owner of which item (identified with name and pkey).
"""
return
self
def
_prepare_subitems_configs
(
self
):
""" Find all sub objects with a name in the controller config.
Store the items config info (cfg, pkey) in the controller (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 controller 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 controller.
# (i.e exclude referenced names as they are not owned by this controller)
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
_controller_init
(
self
):
""" Initialize a controller the same way as the plugin does.
This method must be called if the controller has been directly
instantiated with a config dictionary (i.e without going through the plugin and YML config).
"""
if
not
self
.
__ctrl_is_initialized
:
if
not
self
.
__subitems_configs_ready
:
self
.
_prepare_subitems_configs
()
self
.
__ctrl_is_initialized
=
True
try
:
self
.
_load_config
()
self
.
_init
()
except
BaseException
:
self
.
__ctrl_is_initialized
=
False
raise
# ========== ABSTRACT METHODS ====================
# ========== ABSTRACT METHODS ====================
def
_create_hardware
(
self
):
def
_create_hardware
(
self
):
""" return the low level hardware controller interface """
""" return the low level hardware controller interface """
raise
NotImplementedError
raise
NotImplementedError
def
_get_default_chain_counter_controller
(
self
):
""" return the counter controller that shoud be used with the DefaultAcquisitionChain (i.e for standard step by step scans) """
raise
NotImplementedError
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 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 controller 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 appropriate default class for a given item.
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 controller.
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 ===
# return item_class(cfg)
raise
NotImplementedError
def
_load_config
(
self
):
# Called by bliss_controller plugin (after self._subitems_config has_been filled).
"""
Read and apply the YML configuration of the controller.
"""
raise
NotImplementedError
def
_init
(
self
):
# Called by bliss_controller plugin (just after self._load_config)
"""
Place holder for any action to perform after the configuration has been loaded.
"""
pass
@
autocomplete_property
@
autocomplete_property
def
counters
(
self
):
def
counters
(
self
):
raise
NotImplementedError
raise
NotImplementedError
...
...
bliss/controllers/bliss_controller_mockup.py
View file @
a14b481b
...
@@ -143,7 +143,7 @@ class BCMockup(BlissController):
...
@@ -143,7 +143,7 @@ class BCMockup(BlissController):
return
cnt
return
cnt
elif
parent_key
==
"operators"
:
elif
parent_key
==
"operators"
:
return
item_class
(
cfg
)
return
item_class
(
name
,
cfg
)
elif
parent_key
==
"axes"
:
elif
parent_key
==
"axes"
:
if
item_class
is
None
:
# mean it is a referenced axis (i.e external axis)
if
item_class
is
None
:
# mean it is a referenced axis (i.e external axis)
...
@@ -288,7 +288,7 @@ class FakeItem:
...
@@ -288,7 +288,7 @@ class FakeItem:
class
Operator
:
class
Operator
:
def
__init__
(
self
,
cfg
):
def
__init__
(
self
,
name
,
cfg
):
self
.
name
=
cfg
[
"name"
]
self
.
name
=
cfg
[
"name"
]
self
.
tag
=
cfg
.
get
(
"tag"
)
self
.
tag
=
cfg
.
get
(
"tag"
)
self
.
factor
=
cfg
[
"factor"
]
self
.
factor
=
cfg
[
"factor"
]
...
...
bliss/controllers/diffractometers/diff_base.py
View file @
a14b481b
...
@@ -202,7 +202,7 @@ class Diffractometer(BlissController):
...
@@ -202,7 +202,7 @@ class Diffractometer(BlissController):
pass
pass
def
_init
(
self
):
def
_init
(
self
):
self
.
_motor_calc
.
_
controller_init
()
self
.
_motor_calc
.
_
initialize_config
()
self
.
calc_controller
=
self
.
_motor_calc
self
.
calc_controller
=
self
.
_motor_calc
def
__info__
(
self
):
def
__info__
(
self
):
...
...
tests/controllers/test_bliss_controller.py
View file @
a14b481b
...
@@ -82,7 +82,7 @@ def test_plugin_get_items_from_config(default_session):
...
@@ -82,7 +82,7 @@ def test_plugin_get_items_from_config(default_session):
assert
isinstance
(
fakeop1
,
Operator
)
assert
isinstance
(
fakeop1
,
Operator
)
# check that a subitem of a none-bliss_controller cannot be loaded
# check that a subitem of a none-bliss_controller cannot be loaded
with
pytest
.
raises
(
TypeError
,
match
=
"
is not a BlissControll
er object"
):
with
pytest
.
raises
(
TypeError
,
match
=
"
must be a ConfigItemContain
er object"
):
default_session
.
config
.
get
(
"not_allowed_item"
)
default_session
.
config
.
get
(
"not_allowed_item"
)
...
...
tests/motors/test_initialization.py
View file @
a14b481b
...
@@ -122,26 +122,26 @@ def test_broken_controller_init(default_session):
...
@@ -122,26 +122,26 @@ def test_broken_controller_init(default_session):
with
mock
.
patch
(
with
mock
.
patch
(
"bliss.controllers.motors.mockup.Mockup.initialize"
,
wraps
=
faulty_initialize
"bliss.controllers.motors.mockup.Mockup.initialize"
,
wraps
=
faulty_initialize
):
):
# === expecting failure during plugin.from_config => BlissController._
controller_init
()
# === expecting failure during plugin.from_config => BlissController._
initialize_config
()
with
pytest
.
raises
(
RuntimeError
,
match
=
"FAILED TO INITIALIZE"
):
with
pytest
.
raises
(
RuntimeError
,
match
=
"FAILED TO INITIALIZE"
):
default_session
.
config
.
get
(
"roby"
)
default_session
.
config
.
get
(
"roby"
)
# === now config._name2instance is still empty because _
controller_init
() has failed and roby was not instanciated
# === now config._name2instance is still empty because _
initialize_config
() has failed and roby was not instanciated
# === config._name2cache is also empty because cacheditems have been removed when _
controller_init
() has failed
# === config._name2cache is also empty because cacheditems have been removed when _
initialize_config
() has failed
# print("=== match=FAILED TO INITIALIZE")
# print("=== match=FAILED TO INITIALIZE")
# print("=== name2instance:", list(config._name2instance.keys()))
# print("=== name2instance:", list(config._name2instance.keys()))
# print("=== name2cache:", list(config._name2cache.keys()))
# print("=== name2cache:", list(config._name2cache.keys()))
assert
list
(
config
.
_name2instance
.
keys
())
==
[]
assert
list
(
config
.
_name2instance
.
keys
())
==
[]
assert
list
(
config
.
_name2cache
.
keys
())
==
[]
assert
list
(
config
.
_name2cache
.
keys
())
==
[]
# === expecting same failure during plugin.from_config => BlissController._
controller_init
()
# === expecting same failure during plugin.from_config => BlissController._
initialize_config
()
with
pytest
.
raises
(
with
pytest
.
raises
(
RuntimeError
,
match
=
"FAILED TO INITIALIZE"
RuntimeError
,
match
=
"FAILED TO INITIALIZE"
):
# Controller is disabled
):
# Controller is disabled
default_session
.
config
.
get
(
"roby"
)
default_session
.
config
.
get
(
"roby"
)
# === now config._name2instance is still empty because _
controller_init
() has failed and roby was not instanciated
# === now config._name2instance is still empty because _
initialize_config
() has failed and roby was not instanciated
# === config._name2cache is also empty because cacheditems have been removed when _
controller_init
() has failed
# === config._name2cache is also empty because cacheditems have been removed when _
initialize_config
() has failed
# print("=== match=FAILED TO INITIALIZE")
# print("=== match=FAILED TO INITIALIZE")
# print("=== name2instance:", list(config._name2instance.keys()))
# print("=== name2instance:", list(config._name2instance.keys()))
# print("=== name2cache:", list(config._name2cache.keys()))
# print("=== name2cache:", list(config._name2cache.keys()))
...
...
tests/motors/test_motion_hook.py
View file @
a14b481b
...
@@ -35,7 +35,7 @@ def test_motion_hook_init(beacon):
...
@@ -35,7 +35,7 @@ def test_motion_hook_init(beacon):
cfg
=
{
"axes"
:
[
config_node
]}
cfg
=
{
"axes"
:
[
config_node
]}
mockup_controller
=
Mockup
(
cfg
)
mockup_controller
=
Mockup
(
cfg
)
mockup_controller
.
_
controller_init
()
mockup_controller
.
_
initialize_config
()
test_mh
=
None
test_mh
=
None
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment