mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-07 15:18:29 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user