# coding: utf-8
"""General Utilities"""
from __future__ import annotations
import contextlib
import re
from typing import Iterator, NamedTuple, Any, Sequence, TypeVar
from inspect import isclass
import secrets
import string
# match:
# Any uppercase character that is not at the start of a line
# Any Number that is preceded by a Upper or Lower case character
_REG_TO_SNAKE = re.compile(r"(?<!^)(?=[A-Z])|(?<=[A-zA-Z])(?=[0-9])") # re.compile(r"(?<!^)(?=[A-Z])")
_REG_LETTER_AFTER_NUMBER = re.compile(r"(?<=\d)(?=[a-zA-Z])")
NULL_OBJ = object()
"""Null Object uses with None is not an option"""
TNullObj = TypeVar("TNullObj", bound=object)
[docs]class ArgsHelper:
"Args Helper"
[docs] class NameValue(NamedTuple):
"Name Value pair"
name: str
"""Name component"""
value: Any
"""Value component"""
[docs]class Util:
[docs] @staticmethod
def get_obj_full_name(obj: Any) -> str:
"""
Gets the full name of an object. The full name is the module and class name.
Args:
obj (Any): Object to get full name of.
Returns:
str: Full name of object on success; Otherwise, empty string.
"""
if not obj:
return ""
with contextlib.suppress(Exception):
return f"{obj.__class__.__module__}.{obj.__class__.__name__}"
with contextlib.suppress(Exception):
return f"{obj.__module__}.{obj.__name__}"
return ""
[docs] @staticmethod
def generate_random_string(length: int = 8) -> str:
"""
Generates a random string.
Args:
length (int, optional): Length of string to generate. Default ``8``
Returns:
str: Random string
"""
return "".join(secrets.choice(string.ascii_letters) for _ in range(length))
[docs] @staticmethod
def generate_random_hex_string(length: int = 8) -> str:
"""
Generates a random string.
Args:
length (int, optional): Length of string to generate. Default ``8``
Returns:
str: Random string
"""
return "".join(secrets.choice(string.hexdigits) for _ in range(length))
[docs] @staticmethod
def generate_random_alpha_numeric(length: int = 8) -> str:
"""
Generates a random alpha numeric string.
Args:
length (int, optional): Length of string to generate. Default ``8``
Returns:
str: Random string
"""
s = string.ascii_letters + string.digits
return "".join(secrets.choice(s) for _ in range(length))
[docs] @classmethod
def is_iterable(cls, arg: Any, excluded_types: Sequence[type] | None = None) -> bool:
"""
Gets if ``arg`` is iterable.
Args:
arg (object): object to test
excluded_types (Iterable[type], optional): Iterable of type to exclude.
If ``arg`` matches any type in ``excluded_types`` then ``False`` will be returned.
Default ``(str,)``
Returns:
bool: ``True`` if ``arg`` is an iterable object and not of a type in ``excluded_types``;
Otherwise, ``False``.
Note:
if ``arg`` is of type str then return result is ``False``.
.. collapse:: Example
.. code-block:: python
# non-string iterables
assert is_iterable(arg=("f", "f")) # tuple
assert is_iterable(arg=["f", "f"]) # list
assert is_iterable(arg=iter("ff")) # iterator
assert is_iterable(arg=range(44)) # generator
assert is_iterable(arg=b"ff") # bytes (Python 2 calls this a string)
# strings or non-iterables
assert not is_iterable(arg=u"ff") # string
assert not is_iterable(arg=44) # integer
assert not is_iterable(arg=is_iterable) # function
# excluded_types, optionally exclude types
from enum import Enum, auto
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
assert is_iterable(arg=Color) # Enum
assert not is_iterable(arg=Color, excluded_types=(Enum, str)) # Enum
"""
# if isinstance(arg, str):
# return False
if excluded_types is None:
excluded_types = (str,)
result = False
try:
result = isinstance(iter(arg), Iterator)
except Exception:
result = False
if result and cls._is_iterable_excluded(arg, excluded_types=excluded_types):
result = False
return result
@staticmethod
def _is_iterable_excluded(arg: object, excluded_types: Sequence) -> bool:
try:
isinstance(iter(excluded_types), Iterator)
except Exception:
return False
if len(excluded_types) == 0:
return False
def _is_instance(obj: object) -> bool:
# when obj is instance then isinstance(obj, obj) raises TypeError
# when obj is not instance then isinstance(obj, obj) return False
with contextlib.suppress(TypeError):
if not isinstance(obj, obj): # type: ignore
return False
return True
ex_types = excluded_types if isinstance(excluded_types, tuple) else tuple(excluded_types)
arg_instance = _is_instance(arg)
if arg_instance is True:
return isinstance(arg, ex_types)
return True if isclass(arg) and issubclass(arg, ex_types) else arg in ex_types
[docs] @staticmethod
def camel_to_snake(name: str) -> str:
"""Converts CamelCase to snake_case
Args:
name (str): CamelCase string
Returns:
str: snake_case string
Note:
This method is preferred over the `to_snake_case` method when converting CamelCase strings.
It does a better job of handling leading caps. ``UICamelCase`` will be converted to ``ui_camel_case`` and not ``u_i_camel_case``.
"""
# This function uses regular expressions to insert underscores between the lowercase and uppercase letters, then converts the entire string to lowercase.
# The first `re.sub` call inserts an underscore before any uppercase letter that is preceded by a lowercase letter or a number.
# The second `re.sub` call inserts an underscore before any uppercase letter that is followed by a lowercase letter.
# The `lower` method then converts the entire string to lowercase.
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
[docs] @staticmethod
def to_camel_case(s: str) -> str:
"""
Converts string to ``CamelCase``
Args:
s (str): string to convert such as ``snake_case_word`` or ``pascalCaseWord``
Returns:
str: string converted to ``CamelCaseWord``
"""
s = s.strip()
if not s:
return ""
result = s
if "_" in result:
return "".join(word.title() for word in result.split("_"))
return result[:1].upper() + result[1:]
[docs] @classmethod
def to_pascal_case(cls, s: str) -> str:
"""
Converts string to ``pascalCase``
Args:
s (str): string to convert such as ``snake_case_word`` or ``CamelCaseWord``
Returns:
str: string converted to ``pascalCaseWord``
"""
result = cls.to_camel_case(s)
if result:
result = result[:1].lower() + result[1:]
return result
[docs] @staticmethod
def to_snake_case(s: str) -> str:
"""
Convert string to ``snake_case``
Args:
s (str): string to convert such as ``pascalCaseWord`` or ``CamelCaseWord``
Returns:
str: string converted to ``snake_case_word``
"""
s = s.strip()
if not s:
return ""
result = _REG_TO_SNAKE.sub("_", s)
result = _REG_LETTER_AFTER_NUMBER.sub("_", result)
return result.lower()
[docs] @classmethod
def to_snake_case_upper(cls, s: str) -> str:
"""
Convert string to ``SNAKE_CASE``
Args:
s (str): string to convert such as ``snake_case_word`` or ``pascalCaseWord`` or ``CamelCaseWord``
Returns:
str: string converted to ``SNAKE_CASE_WORD``
"""
result = cls.to_snake_case(s)
return result.upper() if s else ""
[docs] @staticmethod
def to_single_space(s: str, strip=True) -> str:
"""
Gets a string with multiple spaces converted to single spaces
Args:
s (str): String
strip (bool, optional): If ``True`` then whitespace is stripped from start and end or string. Default ``True``.
Returns:
str: String with extra spaces removed.
Example:
.. code-block:: python
>>> s = ' The quick brown fox'
>>> print(Util.to_single_space(s))
'The quick brown fox'
"""
if not s:
return ""
result = re.sub(" +", " ", s)
return result.strip() if strip else result
@staticmethod
def _atoi(text):
return int(text) if text.isdigit() else text
[docs] @staticmethod
def natural_key_sorter(text: str) -> list:
"""
Sort Key Sorts in human order.
# Example:
.. code-block:: python
a_list.sort(key=Util.natural_key_sorter)
"""
# a_list.sort(key=natural_keys) sorts in human order
# http://nedbatchelder.com/blog/200712/human_sorting.html
# (See Toothy's implementation in the comments)
# https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside
return [Util._atoi(c) for c in re.split(r"(\d+)", text)]
[docs] @staticmethod
def get_index(idx: int, count: int, allow_greater: bool = False) -> int:
"""
Gets the index.
Args:
idx (int): Index of element. Can be a negative value to index from the end of the list.
count (int): Number of elements.
allow_greater (bool, optional): If True and index is greater then the count
then the index becomes the next index if element is appended. Defaults to False.
Only affect the ``-1`` index.
Raises:
ValueError: If ``count`` is less than ``0``.
IndexError: If index is out or range.
Returns:
int: Index value.
Note:
``-1`` is the last index in the sequence. Unless ``allow_greater`` is ``True`` then ``-1`` last index ``+ 1``.
Only the ``-1`` is treated differently when ``allow_greater`` is ``True``.
``-2`` is the second to last index in the sequence. ``10`` items and ``idx=-2`` then index ``8`` is returned.
``-3`` is the third to last index in the sequence. ``10`` items and ``idx=-3`` then index ``7`` is returned.
.. versionadded:: 0.20.2
"""
if count < 0:
raise ValueError("count cannot be less than 0")
if idx == -1:
index = 0
if allow_greater:
if count == 0:
return index
else:
index = count
else:
index = count - 1
if index < 0:
raise IndexError("list index out of range")
return index
index = idx
if index < 0:
index = count + index
if index < 0:
raise IndexError("list index out of range")
if index >= count:
if allow_greater:
index = count
else:
raise IndexError("list index out of range")
return index