# Copyright (c) 2022-2025, The Isaac Lab Project Developers.# All rights reserved.## SPDX-License-Identifier: BSD-3-Clause"""Sub-module containing utilities for transforming strings and regular expressions."""importastimportimportlibimportinspectimportrefromcollections.abcimportCallable,SequencefromtypingimportAny"""String formatting."""
[docs]defto_camel_case(snake_str:str,to:str="cC")->str:"""Converts a string from snake case to camel case. Args: snake_str: A string in snake case (i.e. with '_') to: Convention to convert string to. Defaults to "cC". Raises: ValueError: Invalid input argument `to`, i.e. not "cC" or "CC". Returns: A string in camel-case format. """# check input is correctiftonotin["cC","CC"]:msg="to_camel_case(): Choose a valid `to` argument (CC or cC)"raiseValueError(msg)# convert string to lower case and splitcomponents=snake_str.lower().split("_")ifto=="cC":# We capitalize the first letter of each component except the first one# with the 'title' method and join them together.returncomponents[0]+"".join(x.title()forxincomponents[1:])else:# Capitalize first letter in all the componentsreturn"".join(x.title()forxincomponents)
[docs]defto_snake_case(camel_str:str)->str:"""Converts a string from camel case to snake case. Args: camel_str: A string in camel case. Returns: A string in snake case (i.e. with '_') """camel_str=re.sub("(.)([A-Z][a-z]+)",r"\1_\2",camel_str)returnre.sub("([a-z0-9])([A-Z])",r"\1_\2",camel_str).lower()
[docs]defstring_to_slice(s:str):"""Convert a string representation of a slice to a slice object. Args: s: The string representation of the slice. Returns: The slice object. """# extract the content inside the slice()match=re.match(r"slice\((.*),(.*),(.*)\)",s)ifnotmatch:raiseValueError(f"Invalid slice string format: {s}")# extract start, stop, and step valuesstart_str,stop_str,step_str=match.groups()# convert 'None' to None and other strings to integersstart=Noneifstart_str=="None"elseint(start_str)stop=Noneifstop_str=="None"elseint(stop_str)step=Noneifstep_str=="None"elseint(step_str)# create and return the slice objectreturnslice(start,stop,step)
"""String <-> Callable operations."""
[docs]defis_lambda_expression(name:str)->bool:"""Checks if the input string is a lambda expression. Args: name: The input string. Returns: Whether the input string is a lambda expression. """try:ast.parse(name)returnisinstance(ast.parse(name).body[0],ast.Expr)andisinstance(ast.parse(name).body[0].value,ast.Lambda)exceptSyntaxError:returnFalse
[docs]defcallable_to_string(value:Callable)->str:"""Converts a callable object to a string. Args: value: A callable object. Raises: ValueError: When the input argument is not a callable object. Returns: A string representation of the callable object. """# check if callableifnotcallable(value):raiseValueError(f"The input argument is not callable: {value}.")# check if lambda functionifvalue.__name__=="<lambda>":# we resolve the lambda expression by checking the source code and extracting the line with lambda expression# we also remove any comments from the linelambda_line=inspect.getsourcelines(value)[0][0].strip().split("lambda")[1].strip().split(",")[0]lambda_line=re.sub(r"#.*$","",lambda_line).rstrip()returnf"lambda {lambda_line}"else:# get the module and function namemodule_name=value.__module__function_name=value.__name__# return the stringreturnf"{module_name}:{function_name}"
[docs]defstring_to_callable(name:str)->Callable:"""Resolves the module and function names to return the function. Args: name: The function name. The format should be 'module:attribute_name' or a lambda expression of format: 'lambda x: x'. Raises: ValueError: When the resolved attribute is not a function. ValueError: When the module cannot be found. Returns: Callable: The function loaded from the module. """try:ifis_lambda_expression(name):callable_object=eval(name)else:mod_name,attr_name=name.split(":")mod=importlib.import_module(mod_name)callable_object=getattr(mod,attr_name)# check if attribute is callableifcallable(callable_object):returncallable_objectelse:raiseAttributeError(f"The imported object is not callable: '{name}'")except(ValueError,ModuleNotFoundError)ase:msg=(f"Could not resolve the input string '{name}' into callable object."" The format of input should be 'module:attribute_name'.\n"f"Received the error:\n{e}.")raiseValueError(msg)
"""Regex operations."""
[docs]defresolve_matching_names(keys:str|Sequence[str],list_of_strings:Sequence[str],preserve_order:bool=False)->tuple[list[int],list[str]]:"""Match a list of query regular expressions against a list of strings and return the matched indices and names. When a list of query regular expressions is provided, the function checks each target string against each query regular expression and returns the indices of the matched strings and the matched strings. If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order of the provided list of strings. This means that the ordering is dictated by the order of the target strings and not the order of the query regular expressions. If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order of the provided list of query regular expressions. For example, consider the list of strings is ['a', 'b', 'c', 'd', 'e'] and the regular expressions are ['a|c', 'b']. If :attr:`preserve_order` is False, then the function will return the indices of the matched strings and the strings as: ([0, 1, 2], ['a', 'b', 'c']). When :attr:`preserve_order` is True, it will return them as: ([0, 2, 1], ['a', 'c', 'b']). Note: The function does not sort the indices. It returns the indices in the order they are found. Args: keys: A regular expression or a list of regular expressions to match the strings in the list. list_of_strings: A list of strings to match. preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False. Returns: A tuple of lists containing the matched indices and names. Raises: ValueError: When multiple matches are found for a string in the list. ValueError: When not all regular expressions are matched. """# resolve name keysifisinstance(keys,str):keys=[keys]# find matching patternsindex_list=[]names_list=[]key_idx_list=[]# book-keeping to check that we always have a one-to-one mapping# i.e. each target string should match only one regular expressiontarget_strings_match_found=[Nonefor_inrange(len(list_of_strings))]keys_match_found=[[]for_inrange(len(keys))]# loop over all target stringsfortarget_index,potential_match_stringinenumerate(list_of_strings):forkey_index,re_keyinenumerate(keys):ifre.fullmatch(re_key,potential_match_string):# check if match already foundiftarget_strings_match_found[target_index]:raiseValueError(f"Multiple matches for '{potential_match_string}':"f" '{target_strings_match_found[target_index]}' and '{re_key}'!")# add to listtarget_strings_match_found[target_index]=re_keyindex_list.append(target_index)names_list.append(potential_match_string)key_idx_list.append(key_index)# add for regex keykeys_match_found[key_index].append(potential_match_string)# reorder keys if they should be returned in order of the query keysifpreserve_order:reordered_index_list=[None]*len(index_list)global_index=0forkey_indexinrange(len(keys)):forkey_idx_position,key_idx_entryinenumerate(key_idx_list):ifkey_idx_entry==key_index:reordered_index_list[key_idx_position]=global_indexglobal_index+=1# reorder index and names listindex_list_reorder=[None]*len(index_list)names_list_reorder=[None]*len(index_list)foridx,reorder_idxinenumerate(reordered_index_list):index_list_reorder[reorder_idx]=index_list[idx]names_list_reorder[reorder_idx]=names_list[idx]# updateindex_list=index_list_reordernames_list=names_list_reorder# check that all regular expressions are matchedifnotall(keys_match_found):# make this print nicely aligned for debuggingmsg="\n"forkey,valueinzip(keys,keys_match_found):msg+=f"\t{key}: {value}\n"msg+=f"Available strings: {list_of_strings}\n"# raise errorraiseValueError(f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}")# returnreturnindex_list,names_list
[docs]defresolve_matching_names_values(data:dict[str,Any],list_of_strings:Sequence[str],preserve_order:bool=False)->tuple[list[int],list[str],list[Any]]:"""Match a list of regular expressions in a dictionary against a list of strings and return the matched indices, names, and values. If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order of the provided list of strings. This means that the ordering is dictated by the order of the target strings and not the order of the query regular expressions. If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order of the provided list of query regular expressions. For example, consider the dictionary is {"a|d|e": 1, "b|c": 2}, the list of strings is ['a', 'b', 'c', 'd', 'e']. If :attr:`preserve_order` is False, then the function will return the indices of the matched strings, the matched strings, and the values as: ([0, 1, 2, 3, 4], ['a', 'b', 'c', 'd', 'e'], [1, 2, 2, 1, 1]). When :attr:`preserve_order` is True, it will return them as: ([0, 3, 4, 1, 2], ['a', 'd', 'e', 'b', 'c'], [1, 1, 1, 2, 2]). Args: data: A dictionary of regular expressions and values to match the strings in the list. list_of_strings: A list of strings to match. preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False. Returns: A tuple of lists containing the matched indices, names, and values. Raises: TypeError: When the input argument :attr:`data` is not a dictionary. ValueError: When multiple matches are found for a string in the dictionary. ValueError: When not all regular expressions in the data keys are matched. """# check valid inputifnotisinstance(data,dict):raiseTypeError(f"Input argument `data` should be a dictionary. Received: {data}")# find matching patternsindex_list=[]names_list=[]values_list=[]key_idx_list=[]# book-keeping to check that we always have a one-to-one mapping# i.e. each target string should match only one regular expressiontarget_strings_match_found=[Nonefor_inrange(len(list_of_strings))]keys_match_found=[[]for_inrange(len(data))]# loop over all target stringsfortarget_index,potential_match_stringinenumerate(list_of_strings):forkey_index,(re_key,value)inenumerate(data.items()):ifre.fullmatch(re_key,potential_match_string):# check if match already foundiftarget_strings_match_found[target_index]:raiseValueError(f"Multiple matches for '{potential_match_string}':"f" '{target_strings_match_found[target_index]}' and '{re_key}'!")# add to listtarget_strings_match_found[target_index]=re_keyindex_list.append(target_index)names_list.append(potential_match_string)values_list.append(value)key_idx_list.append(key_index)# add for regex keykeys_match_found[key_index].append(potential_match_string)# reorder keys if they should be returned in order of the query keysifpreserve_order:reordered_index_list=[None]*len(index_list)global_index=0forkey_indexinrange(len(data)):forkey_idx_position,key_idx_entryinenumerate(key_idx_list):ifkey_idx_entry==key_index:reordered_index_list[key_idx_position]=global_indexglobal_index+=1# reorder index and names listindex_list_reorder=[None]*len(index_list)names_list_reorder=[None]*len(index_list)values_list_reorder=[None]*len(index_list)foridx,reorder_idxinenumerate(reordered_index_list):index_list_reorder[reorder_idx]=index_list[idx]names_list_reorder[reorder_idx]=names_list[idx]values_list_reorder[reorder_idx]=values_list[idx]# updateindex_list=index_list_reordernames_list=names_list_reordervalues_list=values_list_reorder# check that all regular expressions are matchedifnotall(keys_match_found):# make this print nicely aligned for debuggingmsg="\n"forkey,valueinzip(data.keys(),keys_match_found):msg+=f"\t{key}: {value}\n"msg+=f"Available strings: {list_of_strings}\n"# raise errorraiseValueError(f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}")# returnreturnindex_list,names_list,values_list