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}\'' )