Skip to content

Commit 62fd795

Browse files
fix: add minimal pyproject metadata during init
1 parent 793b42f commit 62fd795

5 files changed

Lines changed: 123 additions & 28 deletions

File tree

codeflash/cli_cmds/init_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from codeflash.cli_cmds.console import apologize_and_exit, console
1616
from codeflash.code_utils.compat import LF
17+
from codeflash.code_utils.pyproject_utils import ensure_minimal_project_metadata
1718
from codeflash.code_utils.config_parser import parse_config_file
1819
from codeflash.code_utils.env_utils import check_formatter_installed
1920
from codeflash.lsp.helpers import is_LSP_enabled
@@ -202,6 +203,7 @@ def configure_pyproject_toml(
202203
f"Please create a new empty pyproject.toml file here, OR if you use poetry then run `poetry init`, OR run `codeflash init` again from a directory with an existing pyproject.toml file."
203204
)
204205
return False
206+
ensure_minimal_project_metadata(pyproject_data, toml_path.parent)
205207

206208
codeflash_section = tomlkit.table()
207209
codeflash_section.add(tomlkit.comment("All paths are relative to this pyproject.toml's directory."))
@@ -252,6 +254,7 @@ def create_empty_pyproject_toml(pyproject_toml_path: Path) -> None:
252254
lsp_mode = is_LSP_enabled()
253255
# Define a minimal pyproject.toml content
254256
new_pyproject_toml = tomlkit.document()
257+
ensure_minimal_project_metadata(new_pyproject_toml, pyproject_toml_path.parent)
255258
new_pyproject_toml["tool"] = {"codeflash": {}}
256259
try:
257260
pyproject_toml_path.write_text(tomlkit.dumps(new_pyproject_toml), encoding="utf8")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
from typing import Any
6+
7+
import tomlkit
8+
9+
DEFAULT_MINIMAL_PROJECT_VERSION = "0.0.0"
10+
11+
12+
def infer_minimal_project_name(project_root: Path) -> str:
13+
"""Generate a PEP 621-friendly project name from the directory name."""
14+
candidate = project_root.resolve().name.strip().lower()
15+
if not candidate:
16+
return "codeflash-project"
17+
18+
normalized = re.sub(r"[^a-z0-9._-]+", "-", candidate)
19+
normalized = re.sub(r"[-_.]{2,}", "-", normalized).strip("-_.")
20+
return normalized or "codeflash-project"
21+
22+
23+
def ensure_minimal_project_metadata(pyproject_data: Any, project_root: Path) -> bool:
24+
"""Add minimal [project] metadata when the file has no standard project section.
25+
26+
Codeflash can create a pyproject.toml solely for tool configuration. Some tooling,
27+
including uv in certain flows, expects PEP 621 metadata once a pyproject.toml exists.
28+
We only inject minimal metadata when neither [project] nor [tool.poetry] exists.
29+
"""
30+
31+
if "project" in pyproject_data:
32+
return False
33+
34+
tool_section = pyproject_data.get("tool", {})
35+
if isinstance(tool_section, dict) and "poetry" in tool_section:
36+
return False
37+
38+
project_section = tomlkit.table()
39+
project_section["name"] = infer_minimal_project_name(project_root)
40+
project_section["version"] = DEFAULT_MINIMAL_PROJECT_VERSION
41+
pyproject_data["project"] = project_section
42+
return True

codeflash/setup/config_writer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import TYPE_CHECKING
1212

1313
import tomlkit
14+
from codeflash.code_utils.pyproject_utils import ensure_minimal_project_metadata
1415

1516
if TYPE_CHECKING:
1617
from pathlib import Path
@@ -66,6 +67,8 @@ def _write_pyproject_toml(project_root: Path, config: CodeflashConfig) -> tuple[
6667
else:
6768
doc = tomlkit.document()
6869

70+
ensure_minimal_project_metadata(doc, project_root)
71+
6972
# Ensure [tool] section exists
7073
if "tool" not in doc:
7174
doc["tool"] = tomlkit.table()

tests/test_cmd_init.py

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
from pathlib import Path
44

55
import pytest
6+
import tomlkit
67

78
from codeflash.cli_cmds.init_config import (
89
CLISetupInfo,
910
VsCodeSetupInfo,
1011
configure_pyproject_toml,
12+
create_empty_pyproject_toml,
1113
get_formatter_cmds,
1214
get_valid_subdirs,
1315
is_valid_pyproject_toml,
1416
)
17+
from codeflash.code_utils.pyproject_utils import infer_minimal_project_name
1518

1619

1720
@pytest.fixture
@@ -103,17 +106,14 @@ def test_configure_pyproject_toml_for_cli(temp_dir: Path) -> None:
103106
assert success
104107

105108
config_content = pyproject_path.read_text()
106-
assert (
107-
config_content
108-
== """[tool.codeflash]
109-
# All paths are relative to this pyproject.toml's directory.
110-
module-root = "."
111-
tests-root = "tests"
112-
ignore-paths = []
113-
disable-telemetry = true
114-
formatter-cmds = ["black $file"]
115-
"""
116-
)
109+
data = tomlkit.parse(config_content)
110+
assert data["project"]["name"] == infer_minimal_project_name(temp_dir)
111+
assert data["project"]["version"] == "0.0.0"
112+
assert data["tool"]["codeflash"]["module-root"] == "."
113+
assert data["tool"]["codeflash"]["tests-root"] == "tests"
114+
assert data["tool"]["codeflash"]["ignore-paths"] == []
115+
assert data["tool"]["codeflash"]["disable-telemetry"] is True
116+
assert data["tool"]["codeflash"]["formatter-cmds"] == ["black $file"]
117117
valid, _, _ = is_valid_pyproject_toml(pyproject_path)
118118
assert valid
119119

@@ -131,14 +131,12 @@ def test_configure_pyproject_toml_for_vscode_with_empty_config(temp_dir: Path) -
131131
assert success
132132

133133
config_content = pyproject_path.read_text()
134-
assert (
135-
config_content
136-
== """[tool.codeflash]
137-
module-root = "."
138-
tests-root = "tests"
139-
formatter-cmds = ["black $file"]
140-
"""
141-
)
134+
data = tomlkit.parse(config_content)
135+
assert data["project"]["name"] == infer_minimal_project_name(temp_dir)
136+
assert data["project"]["version"] == "0.0.0"
137+
assert data["tool"]["codeflash"]["module-root"] == "."
138+
assert data["tool"]["codeflash"]["tests-root"] == "tests"
139+
assert data["tool"]["codeflash"]["formatter-cmds"] == ["black $file"]
142140
valid, _, _ = is_valid_pyproject_toml(pyproject_path)
143141
assert valid
144142

@@ -162,15 +160,13 @@ def test_configure_pyproject_toml_for_vscode_with_existing_config(temp_dir: Path
162160

163161
config_content = pyproject_path.read_text()
164162
# the benchmarks-root shouldn't get overwritten
165-
assert (
166-
config_content
167-
== """[tool.codeflash]
168-
module-root = "."
169-
tests-root = "tests"
170-
benchmarks-root = "tests/benchmarks"
171-
formatter-cmds = ["disabled"]
172-
"""
173-
)
163+
data = tomlkit.parse(config_content)
164+
assert data["project"]["name"] == infer_minimal_project_name(temp_dir)
165+
assert data["project"]["version"] == "0.0.0"
166+
assert data["tool"]["codeflash"]["module-root"] == "."
167+
assert data["tool"]["codeflash"]["tests-root"] == "tests"
168+
assert data["tool"]["codeflash"]["benchmarks-root"] == "tests/benchmarks"
169+
assert data["tool"]["codeflash"]["formatter-cmds"] == ["disabled"]
174170
valid, _, _ = is_valid_pyproject_toml(pyproject_path)
175171
assert valid
176172

@@ -186,3 +182,18 @@ def test_get_valid_subdirs(temp_dir: Path) -> None:
186182
assert "tests" in dirs
187183
assert "dir1" in dirs
188184
assert "dir2" in dirs
185+
186+
187+
def test_create_empty_pyproject_toml_adds_minimal_project_metadata(
188+
temp_dir: Path, monkeypatch: pytest.MonkeyPatch
189+
) -> None:
190+
pyproject_path = temp_dir / "pyproject.toml"
191+
monkeypatch.setattr("codeflash.cli_cmds.init_config.is_LSP_enabled", lambda: True)
192+
monkeypatch.setattr("codeflash.cli_cmds.init_config.ph", lambda *args, **kwargs: None)
193+
194+
create_empty_pyproject_toml(pyproject_path)
195+
196+
data = tomlkit.parse(pyproject_path.read_text())
197+
assert data["project"]["name"] == infer_minimal_project_name(temp_dir)
198+
assert data["project"]["version"] == "0.0.0"
199+
assert data["tool"]["codeflash"] == {}

tests/test_setup/test_config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import tomlkit
66

7+
from codeflash.code_utils.pyproject_utils import infer_minimal_project_name
78
from codeflash.setup.config_schema import CodeflashConfig
89
from codeflash.setup.config_writer import (
910
_write_package_json,
@@ -170,10 +171,29 @@ def test_creates_new_pyproject(self, tmp_path):
170171
# Verify content
171172
content = (tmp_path / "pyproject.toml").read_text()
172173
data = tomlkit.parse(content)
174+
assert data["project"]["name"] == infer_minimal_project_name(tmp_path)
175+
assert data["project"]["version"] == "0.0.0"
173176
assert "tool" in data
174177
assert "codeflash" in data["tool"]
175178
assert data["tool"]["codeflash"]["module-root"] == "src"
176179

180+
def test_adds_minimal_project_metadata_when_missing(self, tmp_path):
181+
"""Should add [project] metadata when pyproject.toml only has tool config."""
182+
(tmp_path / "pyproject.toml").write_text("[tool.ruff]\nline-length = 120")
183+
184+
config = CodeflashConfig(language="python", module_root="src")
185+
186+
success, message = _write_pyproject_toml(tmp_path, config)
187+
188+
assert success is True
189+
190+
content = (tmp_path / "pyproject.toml").read_text()
191+
data = tomlkit.parse(content)
192+
assert data["project"]["name"] == infer_minimal_project_name(tmp_path)
193+
assert data["project"]["version"] == "0.0.0"
194+
assert data["tool"]["ruff"]["line-length"] == 120
195+
assert data["tool"]["codeflash"]["module-root"] == "src"
196+
177197
def test_preserves_existing_content(self, tmp_path):
178198
"""Should preserve existing pyproject.toml content."""
179199
(tmp_path / "pyproject.toml").write_text(
@@ -208,6 +228,22 @@ def test_updates_existing_codeflash_section(self, tmp_path):
208228
assert data["tool"]["codeflash"]["module-root"] == "new"
209229
assert data["tool"]["codeflash"]["tests-root"] == "new_tests"
210230

231+
def test_preserves_poetry_metadata_without_adding_project(self, tmp_path):
232+
"""Should not inject [project] into poetry-managed pyproject.toml files."""
233+
(tmp_path / "pyproject.toml").write_text('[tool.poetry]\nname = "myapp"\nversion = "1.0.0"')
234+
235+
config = CodeflashConfig(language="python", module_root="src")
236+
237+
success, message = _write_pyproject_toml(tmp_path, config)
238+
239+
assert success is True
240+
241+
content = (tmp_path / "pyproject.toml").read_text()
242+
data = tomlkit.parse(content)
243+
assert "project" not in data
244+
assert data["tool"]["poetry"]["name"] == "myapp"
245+
assert data["tool"]["codeflash"]["module-root"] == "src"
246+
211247
def test_preserves_crlf_newlines_for_existing_pyproject(self, tmp_path):
212248
"""Should not introduce doubled carriage returns when preserving CRLF files."""
213249
pyproject_path = tmp_path / "pyproject.toml"

0 commit comments

Comments
 (0)