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 corresponding widgets. Any other type annotation that cannot 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, }
[docs] @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() }
[docs] @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)
[docs] @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)
[docs] @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