from __future__ import annotations
from contextlib import contextmanager
import inspect
from typing import Any, Dict, Iterable, Iterator, List, Sequence, Optional, Tuple, Union
import numpy as np
from collections import OrderedDict
from pymodaq_utils.utils import find_keys_from_val
from serializall import SerializableFactory, SerializableBase
from pymodaq_gui.parameter import ioxml
from pymodaq_gui.parameter import Parameter
ser_factory = SerializableFactory()
_TREE_CHANGE_BLOCKER_HAS_KEEP = 'keep' in inspect.signature(Parameter.treeChangeBlocker).parameters
[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 (a list of names, an empty tuple is also used for a parameter not in a tree),
if None it is constructed from the parameter parents
"""
def __init__(self, parameter: Parameter, path: Optional[Sequence[str]] = None) -> None:
super().__init__()
self._parameter = parameter
if path is None:
path = get_param_path(parameter)
self._path = path
def __repr__(self) -> str:
return f'Parameter {self.parameter.name()} with path {self.path}'
def __eq__(self, other: object) -> bool:
return (self.path == other.path and # type: ignore[attr-defined] # only ParameterWithPath are compared
compareStructureParameter(self.parameter, other.parameter)) # type: ignore[attr-defined]
@property
def parameter(self) -> Parameter:
return self._parameter
@property
def path(self) -> Sequence[str]:
return self._path
[docs]
def value(self) -> Any:
"""The value of the parameter"""
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
"""
values = OrderedDict()
for ch in param:
if 'value' in ch.opts:
values[ch.name()] = (ch.value(), getValues(ch))
else:
values[ch.name()] = (None, getValues(ch))
return values
[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: Parameter, childlist: Optional[list] = None, filter_type: Iterable[str] = (),
filter_name: Iterable[str] = (), select_filter: bool = 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_type, filter_name=filter_name, select_filter=select_filter)
[docs]
def iter_children_params(param: Parameter, childlist: Optional[list] = None, output_type: Optional[str] = None,
filter_type: Iterable[str] = (), filter_name: Iterable[str] = (),
select_filter: bool = False) -> list:
"""
Get a list of parameters under a given Parameter.
Parameters
----------
param : Parameter (pyqtgraph)
the root node to be coursed
childlist: list, optional
the child/output list, the results are appended to it. A new list is used if None
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.
"""
if childlist is None:
childlist = []
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:
childlist.append(getattr(child, output_type)())
except Exception as e: # this child cannot give the attribute: it is skipped
print(str(e))
else:
childlist.append(child)
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: str) -> Optional[Parameter]:
"""Get Parameter under parent whose name is name
Parameters
----------
parent: Parameter
name: str
Returns
-------
Parameter
The first parameter found (depth first), None if there is none
"""
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
return None
[docs]
def is_name_in_dict(dict_tmp: Dict[str, Any], name: str) -> bool:
"""Check if a dict, as used to init a Parameter, has the given name"""
if 'name' in dict_tmp:
if dict_tmp['name'] == name:
return True
return False
[docs]
def get_param_dict_from_name(parent_list: List[Dict[str, Any]], name: str,
pop: bool = False) -> Optional[Dict[str, Any]]:
"""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, None if there is none
"""
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
return None
[docs]
def set_param_from_param(param_old: Parameter, param_new: Parameter) -> None:
"""
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 'value' in child_new.opts:
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_dict = child_old.opts['limits'].copy()
new_limits_dict.update({child_new_key: child_new.value()})
child_old.setLimits(new_limits_dict)
if 'value' in child_new.opts:
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) -> None:
"""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: List[Dict[str, Any]] = [
{'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 # type: ignore[index]
print(parent[0]['children'][1]['children'])
[docs]
@contextmanager
def tree_change_blocker(param: Parameter, keep: Optional[set[str]] = None) -> Iterator[None]:
""" Block and accumulate the tree change signals of param, keeping only some change types
Backward compatible version of ``Parameter.treeChangeBlocker(keep=...)``, whose ``keep`` argument is only
available in recent pyqtgraph versions.
Parameters
----------
param: Parameter
the parameter whose tree change signals are blocked
keep: set of str
the change types (``'value'``, ``'limits'``...) emitted once unblocked. None keeps them all, an empty set
discards all the changes made within the block
"""
if _TREE_CHANGE_BLOCKER_HAS_KEEP:
with param.treeChangeBlocker(keep=keep):
yield
return
param.blockTreeChangeSignal()
try:
yield
finally:
if keep is not None and param.blockTreeChangeEmit == 1: # outermost block: filter what will be emitted
param.treeStateChanges = [change for change in param.treeStateChanges if change[1] in keep]
param.unblockTreeChangeSignal()