Skip to content

Commit 73e2ca0

Browse files
authored
fix: introduce example pyproject for enhance (#287)
1 parent 03b24e6 commit 73e2ca0

File tree

3 files changed

+176
-43
lines changed

3 files changed

+176
-43
lines changed

docs/remote-templates/creating-remote-templates.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ When creating a remote template, you need to consider:
99
### Dependencies (`pyproject.toml`)
1010
**Required for custom dependencies:** If your agent has custom Python dependencies, you **must** include a `pyproject.toml` file at the template root with those dependencies listed.
1111

12+
For guidance on the structure and content of your `pyproject.toml`, you can reference the [base template pyproject.toml](https://github.com/GoogleCloudPlatform/agent-starter-pack/blob/main/src/base_template/pyproject.toml) as an example of the expected format and common dependencies.
13+
1214
### Configuration (`[tool.agent-starter-pack]`)
1315
**Optional but recommended:** Add this section to your `pyproject.toml` for:
1416
- **Discoverability:** Templates with this section appear in `uvx agent-starter-pack list` commands

src/cli/utils/template.py

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -705,89 +705,93 @@ def process_template(
705705
for item in generated_project_dir.iterdir():
706706
dest_item = final_destination / item.name
707707

708-
# Special handling for README files - use base template README if conflict exists
708+
# Special handling for README files - always preserve existing README
709+
# Special handling for pyproject.toml files - only preserve for in-folder updates
710+
should_preserve_file = item.name.lower().startswith(
711+
"readme"
712+
) or (item.name == "pyproject.toml" and in_folder)
709713
if (
710-
item.name.lower().startswith("readme")
714+
should_preserve_file
711715
and (final_destination / item.name).exists()
712716
):
713-
# The existing README stays, use base template README with starter_pack prefix
717+
# The existing file stays, use base template file with starter_pack prefix
714718
base_name = item.stem
715719
extension = item.suffix
716720
dest_item = (
717721
final_destination
718722
/ f"starter_pack_{base_name}{extension}"
719723
)
720724

721-
# Try to use base template README instead of templated README
722-
base_readme = base_template_path / item.name
723-
if base_readme.exists():
725+
# Try to use base template file instead of templated file
726+
base_file = base_template_path / item.name
727+
if base_file.exists():
724728
logging.debug(
725-
f"README conflict: preserving existing {item.name}, using base template README as starter_pack_{base_name}{extension}"
729+
f"{item.name} conflict: preserving existing {item.name}, using base template {item.name} as starter_pack_{base_name}{extension}"
726730
)
727-
# Process the base template README through cookiecutter
731+
# Process the base template file through cookiecutter
728732
try:
729733
import tempfile as tmp_module
730734

731735
with (
732-
tmp_module.TemporaryDirectory() as temp_readme_dir
736+
tmp_module.TemporaryDirectory() as temp_file_dir
733737
):
734-
temp_readme_path = pathlib.Path(temp_readme_dir)
738+
temp_file_path = pathlib.Path(temp_file_dir)
735739

736-
# Create a minimal cookiecutter structure for just the README
737-
readme_template_dir = (
738-
temp_readme_path / "readme_template"
740+
# Create a minimal cookiecutter structure for just the file
741+
file_template_dir = (
742+
temp_file_path / "file_template"
739743
)
740-
readme_template_dir.mkdir()
741-
readme_project_dir = (
742-
readme_template_dir
744+
file_template_dir.mkdir()
745+
file_project_dir = (
746+
file_template_dir
743747
/ "{{cookiecutter.project_name}}"
744748
)
745-
readme_project_dir.mkdir()
749+
file_project_dir.mkdir()
746750

747-
# Copy base README to template structure
751+
# Copy base file to template structure
748752
shutil.copy2(
749-
base_readme, readme_project_dir / item.name
753+
base_file, file_project_dir / item.name
750754
)
751755

752756
# Create cookiecutter.json with same config as main template
753757
with open(
754-
readme_template_dir / "cookiecutter.json",
758+
file_template_dir / "cookiecutter.json",
755759
"w",
756760
encoding="utf-8",
757761
) as f:
758762
json.dump(cookiecutter_config, f, indent=4)
759763

760-
# Process the README template
764+
# Process the file template
761765
cookiecutter(
762-
str(readme_template_dir),
766+
str(file_template_dir),
763767
no_input=True,
764768
overwrite_if_exists=True,
765-
output_dir=str(temp_readme_path),
769+
output_dir=str(temp_file_path),
766770
extra_context={
767771
"project_name": project_name,
768772
"agent_name": agent_name,
769773
},
770774
)
771775

772-
# Copy the processed README
773-
processed_readme = (
774-
temp_readme_path / project_name / item.name
776+
# Copy the processed file
777+
processed_file = (
778+
temp_file_path / project_name / item.name
775779
)
776-
if processed_readme.exists():
777-
shutil.copy2(processed_readme, dest_item)
780+
if processed_file.exists():
781+
shutil.copy2(processed_file, dest_item)
778782
else:
779783
# Fallback to original behavior if processing fails
780784
shutil.copy2(item, dest_item)
781785

782786
except Exception as e:
783787
logging.warning(
784-
f"Failed to process base template README: {e}. Using templated README instead."
788+
f"Failed to process base template {item.name}: {e}. Using templated {item.name} instead."
785789
)
786790
shutil.copy2(item, dest_item)
787791
else:
788-
# Fallback to original behavior if base README doesn't exist
792+
# Fallback to original behavior if base file doesn't exist
789793
logging.debug(
790-
f"README conflict: preserving existing {item.name}, saving templated README as starter_pack_{base_name}{extension}"
794+
f"{item.name} conflict: preserving existing {item.name}, saving templated {item.name} as starter_pack_{base_name}{extension}"
791795
)
792796
shutil.copy2(item, dest_item)
793797
else:

tests/cli/commands/test_create_local.py

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,107 @@ def test_parse_agent_spec_ignores_local_prefix() -> None:
181181
assert spec is None
182182

183183

184-
def test_readme_conflict_handling_logic(tmp_path: pathlib.Path) -> None:
185-
"""Test the basic README conflict detection logic (simplified version).
184+
def test_readme_and_pyproject_conflict_handling_in_folder_mode(
185+
tmp_path: pathlib.Path,
186+
) -> None:
187+
"""Test conflict handling for in-folder updates - both README and pyproject.toml should be preserved."""
188+
import shutil
189+
190+
# Set up directories
191+
final_destination = tmp_path / "destination"
192+
final_destination.mkdir(parents=True)
193+
generated_project_dir = tmp_path / "generated"
194+
generated_project_dir.mkdir(parents=True)
195+
196+
# Create existing README in destination
197+
existing_readme_content = (
198+
"# Existing Project\n\nThis is my existing README content."
199+
)
200+
(final_destination / "README.md").write_text(existing_readme_content)
201+
202+
# Create existing pyproject.toml in destination
203+
existing_pyproject_content = """[build-system]
204+
requires = ["setuptools>=45", "wheel"]
205+
build-backend = "setuptools.build_meta"
206+
207+
[project]
208+
name = "existing-project"
209+
version = "0.1.0"
210+
"""
211+
(final_destination / "pyproject.toml").write_text(existing_pyproject_content)
212+
213+
# Create templated README in generated project
214+
templated_readme_content = "# Test Project\n\nThis is the templated README content."
215+
(generated_project_dir / "README.md").write_text(templated_readme_content)
216+
217+
# Create templated pyproject.toml in generated project
218+
templated_pyproject_content = """[build-system]
219+
requires = ["poetry-core"]
220+
build-backend = "poetry.core.masonry.api"
221+
222+
[tool.poetry]
223+
name = "templated-project"
224+
version = "0.1.0"
225+
"""
226+
(generated_project_dir / "pyproject.toml").write_text(templated_pyproject_content)
227+
228+
# Also create a non-conflicting file
229+
(generated_project_dir / "other_file.py").write_text("# Other file content")
230+
231+
# Simulate the in-folder copying logic from process_template (in_folder=True)
232+
in_folder = True
233+
for item in generated_project_dir.iterdir():
234+
dest_item = final_destination / item.name
235+
236+
# Use the same logic as the updated process_template function
237+
should_preserve_file = item.name.lower().startswith("readme") or (
238+
item.name == "pyproject.toml" and in_folder
239+
)
240+
if should_preserve_file and (final_destination / item.name).exists():
241+
# The existing file stays, save the templated one with a different name
242+
base_name = item.stem
243+
extension = item.suffix
244+
dest_item = final_destination / f"starter_pack_{base_name}{extension}"
245+
246+
if item.is_dir():
247+
if dest_item.exists():
248+
shutil.rmtree(dest_item)
249+
shutil.copytree(item, dest_item, dirs_exist_ok=True)
250+
else:
251+
shutil.copy2(item, dest_item)
252+
253+
# Verify results
254+
original_readme = final_destination / "README.md"
255+
templated_readme = final_destination / "starter_pack_README.md"
256+
original_pyproject = final_destination / "pyproject.toml"
257+
templated_pyproject = final_destination / "starter_pack_pyproject.toml"
258+
other_file = final_destination / "other_file.py"
259+
260+
# Original README should be preserved with original content
261+
assert original_readme.exists()
262+
assert original_readme.read_text() == existing_readme_content
263+
264+
# Templated README should be saved with new name
265+
assert templated_readme.exists()
266+
assert templated_readme.read_text() == templated_readme_content
267+
268+
# Original pyproject.toml should be preserved with original content (in-folder mode)
269+
assert original_pyproject.exists()
270+
assert original_pyproject.read_text() == existing_pyproject_content
271+
272+
# Templated pyproject.toml should be saved with new name (in-folder mode)
273+
assert templated_pyproject.exists()
274+
assert templated_pyproject.read_text() == templated_pyproject_content
186275

187-
Note: This tests the conflict detection pattern, but the actual process_template
188-
function now uses base template README instead of templated README when conflicts occur.
189-
"""
276+
# Other files should copy normally
277+
assert other_file.exists()
278+
assert other_file.read_text() == "# Other file content"
279+
280+
281+
def test_readme_and_pyproject_conflict_handling_remote_template_mode(
282+
tmp_path: pathlib.Path,
283+
) -> None:
284+
"""Test conflict handling for remote templates - README preserved, pyproject.toml should be overwritten."""
190285
import shutil
191286

192287
# Set up directories
@@ -201,23 +296,46 @@ def test_readme_conflict_handling_logic(tmp_path: pathlib.Path) -> None:
201296
)
202297
(final_destination / "README.md").write_text(existing_readme_content)
203298

299+
# Create existing pyproject.toml in destination
300+
existing_pyproject_content = """[build-system]
301+
requires = ["setuptools>=45", "wheel"]
302+
build-backend = "setuptools.build_meta"
303+
304+
[project]
305+
name = "existing-project"
306+
version = "0.1.0"
307+
"""
308+
(final_destination / "pyproject.toml").write_text(existing_pyproject_content)
309+
204310
# Create templated README in generated project
205311
templated_readme_content = "# Test Project\n\nThis is the templated README content."
206312
(generated_project_dir / "README.md").write_text(templated_readme_content)
207313

314+
# Create templated pyproject.toml in generated project
315+
templated_pyproject_content = """[build-system]
316+
requires = ["poetry-core"]
317+
build-backend = "poetry.core.masonry.api"
318+
319+
[tool.poetry]
320+
name = "templated-project"
321+
version = "0.1.0"
322+
"""
323+
(generated_project_dir / "pyproject.toml").write_text(templated_pyproject_content)
324+
208325
# Also create a non-conflicting file
209326
(generated_project_dir / "other_file.py").write_text("# Other file content")
210327

211-
# Simulate the in-folder copying logic from process_template
328+
# Simulate the remote template copying logic from process_template (in_folder=False)
329+
in_folder = False
212330
for item in generated_project_dir.iterdir():
213331
dest_item = final_destination / item.name
214332

215-
# Special handling for README files - rename templated README if conflict exists
216-
if (
217-
item.name.lower().startswith("readme")
218-
and (final_destination / item.name).exists()
219-
):
220-
# The existing README stays, save the templated one with a different name
333+
# Use the same logic as the updated process_template function
334+
should_preserve_file = item.name.lower().startswith("readme") or (
335+
item.name == "pyproject.toml" and in_folder
336+
)
337+
if should_preserve_file and (final_destination / item.name).exists():
338+
# The existing file stays, save the templated one with a different name
221339
base_name = item.stem
222340
extension = item.suffix
223341
dest_item = final_destination / f"starter_pack_{base_name}{extension}"
@@ -232,6 +350,8 @@ def test_readme_conflict_handling_logic(tmp_path: pathlib.Path) -> None:
232350
# Verify results
233351
original_readme = final_destination / "README.md"
234352
templated_readme = final_destination / "starter_pack_README.md"
353+
pyproject_file = final_destination / "pyproject.toml"
354+
templated_pyproject_backup = final_destination / "starter_pack_pyproject.toml"
235355
other_file = final_destination / "other_file.py"
236356

237357
# Original README should be preserved with original content
@@ -242,6 +362,13 @@ def test_readme_conflict_handling_logic(tmp_path: pathlib.Path) -> None:
242362
assert templated_readme.exists()
243363
assert templated_readme.read_text() == templated_readme_content
244364

365+
# pyproject.toml should be overwritten with templated content (remote template mode)
366+
assert pyproject_file.exists()
367+
assert pyproject_file.read_text() == templated_pyproject_content
368+
369+
# No backup pyproject.toml should exist (remote template mode)
370+
assert not templated_pyproject_backup.exists()
371+
245372
# Other files should copy normally
246373
assert other_file.exists()
247374
assert other_file.read_text() == "# Other file content"

0 commit comments

Comments
 (0)