Last active
June 16, 2023 03:07
-
-
Save tanbro/06eb6de43a432b7b0132b6b55b8a22fc to your computer and use it in GitHub Desktop.
Change dictionary's key naming style
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment