diff --git a/.circleci/config.yml b/.circleci/config.yml index 480f0e3400..acf8612eac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -992,6 +992,7 @@ jobs: - run: ruff check ./litellm # - run: python ./tests/documentation_tests/test_general_setting_keys.py - run: python ./tests/code_coverage_tests/router_code_coverage.py + - run: python ./tests/code_coverage_tests/recursive_detector.py - run: python ./tests/code_coverage_tests/test_router_strategy_async.py - run: python ./tests/code_coverage_tests/litellm_logging_code_coverage.py - run: python ./tests/documentation_tests/test_env_keys.py diff --git a/tests/code_coverage_tests/recursive_detector.py b/tests/code_coverage_tests/recursive_detector.py new file mode 100644 index 0000000000..1af0d2708f --- /dev/null +++ b/tests/code_coverage_tests/recursive_detector.py @@ -0,0 +1,89 @@ +import ast +import os + +IGNORE_FUNCTIONS = [ + "_format_type", + "_remove_additional_properties", + "_remove_strict_from_schema", + "text_completion", + "_check_for_os_environ_vars", + "clean_message", + "_prepare_metadata", + "unpack_defs", + "convert_to_nullable", + "add_object_type", + "strip_field", + "_transform_prompt", +] + + +class RecursiveFunctionFinder(ast.NodeVisitor): + def __init__(self): + self.recursive_functions = [] + self.ignored_recursive_functions = [] + + def visit_FunctionDef(self, node): + # Check if the function calls itself + if any(self._is_recursive_call(node, call) for call in ast.walk(node)): + if node.name in IGNORE_FUNCTIONS: + self.ignored_recursive_functions.append(node.name) + else: + self.recursive_functions.append(node.name) + self.generic_visit(node) + + def _is_recursive_call(self, func_node, call_node): + # Check if the call node is a function call + if not isinstance(call_node, ast.Call): + return False + + # Case 1: Direct function call (e.g., my_func()) + if isinstance(call_node.func, ast.Name) and call_node.func.id == func_node.name: + return True + + # Case 2: Method call with self (e.g., self.my_func()) + if isinstance(call_node.func, ast.Attribute) and isinstance( + call_node.func.value, ast.Name + ): + return ( + call_node.func.value.id == "self" + and call_node.func.attr == func_node.name + ) + + return False + + +def find_recursive_functions_in_file(file_path): + with open(file_path, "r") as file: + tree = ast.parse(file.read(), filename=file_path) + finder = RecursiveFunctionFinder() + finder.visit(tree) + return finder.recursive_functions, finder.ignored_recursive_functions + + +def find_recursive_functions_in_directory(directory): + recursive_functions = {} + ignored_recursive_functions = {} + for root, _, files in os.walk(directory): + for file in files: + print("file: ", file) + if file.endswith(".py"): + file_path = os.path.join(root, file) + functions, ignored = find_recursive_functions_in_file(file_path) + if functions: + recursive_functions[file_path] = functions + if ignored: + ignored_recursive_functions[file_path] = ignored + return recursive_functions, ignored_recursive_functions + + +# Example usage +directory_path = "./litellm" +recursive_functions, ignored_recursive_functions = ( + find_recursive_functions_in_directory(directory_path) +) +print("ALL RECURSIVE FUNCTIONS: ", recursive_functions) +print("IGNORED RECURSIVE FUNCTIONS: ", ignored_recursive_functions) +if len(recursive_functions) > 0: + raise Exception( + f"🚨 Recursive functions found in {file}: {functions}. THIS IS REALLY BAD, it has caused CPU Usage spikes in the past. Only keep this if it's ABSOLUTELY necessary." + )