""" Change dictionary's key naming style """ from typing import ( Any, Callable, Iterable, Mapping, MutableMapping, MutableSequence, Union, ) import stringcase __all__ = ["case_convert", "to_camel", "to_pascal", "to_snake"] ITERABLE_SCALAR_TYPES = (str, bytes, bytearray, memoryview) TCaseFunc = Callable[[str], str] def can_be_recursive(obj): if isinstance(obj, Mapping): return True if isinstance(obj, Iterable) and not isinstance(obj, ITERABLE_SCALAR_TYPES): return True return False def case_convert(obj: Any, case: Union[str, TCaseFunc], inplace: bool = False) -> Any: """Recursively covert dictionary's string key naming style inside `obj` When `obj` is * `dict`: Convert naming style of each key, if the key is string. * `list`: Iterate the list and apply naming style conversion to every item, if the item is a dictionary. * `str`: Simply convert it's naming style. `inplace` argument is ignored in this case. And do above **recursively**. Parameters ---------- obj: object to make a naming case convert on it. case: The naming style want to convert to, usually `"snake"`, `"camel"`, `"pascal"`. Or a function apply the conversion. inplace: Whether to perform an in-place conversion. Returns ------- Converted object """ if callable(case): fct = case elif isinstance(case, str): # dynamic load case-convert function from stringcase module case = case.lower().strip() fct: TCaseFunc = getattr(stringcase, case) if case.endswith("case") else getattr(stringcase, f"{case}case") else: raise TypeError("Argument `case` should be str or callable") if isinstance(obj, Mapping): if inplace: if not isinstance(obj, MutableMapping): raise ValueError(f"Can not apply inplace modification on {type(obj)} object") key_pairs = [(k, fct(k)) for k in obj.keys() if isinstance(k, str)] for k0, k1 in key_pairs: if k0 == k1: continue v = obj.pop(k0) if can_be_recursive(v): obj[k1] = case_convert(v, fct, inplace) else: obj[k1] = v else: return { fct(k) if isinstance(k, str) else k: case_convert(v, fct, inplace) if can_be_recursive(v) else v for k, v in obj.items() } elif isinstance(obj, Iterable) and not isinstance(obj, ITERABLE_SCALAR_TYPES): if inplace: if not isinstance(obj, MutableSequence): raise ValueError(f"Can not apply inplace modification on {type(obj)} object") for i, v in enumerate(obj): if can_be_recursive(i): obj[i] = case_convert(v, fct, inplace) else: return [case_convert(m, fct, inplace) if can_be_recursive(m) else m for m in obj] elif isinstance(obj, str): return fct(obj) return obj def to_camel(obj, inplace=False): return case_convert(obj, stringcase.camelcase, inplace) def to_pascal(obj, inplace=False): return case_convert(obj, stringcase.pascalcase, inplace) def to_snake(obj, inplace=False): return case_convert(obj, stringcase.snakecase, inplace)