feat: add folder name validation for Python module names

- Implement validation to ensure folder_name is valid Python identifier
- Check that folder names don't start with digits
- Validate folder names are not Python keywords
- Sanitize invalid characters from folder names
- Raise ValueError with descriptive messages for invalid cases
- Update tests to validate both folder and class name requirements
- Addresses GitHub comment requiring folder names to be valid Python module names

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-06-26 12:59:49 +00:00
parent a3416f4301
commit 5b5b462238
2 changed files with 59 additions and 7 deletions

View File

@@ -18,11 +18,25 @@ def create_folder_structure(name, parent_folder=None):
import re
name = name.rstrip('/')
folder_name = name.replace(" ", "_").replace("-", "_").lower()
if not name.strip():
raise ValueError("Project name cannot be empty or contain only whitespace")
folder_name = name.replace(" ", "_").replace("-", "_").lower()
folder_name = re.sub(r'[^a-zA-Z0-9_]', '', folder_name)
if not folder_name:
raise ValueError(f"Project name '{name}' contains no valid characters for a Python module name")
if folder_name[0].isdigit():
raise ValueError(f"Project name '{name}' would generate folder name '{folder_name}' which cannot start with a digit (invalid Python module name)")
if keyword.iskeyword(folder_name):
raise ValueError(f"Project name '{name}' would generate folder name '{folder_name}' which is a reserved Python keyword")
if not folder_name.isidentifier():
raise ValueError(f"Project name '{name}' would generate invalid Python module name '{folder_name}'")
class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "")
class_name = re.sub(r'[^a-zA-Z0-9_]', '', class_name)

View File

@@ -149,7 +149,7 @@ def test_create_folder_structure_handles_spaces_and_dashes_with_slash():
assert folder_path.parent == Path(temp_dir)
def test_create_folder_structure_raises_error_for_invalid_class_names():
def test_create_folder_structure_raises_error_for_invalid_names():
with tempfile.TemporaryDirectory() as temp_dir:
invalid_cases = [
("123project/", "cannot start with a digit"),
@@ -168,7 +168,7 @@ def test_create_folder_structure_raises_error_for_invalid_class_names():
create_folder_structure(invalid_name, parent_folder=temp_dir)
def test_create_folder_structure_validates_class_names():
def test_create_folder_structure_validates_names():
import keyword
with tempfile.TemporaryDirectory() as temp_dir:
@@ -177,16 +177,21 @@ def test_create_folder_structure_validates_class_names():
("my-project/", "my_project", "MyProject"),
("hello_world/", "hello_world", "HelloWorld"),
("valid123/", "valid123", "Valid123"),
("hello.world/", "hello.world", "HelloWorld"),
("hello@world/", "hello@world", "HelloWorld"),
("hello.world/", "helloworld", "HelloWorld"),
("hello@world/", "helloworld", "HelloWorld"),
]
for valid_name, expected_folder, expected_class in valid_cases:
folder_path, folder_name, class_name = create_folder_structure(valid_name, parent_folder=temp_dir)
assert folder_name == expected_folder
assert class_name == expected_class
assert class_name.isidentifier()
assert not keyword.iskeyword(class_name)
assert folder_name.isidentifier(), f"folder_name '{folder_name}' should be valid Python identifier"
assert not keyword.iskeyword(folder_name), f"folder_name '{folder_name}' should not be Python keyword"
assert not folder_name[0].isdigit(), f"folder_name '{folder_name}' should not start with digit"
assert class_name.isidentifier(), f"class_name '{class_name}' should be valid Python identifier"
assert not keyword.iskeyword(class_name), f"class_name '{class_name}' should not be Python keyword"
assert folder_path.parent == Path(temp_dir)
if folder_path.exists():
@@ -202,6 +207,39 @@ def test_create_crew_with_parent_folder_and_trailing_slash(mock_load_env, mock_w
with tempfile.TemporaryDirectory() as work_dir:
parent_path = Path(work_dir) / "parent"
parent_path.mkdir()
def test_create_folder_structure_folder_name_validation():
"""Test that folder names are validated as valid Python module names"""
with tempfile.TemporaryDirectory() as temp_dir:
folder_invalid_cases = [
("123invalid/", "cannot start with a digit.*invalid Python module name"),
("import/", "reserved Python keyword"),
("class/", "reserved Python keyword"),
("for/", "reserved Python keyword"),
("@#$invalid/", "contains no valid characters.*Python module name"),
]
for invalid_name, expected_error in folder_invalid_cases:
with pytest.raises(ValueError, match=expected_error):
create_folder_structure(invalid_name, parent_folder=temp_dir)
valid_cases = [
("hello-world/", "hello_world"),
("my.project/", "myproject"),
("test@123/", "test123"),
("valid_name/", "valid_name"),
]
for valid_name, expected_folder in valid_cases:
folder_path, folder_name, class_name = create_folder_structure(valid_name, parent_folder=temp_dir)
assert folder_name == expected_folder
assert folder_name.isidentifier()
assert not keyword.iskeyword(folder_name)
if folder_path.exists():
shutil.rmtree(folder_path)
create_crew("child-crew/", skip_provider=True, parent_folder=parent_path)