Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion .generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -501,6 +500,74 @@ def _generate_repo_metadata_file(
_write_json_file(output_repo_metadata, metadata_content)


def _copy_file_to_docs(output: str, library_id: str, filename: str):
"""Copies a file for a generated library to the docs directory.

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.
filename(str): The name of the file to copy.
"""
path_to_library = f"packages/{library_id}"
source_path = f"{output}/{path_to_library}/{filename}"
docs_path = f"{output}/{path_to_library}/docs"
destination_path = f"{docs_path}/{filename}"

# 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 _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 a wrapper around `_copy_file_to_docs` for README.rst.

Args:
output(str): Path to the directory in the container where code
should be generated.
library_id(str): The library id.
"""
_copy_file_to_docs(output, library_id, "README.rst")


def _copy_changelog_to_docs(output: str, library_id: str):
"""Copies the CHANGELOG.md file for a generated library to docs/CHANGELOG.md.

This function is a wrapper around `_copy_file_to_docs` for CHANGELOG.md.

Args:
output(str): Path to the directory in the container where code
should be generated.
library_id(str): The library id.
"""
_copy_file_to_docs(output, library_id, "CHANGELOG.md")


def handle_generate(
librarian: str = LIBRARIAN_DIR,
source: str = SOURCE_DIR,
Expand Down Expand Up @@ -542,6 +609,8 @@ 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)
_copy_changelog_to_docs(output, library_id)
_clean_up_files_after_post_processing(output, library_id)
except Exception as e:
raise ValueError("Generation failed.") from e
Expand Down
202 changes: 202 additions & 0 deletions .generator/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
_verify_library_namespace,
_write_json_file,
_write_text_file,
_copy_readme_to_docs,
_copy_changelog_to_docs,
handle_build,
handle_configure,
handle_generate,
Expand Down Expand Up @@ -1514,3 +1516,203 @@ 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_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"
expected_source = f"{output}/packages/{library_id}/README.rst"
expected_docs_path = f"{output}/packages/{library_id}/docs"
expected_destination = f"{expected_docs_path}/README.rst"

def islink_side_effect(path):
if path == expected_destination:
return False
if path == expected_docs_path:
return True
return False

mock_os_islink = mocker.patch("os.path.islink", side_effect=islink_side_effect)

_copy_readme_to_docs(output, library_id)

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_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")
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()


def test_copy_changelog_to_docs_handles_symlink(mocker):
"""Tests that the CHANGELOG.md is copied to the docs directory, handling symlinks."""
mock_makedirs = mocker.patch("os.makedirs")
mock_shutil_copy = mocker.patch("shutil.copy")
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"
expected_source = f"{output}/packages/{library_id}/CHANGELOG.md"
expected_docs_path = f"{output}/packages/{library_id}/docs"
expected_destination = f"{expected_docs_path}/CHANGELOG.md"

def islink_side_effect(path):
if path == expected_destination:
return False
if path == expected_docs_path:
return True
return False

mock_os_islink = mocker.patch("os.path.islink", side_effect=islink_side_effect)

_copy_changelog_to_docs(output, library_id)

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_changelog_to_docs_source_not_exists(mocker):
"""Tests that the function returns early if the source CHANGELOG.md 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_changelog_to_docs(output, library_id)

expected_source = "output/packages/google-cloud-language/CHANGELOG.md"

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()


def test_copy_changelog_to_docs_destination_path_is_symlink(mocker):
"""Tests that the CHANGELOG.md 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_changelog_to_docs(output, library_id)

expected_destination = "output/packages/google-cloud-language/docs/CHANGELOG.md"
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")
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()
Loading
Loading