Source code for qudi.util.parameters
# -*- coding: utf-8 -*-
"""
This file contains utility methods to annotate arguments for which the user can potentially edit
values via GUI. These arguments are boiled down to simple builtin types that can be represented by
a GUI editor.
Parameters
----------
:int: :class:`~qudi.util.widgets.scientific_spinbox.ScienSpinBox`
An integer type represented by a scientific spinbox widget.
:float: :class:`~qudi.util.widgets.scientific_spinbox.ScienDSpinbox`
A floating-point type represented by a double precision scientific spinbox widget.
:str: :class:`~PySide2.QtWidgets.QLineEdit`
A string type represented by a QLineEdit widget.
:complex: :class:`~qudi.util.widgets.literal_lineedit.ComplexLineEdit`
A complex number type represented by a custom ComplexLineEdit widget.
:set: :class:`~qudi.util.widgets.literal_lineedit.SetLineEdit`
A set type represented by a custom SetLineEdit widget.
:dict: :class:`~qudi.util.widgets.literal_lineedit.DictLineEdit`
A dictionary type represented by a custom DictLineEdit widget.
:list: :class:`~qudi.util.widgets.literal_lineedit.ListLineEdit`
A list type represented by a custom ListLineEdit widget.
:tuple: :class:`~qudi.util.widgets.literal_lineedit.TupleLineEdit`
A tuple type represented by a custom TupleLineEdit widget.
:FilePath: :class:`~PySide2.QtWidgets.QLineEdit`
A type for file paths represented by a QLineEdit widget.
Notes
-----
Here defined are also custom generic types that can be used like any other type from
the typing module.
Arguments annotated with these types are represented with the following widgets:
FilePath: PySide2.QtWidgets.QLineEdit
Any other type annotation that can not be mapped to a simple builtin or a custom generic type
(see above) will result in no corresponding widget (None).
Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>
This file is part of qudi.
Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""
__all__ = ['FilePath', 'RealNumber', 'ParameterWidgetMapper']
import inspect
import typing
from os import PathLike
from PySide2 import QtWidgets
from typing import Callable, Any, Set, FrozenSet, MutableSequence, Mapping, Tuple, Dict, Type, Union
from typing import get_origin, get_args, Iterable, Sequence
from qudi.util.helpers import is_complex_type, is_float_type, is_integer_type, is_string_type
from qudi.util.widgets.scientific_spinbox import ScienSpinBox, ScienDSpinBox
from qudi.util.widgets.literal_lineedit import ComplexLineEdit, TupleLineEdit, ListLineEdit
from qudi.util.widgets.literal_lineedit import SetLineEdit, DictLineEdit
FilePath = Union[str, bytes, PathLike]
RealNumber = Union[int, float]
[docs]
class ParameterWidgetMapper:
_type_widget_map = {int: ScienSpinBox,
float: ScienDSpinBox,
str: QtWidgets.QLineEdit,
PathLike: QtWidgets.QLineEdit,
complex: ComplexLineEdit,
set: SetLineEdit,
dict: DictLineEdit,
tuple: TupleLineEdit,
list: ListLineEdit}
@classmethod
def widgets_for_callable(cls, func: Callable) -> Dict[str, Union[Type[QtWidgets.QWidget], None]]:
"""Returns QWidget classes for each parameter from the call signature of "func".
See ParameterWidgetMapper.widget_for_parameter for more information.
"""
sig = inspect.signature(func)
return {name: cls.widget_for_parameter(param) for name, param in sig.parameters.items()}
@classmethod
def widget_for_parameter(cls, param: inspect.Parameter) -> Union[Type[QtWidgets.QWidget], None]:
"""Tries to determine a suitable QWidget to represent the given parameter.
If no type annotation is given for a parameter it will try to determine the type from the
default value.
If no (known) type can be determined, None will be returned.
"""
# Try to deduce missing type annotation from default parameter type
if param.annotation is inspect.Parameter.empty:
if param.default is inspect.Parameter.empty:
return None
else:
return cls.widget_from_value(param.default)
else:
return cls.widget_from_annotation(param.annotation)
@classmethod
def widget_from_value(cls, value: Any) -> Union[Type[QtWidgets.QWidget], None]:
"""Tries to determine a suitable QWidget to represent the type of the given value.
"""
normalized_type = cls._normalize_type(type(value))
return cls._type_widget_map.get(normalized_type, None)
@classmethod
def widget_from_annotation(cls, annotation: Any) -> Union[Type[QtWidgets.QWidget], None]:
"""Tries to determine a suitable QWidget to represent values of the given annotation type.
"""
normalized_type = cls._annotation_to_type(annotation)
return cls._type_widget_map.get(normalized_type, None)
@staticmethod
def _normalize_type(typ: Type) -> Type:
"""Normalizes given type to a base/builtin type.
Examples:
numpy.float32 -> float
collections.OrderedDict -> dict
"""
if is_string_type(typ):
return str
elif is_integer_type(typ):
return int
elif is_float_type(typ):
return float
elif is_complex_type(typ):
return complex
elif issubclass(typ, (bytes, PathLike)):
return PathLike
elif issubclass(typ, (Set, FrozenSet)):
return set
elif issubclass(typ, Mapping):
return dict
elif issubclass(typ, MutableSequence):
return list
elif issubclass(typ, (Tuple, Iterable, Sequence)):
return tuple
return None
@classmethod
def _annotation_to_type(cls, annotation: Any) -> Type:
"""Converts a type annotation (e.g. from a callable signature) to a normalized type.
See ParameterWidgetMapper._normalize_type for more information.
"""
# If annotation is optional, call this method again on the first argument in order to get a
# type
if cls._is_optional_annotation(annotation):
return cls._annotation_to_type(get_args(annotation)[0])
if annotation == RealNumber:
return float
elif annotation == FilePath:
return PathLike
else:
try:
if inspect.isclass(annotation):
return cls._normalize_type(annotation)
else:
return cls._normalize_type(get_origin(annotation))
except TypeError:
pass
return None
@staticmethod
def _is_optional_annotation(annotation: Any) -> bool:
"""Check if an annotation is optional, i.e. if it is typing.Union with two arguments,
the second one being NoneType.
"""
if get_origin(annotation) == typing.Union:
args = get_args(annotation)
return len(args) == 2 and issubclass(args[1], type(None))
return False