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