Skip to content

Commit 0928066

Browse files
authored
chore(librarian): create docs/README.rst file during generation (#14751)
This PR creates a `docs/index.rst` file for a new library that is configured and generated using librarian.
1 parent 79a5261 commit 0928066

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed

.generator/cli.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ def _clean_up_files_after_post_processing(output: str, library_id: str):
381381
# Safely remove specific files if they exist using pathlib.
382382
Path(f"{output}/{path_to_library}/CHANGELOG.md").unlink(missing_ok=True)
383383
Path(f"{output}/{path_to_library}/docs/CHANGELOG.md").unlink(missing_ok=True)
384-
Path(f"{output}/{path_to_library}/docs/README.rst").unlink(missing_ok=True)
385384

386385
# The glob loops are already safe, as they do nothing if no files match.
387386
for post_processing_file in glob.glob(
@@ -501,6 +500,47 @@ def _generate_repo_metadata_file(
501500
_write_json_file(output_repo_metadata, metadata_content)
502501

503502

503+
def _copy_readme_to_docs(output: str, library_id: str):
504+
"""Copies the README.rst file for a generated library to docs/README.rst.
505+
506+
This function is robust against various symlink configurations that could
507+
cause `shutil.copy` to fail with a `SameFileError`. It reads the content
508+
from the source and writes it to the destination, ensuring the final
509+
destination is a real file.
510+
511+
Args:
512+
output(str): Path to the directory in the container where code
513+
should be generated.
514+
library_id(str): The library id.
515+
"""
516+
path_to_library = f"packages/{library_id}"
517+
source_path = f"{output}/{path_to_library}/README.rst"
518+
docs_path = f"{output}/{path_to_library}/docs"
519+
destination_path = f"{docs_path}/README.rst"
520+
521+
# If the source file doesn't exist (not even as a broken symlink),
522+
# there's nothing to copy.
523+
if not os.path.lexists(source_path):
524+
return
525+
526+
# Read the content from the source, which will resolve any symlinks.
527+
with open(source_path, "r") as f:
528+
content = f.read()
529+
530+
# Remove any symlinks at the destination to prevent errors.
531+
if os.path.islink(destination_path):
532+
os.remove(destination_path)
533+
elif os.path.islink(docs_path):
534+
os.remove(docs_path)
535+
536+
# Ensure the destination directory exists as a real directory.
537+
os.makedirs(docs_path, exist_ok=True)
538+
539+
# Write the content to the destination, creating a new physical file.
540+
with open(destination_path, "w") as f:
541+
f.write(content)
542+
543+
504544
def handle_generate(
505545
librarian: str = LIBRARIAN_DIR,
506546
source: str = SOURCE_DIR,
@@ -542,6 +582,7 @@ def handle_generate(
542582
_copy_files_needed_for_post_processing(output, input, library_id)
543583
_generate_repo_metadata_file(output, library_id, source, apis_to_generate)
544584
_run_post_processor(output, library_id)
585+
_copy_readme_to_docs(output, library_id)
545586
_clean_up_files_after_post_processing(output, library_id)
546587
except Exception as e:
547588
raise ValueError("Generation failed.") from e

.generator/test_cli.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
_verify_library_namespace,
7171
_write_json_file,
7272
_write_text_file,
73+
_copy_readme_to_docs,
7374
handle_build,
7475
handle_configure,
7576
handle_generate,
@@ -1514,3 +1515,100 @@ def test_stage_gapic_library(mocker):
15141515
mock_shutil_copytree.assert_called_once_with(
15151516
tmp_dir, staging_dir, dirs_exist_ok=True
15161517
)
1518+
1519+
1520+
def test_copy_readme_to_docs(mocker):
1521+
"""Tests that the README.rst is copied to the docs directory, handling symlinks."""
1522+
mock_makedirs = mocker.patch("os.makedirs")
1523+
mock_shutil_copy = mocker.patch("shutil.copy")
1524+
mock_os_islink = mocker.patch("os.path.islink", return_value=False)
1525+
mock_os_remove = mocker.patch("os.remove")
1526+
mock_os_lexists = mocker.patch("os.path.lexists", return_value=True)
1527+
mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content"))
1528+
1529+
output = "output"
1530+
library_id = "google-cloud-language"
1531+
_copy_readme_to_docs(output, library_id)
1532+
1533+
expected_source = "output/packages/google-cloud-language/README.rst"
1534+
expected_docs_path = "output/packages/google-cloud-language/docs"
1535+
expected_destination = "output/packages/google-cloud-language/docs/README.rst"
1536+
1537+
mock_os_lexists.assert_called_once_with(expected_source)
1538+
mock_open.assert_any_call(expected_source, "r")
1539+
mock_os_islink.assert_any_call(expected_destination)
1540+
mock_os_islink.assert_any_call(expected_docs_path)
1541+
mock_os_remove.assert_not_called()
1542+
mock_makedirs.assert_called_once_with(expected_docs_path, exist_ok=True)
1543+
mock_open.assert_any_call(expected_destination, "w")
1544+
mock_open().write.assert_called_once_with("dummy content")
1545+
1546+
1547+
def test_copy_readme_to_docs_handles_symlink(mocker):
1548+
"""Tests that the README.rst is copied to the docs directory, handling symlinks."""
1549+
mock_makedirs = mocker.patch("os.makedirs")
1550+
mock_shutil_copy = mocker.patch("shutil.copy")
1551+
mock_os_islink = mocker.patch("os.path.islink")
1552+
mock_os_remove = mocker.patch("os.remove")
1553+
mock_os_lexists = mocker.patch("os.path.lexists", return_value=True)
1554+
mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content"))
1555+
1556+
# Simulate docs_path being a symlink
1557+
mock_os_islink.side_effect = [False, True] # First call for destination_path, second for docs_path
1558+
1559+
output = "output"
1560+
library_id = "google-cloud-language"
1561+
_copy_readme_to_docs(output, library_id)
1562+
1563+
expected_source = "output/packages/google-cloud-language/README.rst"
1564+
expected_docs_path = "output/packages/google-cloud-language/docs"
1565+
expected_destination = "output/packages/google-cloud-language/docs/README.rst"
1566+
1567+
mock_os_lexists.assert_called_once_with(expected_source)
1568+
mock_open.assert_any_call(expected_source, "r")
1569+
mock_os_islink.assert_any_call(expected_destination)
1570+
mock_os_islink.assert_any_call(expected_docs_path)
1571+
mock_os_remove.assert_called_once_with(expected_docs_path)
1572+
mock_makedirs.assert_called_once_with(expected_docs_path, exist_ok=True)
1573+
mock_open.assert_any_call(expected_destination, "w")
1574+
mock_open().write.assert_called_once_with("dummy content")
1575+
1576+
1577+
def test_copy_readme_to_docs_destination_path_is_symlink(mocker):
1578+
"""Tests that the README.rst is copied to the docs directory, handling destination_path being a symlink."""
1579+
mock_makedirs = mocker.patch("os.makedirs")
1580+
mock_shutil_copy = mocker.patch("shutil.copy")
1581+
mock_os_islink = mocker.patch("os.path.islink", return_value=True)
1582+
mock_os_remove = mocker.patch("os.remove")
1583+
mock_os_lexists = mocker.patch("os.path.lexists", return_value=True)
1584+
mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content"))
1585+
1586+
output = "output"
1587+
library_id = "google-cloud-language"
1588+
_copy_readme_to_docs(output, library_id)
1589+
1590+
expected_destination = "output/packages/google-cloud-language/docs/README.rst"
1591+
mock_os_remove.assert_called_once_with(expected_destination)
1592+
1593+
1594+
def test_copy_readme_to_docs_source_not_exists(mocker):
1595+
"""Tests that the function returns early if the source README.rst does not exist."""
1596+
mock_makedirs = mocker.patch("os.makedirs")
1597+
mock_shutil_copy = mocker.patch("shutil.copy")
1598+
mock_os_islink = mocker.patch("os.path.islink")
1599+
mock_os_remove = mocker.patch("os.remove")
1600+
mock_os_lexists = mocker.patch("os.path.lexists", return_value=False)
1601+
mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content"))
1602+
1603+
output = "output"
1604+
library_id = "google-cloud-language"
1605+
_copy_readme_to_docs(output, library_id)
1606+
1607+
expected_source = "output/packages/google-cloud-language/README.rst"
1608+
1609+
mock_os_lexists.assert_called_once_with(expected_source)
1610+
mock_open.assert_not_called()
1611+
mock_os_islink.assert_not_called()
1612+
mock_os_remove.assert_not_called()
1613+
mock_makedirs.assert_not_called()
1614+
mock_shutil_copy.assert_not_called()

0 commit comments

Comments
 (0)