From ff47911a14ad9d41851ed7d976368d6f314eb82c Mon Sep 17 00:00:00 2001 From: ohmayr Date: Fri, 17 Oct 2025 00:55:46 +0000 Subject: [PATCH 1/4] chore: copy README.rst to docs/README.rst --- .generator/cli.py | 43 +++++++++++++++++++++- .generator/test_cli.py | 81 ++++++++++++++++++++++++++++++++++++++++++ .librarian/state.yaml | 2 +- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/.generator/cli.py b/.generator/cli.py index 3b10d6ec4306..26509443ffad 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -381,7 +381,6 @@ def _clean_up_files_after_post_processing(output: str, library_id: str): # Safely remove specific files if they exist using pathlib. Path(f"{output}/{path_to_library}/CHANGELOG.md").unlink(missing_ok=True) Path(f"{output}/{path_to_library}/docs/CHANGELOG.md").unlink(missing_ok=True) - Path(f"{output}/{path_to_library}/docs/README.rst").unlink(missing_ok=True) # The glob loops are already safe, as they do nothing if no files match. for post_processing_file in glob.glob( @@ -501,6 +500,47 @@ def _generate_repo_metadata_file( _write_json_file(output_repo_metadata, metadata_content) +def _copy_readme_to_docs(output: str, library_id: str): + """Copies the README.rst file for a generated library to docs/README.rst. + + This function is robust against various symlink configurations that could + cause `shutil.copy` to fail with a `SameFileError`. It reads the content + from the source and writes it to the destination, ensuring the final + destination is a real file. + + Args: + output(str): Path to the directory in the container where code + should be generated. + library_id(str): The library id. + """ + path_to_library = f"packages/{library_id}" + source_path = f"{output}/{path_to_library}/README.rst" + docs_path = f"{output}/{path_to_library}/docs" + destination_path = f"{docs_path}/README.rst" + + # If the source file doesn't exist (not even as a broken symlink), + # there's nothing to copy. + if not os.path.lexists(source_path): + return + + # Read the content from the source, which will resolve any symlinks. + with open(source_path, "r") as f: + content = f.read() + + # Remove any symlinks at the destination to prevent errors. + if os.path.islink(destination_path): + os.remove(destination_path) + elif os.path.islink(docs_path): + os.remove(docs_path) + + # Ensure the destination directory exists as a real directory. + os.makedirs(docs_path, exist_ok=True) + + # Write the content to the destination, creating a new physical file. + with open(destination_path, "w") as f: + f.write(content) + + def handle_generate( librarian: str = LIBRARIAN_DIR, source: str = SOURCE_DIR, @@ -542,6 +582,7 @@ def handle_generate( _copy_files_needed_for_post_processing(output, input, library_id) _generate_repo_metadata_file(output, library_id, source, apis_to_generate) _run_post_processor(output, library_id) + _copy_readme_to_docs(output, library_id) _clean_up_files_after_post_processing(output, library_id) except Exception as e: raise ValueError("Generation failed.") from e diff --git a/.generator/test_cli.py b/.generator/test_cli.py index 5751c60b71a2..96c73598b70f 100644 --- a/.generator/test_cli.py +++ b/.generator/test_cli.py @@ -70,6 +70,7 @@ _verify_library_namespace, _write_json_file, _write_text_file, + _copy_readme_to_docs, handle_build, handle_configure, handle_generate, @@ -1514,3 +1515,83 @@ def test_stage_gapic_library(mocker): mock_shutil_copytree.assert_called_once_with( tmp_dir, staging_dir, dirs_exist_ok=True ) + + +def test_copy_readme_to_docs(mocker): + """Tests that the README.rst is copied to the docs directory, handling symlinks.""" + mock_makedirs = mocker.patch("os.makedirs") + mock_shutil_copy = mocker.patch("shutil.copy") + mock_os_islink = mocker.patch("os.path.islink", return_value=False) + mock_os_remove = mocker.patch("os.remove") + mock_os_lexists = mocker.patch("os.path.lexists", return_value=True) + mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content")) + + output = "output" + library_id = "google-cloud-language" + _copy_readme_to_docs(output, library_id) + + expected_source = "output/packages/google-cloud-language/README.rst" + expected_docs_path = "output/packages/google-cloud-language/docs" + expected_destination = "output/packages/google-cloud-language/docs/README.rst" + + mock_os_lexists.assert_called_once_with(expected_source) + mock_open.assert_any_call(expected_source, "r") + mock_os_islink.assert_any_call(expected_destination) + mock_os_islink.assert_any_call(expected_docs_path) + mock_os_remove.assert_not_called() + mock_makedirs.assert_called_once_with(expected_docs_path, exist_ok=True) + mock_open.assert_any_call(expected_destination, "w") + mock_open().write.assert_called_once_with("dummy content") + + +def test_copy_readme_to_docs_handles_symlink(mocker): + """Tests that the README.rst is copied to the docs directory, handling symlinks.""" + mock_makedirs = mocker.patch("os.makedirs") + mock_shutil_copy = mocker.patch("shutil.copy") + mock_os_islink = mocker.patch("os.path.islink") + mock_os_remove = mocker.patch("os.remove") + mock_os_lexists = mocker.patch("os.path.lexists", return_value=True) + mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content")) + + # Simulate docs_path being a symlink + mock_os_islink.side_effect = [False, True] # First call for destination_path, second for docs_path + + output = "output" + library_id = "google-cloud-language" + _copy_readme_to_docs(output, library_id) + + expected_source = "output/packages/google-cloud-language/README.rst" + expected_docs_path = "output/packages/google-cloud-language/docs" + expected_destination = "output/packages/google-cloud-language/docs/README.rst" + + mock_os_lexists.assert_called_once_with(expected_source) + mock_open.assert_any_call(expected_source, "r") + mock_os_islink.assert_any_call(expected_destination) + mock_os_islink.assert_any_call(expected_docs_path) + mock_os_remove.assert_called_once_with(expected_docs_path) + mock_makedirs.assert_called_once_with(expected_docs_path, exist_ok=True) + mock_open.assert_any_call(expected_destination, "w") + mock_open().write.assert_called_once_with("dummy content") + + +def test_copy_readme_to_docs_source_not_exists(mocker): + """Tests that the function returns early if the source README.rst does not exist.""" + mock_makedirs = mocker.patch("os.makedirs") + mock_shutil_copy = mocker.patch("shutil.copy") + mock_os_islink = mocker.patch("os.path.islink") + mock_os_remove = mocker.patch("os.remove") + mock_os_lexists = mocker.patch("os.path.lexists", return_value=False) + mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content")) + + output = "output" + library_id = "google-cloud-language" + _copy_readme_to_docs(output, library_id) + + expected_source = "output/packages/google-cloud-language/README.rst" + + mock_os_lexists.assert_called_once_with(expected_source) + mock_open.assert_not_called() + mock_os_islink.assert_not_called() + mock_os_remove.assert_not_called() + mock_makedirs.assert_not_called() + mock_shutil_copy.assert_not_called() diff --git a/.librarian/state.yaml b/.librarian/state.yaml index b2a02a6b3a8a..887d2d736ffb 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,4 +1,4 @@ -image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator:latest +image: python-librarian-generator:latest libraries: - id: google-ads-admanager version: 0.5.0 From 03db1c6d50793cddbbbf22c15d4310f4ef3afed7 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Fri, 17 Oct 2025 22:01:30 +0000 Subject: [PATCH 2/4] add code coverage --- .generator/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.generator/cli.py b/.generator/cli.py index 26509443ffad..b6697cf8cb09 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -530,7 +530,7 @@ def _copy_readme_to_docs(output: str, library_id: str): # Remove any symlinks at the destination to prevent errors. if os.path.islink(destination_path): os.remove(destination_path) - elif os.path.islink(docs_path): + if os.path.islink(docs_path): os.remove(docs_path) # Ensure the destination directory exists as a real directory. From 26ef3d48026c413fb3b75d1f5621441b94a5bcc0 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Fri, 17 Oct 2025 23:14:09 +0000 Subject: [PATCH 3/4] add missing coverage --- .generator/cli.py | 2 +- .generator/test_cli.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.generator/cli.py b/.generator/cli.py index b6697cf8cb09..26509443ffad 100644 --- a/.generator/cli.py +++ b/.generator/cli.py @@ -530,7 +530,7 @@ def _copy_readme_to_docs(output: str, library_id: str): # Remove any symlinks at the destination to prevent errors. if os.path.islink(destination_path): os.remove(destination_path) - if os.path.islink(docs_path): + elif os.path.islink(docs_path): os.remove(docs_path) # Ensure the destination directory exists as a real directory. diff --git a/.generator/test_cli.py b/.generator/test_cli.py index 96c73598b70f..a401229a6723 100644 --- a/.generator/test_cli.py +++ b/.generator/test_cli.py @@ -1574,6 +1574,23 @@ def test_copy_readme_to_docs_handles_symlink(mocker): mock_open().write.assert_called_once_with("dummy content") +def test_copy_readme_to_docs_destination_path_is_symlink(mocker): + """Tests that the README.rst is copied to the docs directory, handling destination_path being a symlink.""" + mock_makedirs = mocker.patch("os.makedirs") + mock_shutil_copy = mocker.patch("shutil.copy") + mock_os_islink = mocker.patch("os.path.islink", return_value=True) + mock_os_remove = mocker.patch("os.remove") + mock_os_lexists = mocker.patch("os.path.lexists", return_value=True) + mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content")) + + output = "output" + library_id = "google-cloud-language" + _copy_readme_to_docs(output, library_id) + + expected_destination = "output/packages/google-cloud-language/docs/README.rst" + mock_os_remove.assert_called_once_with(expected_destination) + + def test_copy_readme_to_docs_source_not_exists(mocker): """Tests that the function returns early if the source README.rst does not exist.""" mock_makedirs = mocker.patch("os.makedirs") From cb0318dcbe181836a651efa71b23fdc2703bc5a8 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Fri, 17 Oct 2025 23:18:10 +0000 Subject: [PATCH 4/4] revert state --- .librarian/state.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.librarian/state.yaml b/.librarian/state.yaml index 887d2d736ffb..b2a02a6b3a8a 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,4 +1,4 @@ -image: python-librarian-generator:latest +image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator:latest libraries: - id: google-ads-admanager version: 0.5.0