Source code for smarter.common.utils.conversion

"""
Smarter.common.utils.conversion
===============================

Case conversion utility functions for the Smarter framework.

This module provides functions to convert between different naming conventions,
such as camelCase, PascalCase, and snake_case, for strings, dictionary keys, and lists.
These utilities assure consistent treatment to/from various case formats.

Functions
---------
- to_snake_case(obj): Converts camelCase or PascalCase strings (or class/type objects) to snake_case.
- to_camel_case(data, convert_values=False): Converts snake_case strings, dict keys, or lists to camelCase.

Example
-------
.. code-block:: python

    from smarter.common.utils import to_snake_case, to_snake_case, to_camel_case

    print(to_snake_case("UserProfile"))  # Output: user_profile
    print(to_snake_case("userName"))     # Output: user_name
    print(to_camel_case("user_name"))    # Output: userName
"""

import re
from functools import lru_cache
from typing import Any, Union

from smarter.common.exceptions import SmarterValueError

LRU_MAXSIZE = 32  # Default max size for LRU caches in this module
SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")

ConvertibleCaseType = Union[str, dict[str, object], list[object], object]
"""
A type alias representing data that can be converted between different case.

formats. This includes strings, dictionaries with string keys, lists of such
elements, or any object.
"""


@lru_cache(maxsize=LRU_MAXSIZE)
def _convert_snake_to_camel(name: str) -> str:
    components = name.split("_")
    return components[0] + "".join(x.title() for x in components[1:])


# pylint: disable=W0613
[docs] def to_camel_case(data: ConvertibleCaseType, convert_values: bool = False, is_recursive: bool = False) -> Any: """ Convert snake_case strings, dictionary keys, or lists to camelCase format. Args: data (str | dict | list): The input to convert. Can be a string, a dictionary (with snake_case keys), or a list containing strings or dictionaries. convert_values (bool, optional): If True, string values within dictionaries and lists are also converted to camelCase. Default is False. Returns: Any: The converted data in camelCase format. The return type matches the input type (str, dict, or list). Notes: - For dictionaries, only keys are converted by default. If ``convert_values`` is True, string values are also converted. - Nested dictionaries and lists are processed recursively. - If the input is not a string, dictionary, or list, the original value is returned. Raises: SmarterValueError: If the input is not a string, dictionary, or list, and cannot be converted. Examples: >>> from smarter.common.utils import to_camel_case # Convert a string >>> to_camel_case("user_name") 'userName' # Convert a dictionary >>> data = { ... "user_name": "alice", ... "user_profile": { ... "first_name": "Alice", ... "last_name": "Smith" ... } ... } >>> to_camel_case(data) {'userName': 'alice', 'userProfile': {'firstName': 'Alice', 'lastName': 'Smith'}} # Convert a list of strings >>> to_camel_case(["first_name", "last_name"]) ['firstName', 'lastName'] # Convert values as well >>> data = {"user_name": "first_name"} >>> to_camel_case(data, convert_values=True) {'userName': 'firstName'} """ if isinstance(data, str): return _convert_snake_to_camel(data) elif isinstance(data, list): return [to_camel_case(item, convert_values=convert_values, is_recursive=True) for item in data] elif isinstance(data, dict): # For dictionaries, convert keys and optionally the values as well retval = {} for key, value in data.items(): key = _convert_snake_to_camel(key) if convert_values: value = to_camel_case(value, convert_values=convert_values, is_recursive=True) retval[key] = value return retval else: try: data_str = data.__name__ if hasattr(data, "__name__") else str(data) # type: ignore return to_camel_case(data_str, convert_values=convert_values) except Exception as e: raise SmarterValueError(f"Received an unsupported type: {type(data)}") from e
@lru_cache(maxsize=LRU_MAXSIZE) def _convert_camel_to_snake(name: str): name = name.replace(" ", "_").replace("-", "_") # Split acronym boundaries such as `LLMClient` -> `LLM_Client` before the general camelCase split. name = re.sub(r"(?<=[A-Z])(?=[A-Z][a-z])", "_", name) name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", name).lower() return re.sub(r"_+", "_", name)
[docs] def to_snake_case(data: ConvertibleCaseType, convert_values: bool = False) -> Any: """ Convert camelCase or PascalCase strings, dictionary keys, or lists to snake_case format. Args: data (str | dict | list): The input to convert. Can be a string, a dictionary (with camelCase or PascalCase keys), or a list containing strings or dictionaries. convert_values (bool, optional): If True, string values within dictionaries and lists are also converted to snake_case. Default is False. Returns: Any: The converted data in snake_case format. The return type matches the input type (str, dict, or list). Notes: - For dictionaries, only keys are converted by default. If ``convert_values`` is True, string values are also converted. - Spaces in keys are replaced with underscores. - Multiple consecutive underscores are collapsed into a single underscore. - Nested dictionaries and lists are processed recursively. - If the input is not a string, dictionary, or list, the function attempts to convert its string representation. Raises: SmarterValueError: If the input is not a string, dictionary, or list, and cannot be converted. Examples: >>> from smarter.common.utils import to_snake_case # Convert a string >>> to_snake_case("userName") 'user_name' # Convert a dictionary >>> data = { ... "userName": "alice", ... "userProfile": { ... "firstName": "Alice", ... "lastName": "Smith" ... } ... } >>> to_snake_case(data) {'user_name': 'alice', 'user_profile': {'first_name': 'Alice', 'last_name': 'Smith'}} # Convert a list of strings >>> to_snake_case(["firstName", "lastName"]) ['first_name', 'last_name'] """ if isinstance(data, str): return _convert_camel_to_snake(data) elif isinstance(data, list): return [ to_snake_case(item, convert_values=convert_values) if isinstance(item, (dict, list)) else item for item in data ] elif isinstance(data, dict): retval = {} for key, value in data.items(): key = _convert_camel_to_snake(key) if isinstance(value, dict) and convert_values: value = to_snake_case(data=value, convert_values=convert_values) elif isinstance(value, list) and convert_values: value = [to_snake_case(item, convert_values=convert_values) for item in value] retval[key] = value return retval else: try: data_str = data.__name__ if hasattr(data, "__name__") else str(data) # type: ignore return to_snake_case(data_str, convert_values=convert_values) except Exception as e: raise SmarterValueError(f"Received an unsupported type: {type(data)}") from e
__all__ = [ "to_snake_case", "to_camel_case", "ConvertibleCaseType", ]