from __future__ import annotations
from typing import List, Tuple, Union
import numpy as np
from collections import OrderedDict
from pymodaq_utils.utils import find_keys_from_val
from pymodaq_utils.serialize.factory import SerializableFactory, SerializableBase
from pymodaq_gui.parameter import ioxml
from pymodaq_gui.parameter import Parameter
ser_factory = SerializableFactory()
[docs]
@SerializableFactory.register_decorator()
class ParameterWithPath(SerializableBase):
""" holds together a Parameter object and its full path
To be used when communicating between TCPIP to reconstruct properly the Parameter
Attributes
----------
parameter: Parameter
a Parameter object
path: full path of the parameter, if None it is constructed from the parameter parents
"""
def __init__(self, parameter: Parameter, path: List[str] = None):
super().__init__()
self._parameter = parameter
if path is None:
path = get_param_path(parameter)
self._path = path
def __repr__(self):
return f'Parameter {self.parameter.name()} with path {self.path}'
def __eq__(self, other: 'ParameterWithPath'):
return (self.path == other.path and
compareParameters(self.parameter, other.parameter))
@property
def parameter(self) -> Parameter:
return self._parameter
@property
def path(self) -> List[str]:
return self._path
[docs]
def value(self):
return self.parameter.value()
[docs]
@staticmethod
def serialize(param: 'ParameterWithPath') -> bytes:
""" """
bytes_string = b''
path = param.path
param_as_xml = ioxml.parameter_to_xml_string(param.parameter)
bytes_string += ser_factory.get_apply_serializer(path)
bytes_string += ser_factory.get_apply_serializer(param_as_xml)
return bytes_string
[docs]
@classmethod
def deserialize(cls,
bytes_str: bytes) -> Union[ParameterWithPath,
Tuple[ParameterWithPath, bytes]]:
"""Convert bytes into a ParameterWithPath object
Returns
-------
ParameterWithPath: the decoded object
bytes: the remaining bytes string if any
"""
path, remaining_bytes = ser_factory.get_apply_deserializer(bytes_str, False)
param_as_xml, remaining_bytes = ser_factory.get_apply_deserializer(remaining_bytes, False)
param_dict = ioxml.xml_string_to_parameter_dict(param_as_xml)
param_obj = Parameter.create(**param_dict)
return ParameterWithPath(param_obj, path), remaining_bytes
[docs]
def get_param_path(param: Parameter) -> List[str]:
""" Get the parameter path from its highest parent down to the given parameter including its
identifier (name)
Parameters
----------
param: Parameter
The parameter object
Returns
-------
List[str]: the path as a list of parameter identifiers
"""
path = [param.name()]
while param.parent() is not None:
path.append(param.parent().name())
param = param.parent()
return path[::-1]
[docs]
def getOpts(param:Parameter,) -> OrderedDict:
"""Return an OrderedDict with tree structures of all opts for all children of this parameter
Parameters
----------
param: Parameter
Returns
-------
OrderedDict
"""
vals = OrderedDict()
for ch in param:
vals[ch.name()] = (ch.opts, getOpts(ch))
return vals
[docs]
def getStruct(param:Parameter,) -> OrderedDict:
"""Return an OrderedDict with tree structures of all children of this parameter
Parameters
----------
param: Parameter
Returns
-------
OrderedDict
"""
vals = OrderedDict()
for ch in param:
vals[ch.name()] = (None, getStruct(ch))
return vals
[docs]
def getValues(param:Parameter,) -> OrderedDict:
"""Return an OrderedDict with tree structures of all values for all children of this parameter
Parameters
----------
param: Parameter
Returns
-------
OrderedDict
"""
return param.getValues()
[docs]
def compareParameters(param1:Parameter, param2:Parameter, with_self: bool = True)-> bool:
"""Compare the structure and the opts of two parameters with their children,
return True if structure and all opts are identical.
If with_self is False, only the children opts are compared.
Parameters
----------
param1: Parameter
param2: Parameter
with_self: bool
Returns
-------
Bool
"""
is_same = getOpts(param1) == getOpts(param2)
if with_self:
is_same = is_same and (param1.opts == param2.opts)
return is_same
[docs]
def compareStructureParameter(param1:Parameter, param2: Parameter,)-> bool:
"""Compare the structure of two parameters with their children, return True if structure is identical
Parameters
----------
param1: Parameter
param2: Parameter
Returns
-------
Bool
"""
return getStruct(param1) == getStruct(param2)
[docs]
def compareValuesParameter(param1:Parameter, param2: Parameter, with_self: bool = True)-> bool:
"""Compare the structure and the values of two parameters with their children, return True if structures and values
are identical.
If with_self is False, only the children opts are compared.
Parameters
----------
param1: Parameter
param2: Parameter
with_self: bool
Returns
-------
Bool
"""
is_same = getValues(param1) == getValues(param2)
if with_self:
is_same = is_same and (param1.value == param2.value)
return is_same
[docs]
def iter_children(param, childlist: list = [], filter_type=(), filter_name=(), select_filter=False)-> list:
"""
Get a list of parameters' name under a given Parameter (see iter_children_params)
Returns
-------
list
The list of the children name from the given node.
"""
return iter_children_params(param, childlist=childlist, output_type='name',
filter_type=(), filter_name=(), select_filter=False)
[docs]
def iter_children_params(param, childlist: list = [], output_type=None,
filter_type=(), filter_name=(), select_filter=False)-> list:
"""
Get a list of parameters under a given Parameter.
Parameters
----------
param : Parameter (pyqtgraph)
the root node to be coursed
childlist: list
the child/output list
output_type: str
the attribute of parameter that will be added to the output list
filter_type: list
filter children sharing those types
filter_name: list
filter children sharing those names
select_filter: bool
if True, add filtered parameters to output list.
if False (default), add non-filtered parameter to output list.
Returns
-------
list
The list of the children from the given node.
"""
for child in param.children():
# XNOR Gate
is_filtered = child.type() in filter_type or child.name() in filter_name
add_selected_child = select_filter and is_filtered
add_notselected_child = not select_filter and not is_filtered
if add_selected_child or add_notselected_child:
if output_type is not None:
try:
output = getattr(child,output_type)()
except Exception as e:
print(str(e))
else:
output = child
childlist.append(output)
if child.hasChildren():
childlist.extend(iter_children_params(child, [], output_type, filter_type, filter_name, select_filter))
return childlist
[docs]
def get_param_from_name(parent: Parameter, name) -> Parameter:
"""Get Parameter under parent whose name is name
Parameters
----------
parent: Parameter
name: str
Returns
-------
Parameter
"""
for child in parent.children():
if child.name() == name:
return child
if child.hasChildren():
ch = get_param_from_name(child, name)
if ch is not None:
return ch
[docs]
def is_name_in_dict(dict_tmp, name):
if 'name' in dict_tmp:
if dict_tmp['name'] == name:
return True
return False
[docs]
def get_param_dict_from_name(parent_list, name, pop=False):
"""Get dict under parent whose name is name. The parent_list structure is the one used to init a Parameter object
Parameters
----------
parent_list: (list of dicts) as defined to init Parameter object
name: (str) value to find for the key: name
pop: (bool) if True remove the matched dict from parent
Returns
-------
dict the matched dict
"""
for index, parent_dict in enumerate(parent_list):
if is_name_in_dict(parent_dict, name):
if pop:
return parent_list.pop(index)
else:
return parent_dict
elif 'children' in parent_dict:
ch = get_param_dict_from_name(parent_dict['children'], name, pop)
if ch is not None:
return ch
[docs]
def set_param_from_param(param_old, param_new):
"""
Walk through parameters children and set values using new parameter values.
"""
for child_old in param_old.children():
# try:
path = param_old.childPath(child_old)
child_new = param_new.child(*path)
param_type = child_old.type()
if 'group' not in param_type: # covers 'group', custom 'groupmove'...
# try:
if 'list' in param_type: # check if the value is in the limits of the old params
# (limits are usually set at initialization) but carefull as such paramater limits can be a list or a
# dict object
if isinstance(child_old.opts['limits'], list):
if child_new.value() not in child_old.opts['limits']:
new_limits = child_old.opts['limits'].copy()
new_limits.append(child_new.value())
child_old.setLimits(new_limits)
elif isinstance(child_old.opts['limits'], dict):
if child_new.value() not in child_old.opts['limits'].values():
child_new_key = find_keys_from_val(child_new.opts['limits'], child_new.value())[0]
new_limits = child_old.opts['limits'].copy()
new_limits.update({child_new_key: child_new.value()})
child_old.setLimits(new_limits)
child_old.setValue(child_new.value())
elif 'str' in param_type or 'browsepath' in param_type or 'text' in param_type:
if child_new.value() != "": # to make sure one doesnt overwrite something
child_old.setValue(child_new.value())
else:
child_old.setValue(child_new.value())
# except Exception as e:
# print(str(e))
else:
set_param_from_param(child_old, child_new)
# except Exception as e:
# print(str(e))
[docs]
def filter_parameter_tree(param:Parameter, search_text:str = "") -> bool:
"""
Filter parameter tree based on search text.
Returns True if this parameter or any of its children match the search.
"""
if not search_text:
# If search is empty, show everything
param.show()
for child in param.children():
filter_parameter_tree(child, search_text)
return True
search_lower = search_text.lower()
param_name_lower = param.title().lower()
# Check if current parameter matches
current_matches = search_lower in param_name_lower
# If this is a group and it matches, show all children
if param.hasChildren() and current_matches:
param.setOpts(expanded=True)
param.show()
for child in param.children():
child.show()
# Recursively show all descendants
change_visibility_all_descendants(child, visible=True)
return True
# Check if any children match (recursively)
# Each child is evaluated independently
any_child_matches = False
for child in param.children():
child_matches = filter_parameter_tree(child, search_text)
any_child_matches = any_child_matches or child_matches
# Show this parameter only if it matches OR any of its children match
should_show = current_matches or any_child_matches
if should_show:
param.show()
# If children match, expand this parent to show them
if any_child_matches and param.hasChildren():
items = param.items
if items:
for item in items:
item.setExpanded(True)
param.setOpts(expanded=True)
else:
param.hide()
return should_show
[docs]
def change_visibility_all_descendants(param: Parameter, visible: bool = True):
"""Recursively show all descendants of a parameter"""
if param.hasChildren():
param.setOpts(expanded=True)
for child in param.children():
if visible:
child.show()
else:
child.hide()
change_visibility_all_descendants(child, visible)
if __name__ == '__main__': # pragma: no cover
parent = [
{'title': 'Spectro Settings:', 'name': 'spectro_settings', 'type': 'group', 'expanded': True,
'children': [
{'title': 'Home Wavelength (nm):', 'name': 'spectro_wl_home', 'type': 'float', 'value': 600, 'min': 0,
'readonly': False},
{'title': 'Grating Settings:', 'name': 'grating_settings', 'type': 'group', 'expanded': True,
'children': [
{'title': 'Grating:', 'name': 'grating', 'type': 'list'},
{'title': 'Lines (/mm):', 'name': 'lines', 'type': 'int', 'readonly': True},
{'title': 'Blaze WL (nm):', 'name': 'blaze', 'type': 'str', 'readonly': True},
]},
]
},
]
d = get_param_dict_from_name(parent, 'lines')
d['readonly'] = False
print(parent[0]['children'][1]['children'])