diff --git a/lean/components/util/name_extraction.py b/lean/components/util/name_extraction.py index 4c058e86..67dd2c18 100644 --- a/lean/components/util/name_extraction.py +++ b/lean/components/util/name_extraction.py @@ -31,4 +31,12 @@ def convert_to_class_name(file_path: Path): :return: returns a valid class name """ from re import sub - return sub(f"[^a-zA-Z0-9]", "", "".join(map(_capitalize, file_path.name.split(" ")))) + from keyword import iskeyword + class_name = sub(f"[^a-zA-Z0-9]", "", "".join(map(_capitalize, file_path.name.split(" ")))) + + # An identifier cannot be empty, start with a digit or be a keyword. + # Keywords are lowercase in Python and C# except for None/True/False, so a lowercase start is enough to flag them + if class_name == "" or not class_name[0].isupper() or iskeyword(class_name): + class_name = "_" + class_name + + return class_name diff --git a/tests/commands/test_create_project.py b/tests/commands/test_create_project.py index bc27c93f..9d6ea5d5 100644 --- a/tests/commands/test_create_project.py +++ b/tests/commands/test_create_project.py @@ -157,6 +157,26 @@ def test_create_project_preserves_capitals_in_class_name() -> None: assert "class MyFIRSTProject(QCAlgorithm)" in file.read() +@pytest.mark.parametrize("name,class_name", [("2024 momentum", "_2024Momentum"), + ("123", "_123"), + ("none", "_None"), + ("true", "_True"), + ("_class", "_class"), + ("__import__", "_import")]) +def test_create_project_creates_valid_class_name_when_name_is_not_an_identifier(name: str, class_name: str) -> None: + create_fake_lean_cli_directory() + + result = CliRunner().invoke(lean, ["create-project", "--language", "python", name]) + + assert result.exit_code == 0 + + with open(Path.cwd() / name / "main.py") as file: + content = file.read() + + assert f"class {class_name}(QCAlgorithm)" in content + compile(content, "main.py", "exec") + + def test_create_project_aborts_when_path_invalid() -> None: create_fake_lean_cli_directory()