Skip to content

Commit

Permalink
Merge pull request #43 from SweepMe/update-guiparameters
Browse files Browse the repository at this point in the history
New handling of GUI parameters supporting dynamic GUI parameters
  • Loading branch information
fk3 authored Jan 31, 2025
2 parents ec24478 + afe5c21 commit 6b11a8f
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 5 deletions.
145 changes: 141 additions & 4 deletions src/pysweepme/EmptyDeviceClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from __future__ import annotations

import contextlib
import functools
import inspect
import os
from configparser import ConfigParser
Expand Down Expand Up @@ -76,6 +77,33 @@ def __init__(self) -> None:

self._latest_parameters: dict[str, Any] | None = None

def is_function_overwritten(self, function: str) -> bool:
"""Test if a given function is overwritten in a child class of EmptyDevice.
Args:
function: The name of the function to check.
Returns:
True, if the function is overwritten in a child class, False otherwise.
"""
overwrites = False
# Get the class and a list of all base classes in the order python would resolve functions
# using getmro(). If function is element of the classes dictionary and a function, BEFORE looking into
# the EmptyDevice class, then function was obviously overridden by a subclass.
for cls in inspect.getmro(self.__class__):
if cls is EmptyDevice:
break
if function in cls.__dict__ and callable(getattr(cls, function, None)):
overwrites = True
break

return overwrites

@functools.cached_property
def uses_update_gui_parameters(self) -> bool:
"""Boolean that tells if the driver uses the new update_gui_parameters function."""
return self.is_function_overwritten(self.update_gui_parameters.__name__)

@property
def device_communication(self) -> dict[str, Any]:
"""Single (global) dictionary where drivers can store their information that can be shared across instances."""
Expand Down Expand Up @@ -219,13 +247,109 @@ def get_config(self):
"""
return {section: self.get_configoptions(section) for section in self.get_configsections()}

def get_GUIparameter(self, parameter):
def get_GUIparameter(self, parameter: dict[str, Any]):
"""Is overwritten by Device Class to retrieve the GUI parameter selected by the user."""
# Used for compatibility with old code that still uses get_GUIparameter
if self.uses_update_gui_parameters:
if parameter:
self.apply_gui_parameters(parameter)
self.update_gui_parameters(parameter)

def set_GUIparameter(self) -> dict[str, Any]:
"""Is overwritten by Device Class to set the GUI parameter a user can select."""
# Used for compatibility with old code that still uses set_GUIparameter
if self.uses_update_gui_parameters:
return self.update_gui_parameters({})
return {}

def update_gui_parameters_with_fallback(
self, reading_mode: bool, parameters: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Update the driver's current parameters with the given values and return the parameters.
This is a helper function that ensures compatibility with simple drivers. When the GUI parameters
passed to the update_gui_parameters function don't cover all parameters required by simple drivers,
the driver might raise an Exception because a required key is not found. This helper solves the issue
by enhancing any missing GUI parameter with the respective default of the driver.
For advanced drivers, this helper may also add fields, but the advanced driver should be intelligent
enough to only extract the values that are required.
Additionally, None-Type parameters (e.g. from the Parameter Syntax) are replaced with the defaults
as well.
Drivers should not overwrite this function.
Args:
reading_mode: When True, the purpose of this call is to get the default parameters of the driver.
This means this function should call set_GUIparameter for old drivers, and
update_gui_parameters for new drivers.
When False, the purpose of this call is to apply the parameters passed to this function
to the driver. This means this function should call get_GUIparameter for old drivers, and
update_gui_parameters for new drivers.
parameters: A dictionary where keys correspond to the GUI parameter name and the value is
the value as specified in the GUI.
When parameters is None, nothing shall be updated and instead only the defaults shall
be returned.
Returns:
A dictionary where the keys are the fields that shall be shown in the GUI and the values are
the default value. Simple drivers will always return the same defaults.
"""
if not self.uses_update_gui_parameters:
if reading_mode:
return self.set_GUIparameter() # set_GUIparameter will actually get the parameters from the driver
self.get_GUIparameter(parameters or {}) # get_GUIparameter will actually apply the parameters
return {}

# Note to developers:
# The "enhance with defaults" is necessary, because when switching the driver in SweepMe!,
# SweepMe! will pass the GUI parameters of the previous driver to the new driver, which
# obviously doesn't match. Once this behaviour is fixed, this function won't be needed any longer.
# This behaviour does not impact the correctness of the operation, as a second call to update the
# GUI parameters will pass the correct parameters to the driver anyway.
if parameters is not None:
default_parameters = {k: v[0] if isinstance(v, list) and len(v) > 0 else v
for k, v in self.update_gui_parameters({}).items()}
parameters = default_parameters | { k: v for k, v in parameters.items() if v is not None}
if parameters:
self.apply_gui_parameters(parameters)
return self.update_gui_parameters(parameters or {})

def apply_gui_parameters(self, parameters: dict[str, Any]) -> None:
"""Apply the given parameters to the driver instance.
Args:
parameters: A dictionary where keys correspond to the GUI parameter name and the value is
the value as specified in the GUI. Drivers
must be able to handle incomplete or invalid dictionaries (i.e. only certain keys are provided)
and complete them to a valid configuration.
"""

def update_gui_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]: # noqa: ARG002 - defines signature
"""Determine the new GUI parameters of the driver depending on the current parameters.
The available driver's parameters are updated depending on the values that are passed to this function.
The function will then return a dictionary with keys and defaults that correspond to
the current parameter state. Most (simple) drivers will always return the default
GUI parameters. More advanced drivers might return different GUI fields depending on
certain conditions, like other GUI parameters or instrument identification / capabilities.
Args:
parameters: A dictionary where keys correspond to the GUI parameter name and the value is
the value as specified in the GUI. Drivers
must be able to handle incomplete or invalid dictionaries (i.e. only certain keys are provided)
and complete them to a valid configuration.
When parameters is an empty dictionary, nothing shall be updated
and instead only the defaults shall be returned.
Returns:
A dictionary where the keys are the fields that shall be shown in the GUI and the values are
the default value. Simple drivers will always return the same defaults.
"""
msg = ("This driver does not implement the update_gui_parameters function. "
"use either set_GUIparameter and get_GUIparameter, "
"or call the update_gui_parameters_with_fallback function.")
raise NotImplementedError(msg)

def reset_latest_parameters(self) -> None:
"""Initialize or reset the saved parameters to their default.
Expand All @@ -235,7 +359,10 @@ def reset_latest_parameters(self) -> None:
"""
previous_parameters = self._latest_parameters or {}
# we need to do a deepcopy. If the driver has it's defaults in a dictionary, we do not want to change it
self._latest_parameters = deepcopy(self.set_GUIparameter())
if self.uses_update_gui_parameters:
self._latest_parameters = deepcopy(self.update_gui_parameters({}))
else:
self._latest_parameters = deepcopy(self.set_GUIparameter())

# if the default for a property is a list (user shall choose one), we use the first element as the default
for key, default in self._latest_parameters.items():
Expand Down Expand Up @@ -275,7 +402,12 @@ def set_parameters(self, parameters: dict[str, Any] | None = None) -> None:
)
raise ValueError(msg)

self.get_GUIparameter(self._latest_parameters)
if self.uses_update_gui_parameters:
if self._latest_parameters:
self.apply_gui_parameters(self._latest_parameters)
self.update_gui_parameters(self._latest_parameters)
else:
self.get_GUIparameter(self._latest_parameters)

def get_parameters(self) -> dict[str, Any]:
"""Retrieve the parameters that are currently saved for the device.
Expand Down Expand Up @@ -325,7 +457,12 @@ def reconfigure(self, parameters={}, keys=[]):
Default behavior is that all parameters are set again and 'configure' is called.
The device class maintainer can redefine/overwrite 'reconfigure' with a more individual procedure.
"""
self.get_GUIparameter(parameters)
if self.uses_update_gui_parameters:
if parameters:
self.apply_gui_parameters(parameters)
self.update_gui_parameters(parameters)
else:
self.get_GUIparameter(parameters)
self.configure()

def configure(self):
Expand Down
2 changes: 1 addition & 1 deletion src/pysweepme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# SOFTWARE.


__version__ = "1.5.6.13"
__version__ = "1.5.6.14"

import sys

Expand Down

0 comments on commit 6b11a8f

Please sign in to comment.