feat: add comprehensive class name validation for Python identifiers

- 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 <joao@crewai.com>
This commit is contained in:
Devin AI
2025-06-24 19:17:52 +00:00
parent bc4fd6a39b
commit dc25c32ca0
2 changed files with 88 additions and 1 deletions

View File

@@ -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

View File

@@ -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")