From dc25c32ca0f30b7e5c2b1d89bee17b4ecd163227 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:17:52 +0000 Subject: [PATCH] feat: add comprehensive class name validation for Python identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ensure generated class names are always valid Python identifiers - Handle edge cases: names starting with numbers, special characters, keywords, built-ins - Add sanitization logic to remove invalid characters and prefix with 'Crew' when needed - Add comprehensive test coverage for class name validation edge cases - Addresses GitHub PR comment from lucasgomide about class name validity Fixes include: - Names starting with numbers: '123project' -> 'Crew123Project' - Python built-ins: 'True' -> 'TrueCrew', 'False' -> 'FalseCrew' - Special characters: 'hello@world' -> 'HelloWorld' - Empty/whitespace: ' ' -> 'DefaultCrew' - All generated class names pass isidentifier() and keyword checks Co-Authored-By: João --- src/crewai/cli/create_crew.py | 20 +++++++++- tests/cli/test_create_crew.py | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/crewai/cli/create_crew.py b/src/crewai/cli/create_crew.py index 362c41517..e7e8e85a8 100644 --- a/src/crewai/cli/create_crew.py +++ b/src/crewai/cli/create_crew.py @@ -14,9 +14,27 @@ from crewai.cli.utils import copy_template, load_env_vars, write_env_file def create_folder_structure(name, parent_folder=None): + import keyword + import re + name = name.rstrip('/') folder_name = name.replace(" ", "_").replace("-", "_").lower() - class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "") + + if not name.strip(): + class_name = "DefaultCrew" + else: + class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "") + + class_name = re.sub(r'[^a-zA-Z0-9_]', '', class_name) + + if class_name and class_name[0].isdigit(): + class_name = "Crew" + class_name + + if not class_name: + class_name = "DefaultCrew" + + if keyword.iskeyword(class_name) or class_name in ('True', 'False', 'None'): + class_name = class_name + "Crew" if parent_folder: folder_path = Path(parent_folder) / folder_name diff --git a/tests/cli/test_create_crew.py b/tests/cli/test_create_crew.py index 027053801..1118b6579 100644 --- a/tests/cli/test_create_crew.py +++ b/tests/cli/test_create_crew.py @@ -149,6 +149,75 @@ def test_create_folder_structure_handles_spaces_and_dashes_with_slash(): assert folder_path.exists() +def test_create_folder_structure_handles_invalid_class_name_edge_cases(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + + folder_path, folder_name, class_name = create_folder_structure("123project/") + assert folder_name == "123project" + assert class_name == "Crew123Project" + assert class_name.isidentifier() + assert folder_path.exists() + + folder_path, folder_name, class_name = create_folder_structure("True/") + assert folder_name == "true" + assert class_name == "TrueCrew" + assert class_name.isidentifier() + assert folder_path.exists() + + folder_path, folder_name, class_name = create_folder_structure(" /") + assert folder_name == "___" # Spaces become underscores in folder_name + assert class_name == "DefaultCrew" # But class_name should be DefaultCrew for whitespace-only input + assert class_name.isidentifier() + assert folder_path.exists() + + folder_path, folder_name, class_name = create_folder_structure("hello@world/") + assert folder_name == "hello@world" + assert class_name == "HelloWorld" + assert class_name.isidentifier() + assert folder_path.exists() + + +def test_create_folder_structure_class_names_are_valid_python_identifiers(): + import keyword + + test_cases = [ + "hello/", + "my-project/", + "123project/", + "class/", + "def/", + "import/", + "True/", + "False/", + "None/", + "hello.world/", + "hello@world/", + "hello#world/", + "hello$world/", + "///", + "", + " /", + ] + + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + + for i, test_case in enumerate(test_cases): + unique_name = f"{test_case.rstrip('/')}_test_{i}/" + if not test_case.strip('/'): + unique_name = f"empty_test_{i}/" + + folder_path, folder_name, class_name = create_folder_structure(unique_name) + + assert class_name.isidentifier(), f"Class name '{class_name}' from input '{unique_name}' is not a valid identifier" + assert not keyword.iskeyword(class_name), f"Class name '{class_name}' from input '{unique_name}' is a Python keyword" + assert class_name not in ('True', 'False', 'None'), f"Class name '{class_name}' from input '{unique_name}' is a Python built-in" + + if folder_path.exists(): + shutil.rmtree(folder_path) + + @mock.patch("crewai.cli.create_crew.copy_template") @mock.patch("crewai.cli.create_crew.write_env_file") @mock.patch("crewai.cli.create_crew.load_env_vars")