Source code for qudi.util.helpers

# -*- coding: utf-8 -*-
"""
.. 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__ = [
    "csv_2_list",
    "in_range",
    "is_complex",
    "is_complex_type",
    "is_float",
    "is_float_type",
    "is_integer",
    "is_integer_type",
    "is_number",
    "is_number_type",
    "is_string",
    "is_string_type",
    "iter_modules_recursive",
    "natural_sort",
    "str_to_number",
]

import re
import os
import pkgutil
import numpy as np
from typing import Union, Optional, Iterable, List, Any, Type, Tuple, Callable

_RealNumber = Union[int, float]


[docs] def iter_modules_recursive( paths: Union[str, Iterable[str]], prefix: Optional[str] = "" ) -> List[pkgutil.ModuleInfo]: """ Has the same signature as pkgutil.iter_modules() but extends the functionality by walking through the entire directory tree and concatenating the return values of pkgutil.iter_modules() for each directory. Additional modifications include: - Directories starting with "_" or "." are ignored (including their sub-directories). - Python modules starting with a double-underscore ("__") are excluded from the result. Parameters ---------- paths : iterable Iterable of root directories to start the search for modules. prefix : str, optional Prefix to prepend to all module names. Returns ------- iterable Concatenated return values of pkgutil.iter_modules() for all directories in the tree. """ if isinstance(paths, str): paths = [paths] module_infos = list() for search_top in paths: for root, dirs, files in os.walk(search_top): rel_path = os.path.relpath(root, search_top) if rel_path and rel_path != "." and rel_path[0] in "._": # Prevent os.walk to descent further down this tree branch dirs.clear() # Ignore this directory continue # Resolve current module prefix if not rel_path or rel_path == ".": curr_prefix = prefix else: curr_prefix = prefix + ".".join(rel_path.split(os.sep)) + "." # find modules and packages in current dir tmp = pkgutil.iter_modules([root], prefix=curr_prefix) module_infos.extend( [ mod_inf for mod_inf in tmp if not mod_inf.name.rsplit(".", 1)[-1].startswith("__") ] ) return module_infos
[docs] def natural_sort(iterable: Iterable[Any]) -> List[Any]: """ Sort an iterable of strings in an intuitive, natural way (human/natural sort). This is useful for sorting alphanumeric strings that contain integers. Parameters ---------- iterable : list of str Iterable with string items to sort. Returns ------- list Sorted list of strings. """ def conv(s): return int(s) if s.isdigit() else s try: return sorted( iterable, key=lambda key: [conv(i) for i in re.split(r"(\d+)", key)] ) except: return sorted(iterable)
[docs] def is_number(test_value: Any) -> bool: """Check whether passed value is a number""" return is_integer(test_value) or is_float(test_value) or is_complex(test_value)
[docs] def is_number_type(test_obj: Type) -> bool: """Check whether passed object is a number type""" return ( is_integer_type(test_obj) or is_float_type(test_obj) or is_complex_type(test_obj) )
[docs] def is_integer(test_value: Any) -> bool: """Check all available integer representations""" return isinstance(test_value, (int, np.integer))
[docs] def is_integer_type(test_obj: Type) -> bool: """Check if passed object is an integer type""" return issubclass(test_obj, (int, np.integer))
[docs] def is_float(test_value: Any) -> bool: """Check all available float representations""" return isinstance(test_value, (float, np.floating))
[docs] def is_float_type(test_obj: Type) -> bool: """Check if passed object is a float type""" return issubclass(test_obj, (float, np.floating))
[docs] def is_complex(test_value: Any) -> bool: """Check all available complex representations""" return isinstance(test_value, (complex, np.complexfloating))
[docs] def is_complex_type(test_obj: Type) -> bool: """Check if passed object is a complex type""" return issubclass(test_obj, (complex, np.complexfloating))
[docs] def is_string(test_value: Any) -> bool: """Check all available string representations""" return isinstance(test_value, (str, np.str_, np.string_))
[docs] def is_string_type(test_obj: Type) -> bool: """Check if passed object is a string type""" return issubclass(test_obj, (str, np.str_, np.string_))
[docs] def in_range( value: _RealNumber, lower_limit: _RealNumber, upper_limit: _RealNumber ) -> Tuple[bool, _RealNumber]: """Check if a value is in a given range an return closest possible value in range. Also check the range. Return value is clipped to range. """ if upper_limit < lower_limit: lower_limit, upper_limit = upper_limit, lower_limit if value > upper_limit: return False, upper_limit if value < lower_limit: return False, lower_limit return True, value
[docs] def csv_2_list( csv_string: str, str_2_val: Optional[Callable[[str], Any]] = None ) -> List[Any]: """ Parse a list literal (with or without square brackets) given as a string containing comma-separated int or float values to a Python list. Blanks before and after commas are handled. Parameters ---------- csv_string : str Scalar number literals as strings separated by a single comma and any number of blanks. Brackets are ignored. Example: '[1e-6,2.5e6, 42]' or '1e-6, 2e-6, 42'. str_2_val : function, optional Function to use for casting substrings into single values. Returns ------- list List of float values. If `str_2_val` is provided, type is invoked by this function. """ if not isinstance(csv_string, str): raise TypeError("string_2_list accepts only str type input.") if csv_string == "": return [] csv_string = csv_string.replace("[", "").replace("]", "") # Remove square brackets csv_string = csv_string.replace("(", "").replace(")", "") # Remove round brackets csv_string = csv_string.replace("{", "").replace("}", "") # Remove curly brackets csv_string = csv_string.strip().strip( "," ) # Remove trailing/leading blanks and commas # Cast each str value to float if no explicit cast function is given by parameter str_2_val. if str_2_val is None: csv_list = [str_to_number(val_str) for val_str in csv_string.split(",")] else: csv_list = [str_2_val(val_str.strip()) for val_str in csv_string.split(",")] return csv_list
[docs] def str_to_number( str_value: str, return_failed: Optional[bool] = False ) -> Union[int, float, complex, str]: """Parse a string into either int, float or complex (in that order)""" try: return int(str_value) except ValueError: try: return float(str_value) except ValueError: try: return complex(str_value) except ValueError: if return_failed: return str_value else: raise ValueError( f"Could not convert string to int, float or complex: '{str_value}'" )