Skip to content

Commit c4643d3

Browse files
authored
chore(librarian): Add header to files under .librarian/generator-input (#14901)
Files residing in `.librarian/generator-input` are the source of truth and override their counterparts in the root directory. Modifications intended for files such as `setup.py` must be applied within `.librarian/generator-input`. IOW, for `python-datastore`, we need to make changes in https://github.com/googleapis/python-datastore/blob/main/.librarian/generator-input/setup.py rather than https://github.com/googleapis/python-datastore/blob/main/setup.py because the source of truth is `.librarian/generator-input/setup.py` This PR adds a header with the following text to all files in `.librarian/generator-input`, excluding JSON files which don't support comments. ``` # DO NOT EDIT THIS FILE OUTSIDE OF `.librarian/generator-input` # The source of truth for this file is .librarian/generator-input ```
1 parent 40657cc commit c4643d3

File tree

2 files changed

+143
-1
lines changed

2 files changed

+143
-1
lines changed

.generator/cli.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
SOURCE_DIR = "source"
5858
_GITHUB_BASE = "https://github.com"
5959

60+
_GENERATOR_INPUT_HEADER_TEXT = (
61+
"# DO NOT EDIT THIS FILE OUTSIDE OF `.librarian/generator-input`\n"
62+
"# The source of truth for this file is `.librarian/generator-input`\n"
63+
)
64+
6065

6166
def _read_text_file(path: str) -> str:
6267
"""Helper function that reads a text file path and returns the content.
@@ -349,6 +354,41 @@ def _run_post_processor(output: str, library_id: str, is_mono_repo: bool):
349354
logger.info("Python post-processor ran successfully.")
350355

351356

357+
def _add_header_to_files(directory: str) -> None:
358+
"""Adds a 'DO NOT EDIT' header to files in the specified directory.
359+
360+
Skips JSON and YAML files. Attempts to insert the header after any existing
361+
license headers (blocks of comments starting with '#').
362+
363+
Args:
364+
directory (str): The directory containing files to update.
365+
"""
366+
367+
# Files with these extensions should be ignored.
368+
skipped_extensions = {".json", ".yaml"}
369+
370+
for root, _, files in os.walk(directory):
371+
for file_name in files:
372+
file_path = Path(root) / file_name
373+
374+
if file_path.suffix in skipped_extensions:
375+
continue
376+
377+
with open(file_path, "r", encoding="utf-8") as f:
378+
lines = f.readlines()
379+
380+
line_index = 0
381+
# Skip the license header (contiguous block of comments starting with '#').
382+
while line_index < len(lines) and lines[line_index].strip().startswith("#"):
383+
line_index += 1
384+
385+
header_prefix = "\n" if line_index > 0 else ""
386+
lines.insert(line_index, f"{header_prefix}{_GENERATOR_INPUT_HEADER_TEXT}\n")
387+
388+
with open(file_path, "w", encoding="utf-8") as f:
389+
f.writelines(lines)
390+
391+
352392
def _copy_files_needed_for_post_processing(
353393
output: str, input: str, library_id: str, is_mono_repo: bool
354394
):
@@ -367,13 +407,16 @@ def _copy_files_needed_for_post_processing(
367407

368408
path_to_library = f"packages/{library_id}" if is_mono_repo else "."
369409
source_dir = f"{input}/{path_to_library}"
410+
destination_dir = f"{output}/{path_to_library}"
370411

371412
if Path(source_dir).exists():
372413
shutil.copytree(
373414
source_dir,
374-
f"{output}/{path_to_library}",
415+
destination_dir,
375416
dirs_exist_ok=True,
376417
)
418+
# Apply headers only to the generator-input files copied above.
419+
_add_header_to_files(destination_dir)
377420

378421
# We need to create these directories so that we can copy files necessary for post-processing.
379422
os.makedirs(

.generator/test_cli.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import pytest
2828
from cli import (
29+
_GENERATOR_INPUT_HEADER_TEXT,
2930
GENERATE_REQUEST_FILE,
3031
BUILD_REQUEST_FILE,
3132
CONFIGURE_REQUEST_FILE,
@@ -34,6 +35,7 @@
3435
STATE_YAML_FILE,
3536
LIBRARIAN_DIR,
3637
REPO_DIR,
38+
_add_header_to_files,
3739
_clean_up_files_after_post_processing,
3840
_copy_files_needed_for_post_processing,
3941
_create_main_version_header,
@@ -142,6 +144,16 @@
142144
)"""
143145

144146

147+
@pytest.fixture
148+
def setup_dirs(tmp_path):
149+
"""Creates input and output directories."""
150+
input_dir = tmp_path / "input"
151+
output_dir = tmp_path / "output"
152+
input_dir.mkdir()
153+
output_dir.mkdir()
154+
return input_dir, output_dir
155+
156+
145157
@pytest.fixture(autouse=True)
146158
def _clear_lru_cache():
147159
"""Automatically clears the cache of all LRU-cached functions after each test."""
@@ -906,6 +918,93 @@ def test_copy_files_needed_for_post_processing_copies_files_from_generator_input
906918
mock_makedirs.assert_called()
907919

908920

921+
def test_copy_files_needed_for_post_processing_copies_files_from_generator_input_skips_json_files(
922+
setup_dirs,
923+
):
924+
"""Test that .json files are copied but NOT modified."""
925+
input_dir, output_dir = setup_dirs
926+
927+
json_content = '{"key": "value"}'
928+
(input_dir / ".repo-metadata.json").write_text(json_content)
929+
930+
_copy_files_needed_for_post_processing(
931+
output=str(output_dir),
932+
input=str(input_dir),
933+
library_id="google-cloud-foo",
934+
is_mono_repo=False,
935+
)
936+
937+
dest_file = output_dir / ".repo-metadata.json"
938+
assert dest_file.exists()
939+
# Content should be exactly the same, no # comments added
940+
assert dest_file.read_text() == json_content
941+
942+
943+
def test_add_header_with_existing_license(tmp_path):
944+
"""
945+
Test that the header is inserted AFTER the existing license block.
946+
"""
947+
# Setup: Create a file with a license header
948+
file_path = tmp_path / "example.py"
949+
original_content = (
950+
"# Copyright 2025 Google LLC\n" "# Licensed under Apache 2.0\n" "\n" "import os"
951+
)
952+
file_path.write_text(original_content, encoding="utf-8")
953+
954+
# Execute
955+
_add_header_to_files(str(tmp_path))
956+
957+
# Verify
958+
new_content = file_path.read_text(encoding="utf-8")
959+
expected_content = (
960+
"# Copyright 2025 Google LLC\n"
961+
"# Licensed under Apache 2.0\n"
962+
"\n"
963+
f"{_GENERATOR_INPUT_HEADER_TEXT}\n"
964+
"\n"
965+
"import os"
966+
)
967+
assert new_content == expected_content
968+
969+
970+
def test_add_header_to_files_add_header_no_license(tmp_path):
971+
"""
972+
Test that the header is inserted at the top if no license block exists.
973+
"""
974+
# Setup: Create a file starting directly with code
975+
file_path = tmp_path / "script.sh"
976+
original_content = "echo 'Hello World'"
977+
file_path.write_text(original_content, encoding="utf-8")
978+
979+
# Execute
980+
_add_header_to_files(str(tmp_path))
981+
982+
# Verify
983+
new_content = file_path.read_text(encoding="utf-8")
984+
expected_content = f"{_GENERATOR_INPUT_HEADER_TEXT}\n" "echo 'Hello World'"
985+
assert new_content == expected_content
986+
987+
988+
def test_add_header_to_files_skips_excluded_extensions(tmp_path):
989+
"""
990+
Test that .json and .yaml files are ignored.
991+
"""
992+
# Setup: Create files that should be ignored
993+
json_file = tmp_path / "data.json"
994+
yaml_file = tmp_path / "config.yaml"
995+
996+
content = "key: value"
997+
json_file.write_text('{"key": "value"}', encoding="utf-8")
998+
yaml_file.write_text(content, encoding="utf-8")
999+
1000+
# Execute
1001+
_add_header_to_files(str(tmp_path))
1002+
1003+
# Verify contents remain exactly the same
1004+
assert json_file.read_text(encoding="utf-8") == '{"key": "value"}'
1005+
assert yaml_file.read_text(encoding="utf-8") == content
1006+
1007+
9091008
@pytest.mark.parametrize("is_mono_repo", [False, True])
9101009
def test_clean_up_files_after_post_processing_success(mocker, is_mono_repo):
9111010
mock_shutil_rmtree = mocker.patch("shutil.rmtree")

0 commit comments

Comments
 (0)