Skip to content
55 changes: 43 additions & 12 deletions .generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,6 @@ def _clean_up_files_after_post_processing(output: str, library_id: str):
shutil.rmtree(f"{output}/owl-bot-staging", ignore_errors=True)

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

# 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 @@ -500,8 +498,8 @@ 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.
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
Expand All @@ -512,20 +510,20 @@ def _copy_readme_to_docs(output: str, library_id: str):
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}/README.rst"
source_path = f"{output}/{path_to_library}/{filename}"
docs_path = f"{output}/{path_to_library}/docs"
destination_path = f"{docs_path}/README.rst"
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()
content = _read_text_file(source_path)

# Remove any symlinks at the destination to prevent errors.
if os.path.islink(destination_path):
Expand All @@ -535,10 +533,42 @@ def _copy_readme_to_docs(output: str, library_id: str):

# Ensure the destination directory exists as a real directory.
os.makedirs(docs_path, exist_ok=True)
_write_text_file(destination_path, 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.
"""
path_to_library = f"packages/{library_id}"
source_path = f"{output}/{path_to_library}/CHANGELOG.md"

# If the source CHANGELOG.md doesn't exist, create it at the source location.
if not os.path.lexists(source_path):
content = "# Changelog\n"
_write_text_file(source_path, content)

# Write the content to the destination, creating a new physical file.
with open(destination_path, "w") as f:
f.write(content)
# Now, copy the (guaranteed to exist) source CHANGELOG.md to the docs directory.
_copy_file_to_docs(output, library_id, "CHANGELOG.md")


def handle_generate(
Expand Down Expand Up @@ -583,6 +613,7 @@ def handle_generate(
_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
124 changes: 93 additions & 31 deletions .generator/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
_write_json_file,
_write_text_file,
_copy_readme_to_docs,
_copy_changelog_to_docs,
_copy_file_to_docs,
handle_build,
handle_configure,
handle_generate,
Expand Down Expand Up @@ -640,6 +642,8 @@ def test_handle_generate_success(
"cli._clean_up_files_after_post_processing"
)
mocker.patch("cli._generate_repo_metadata_file")
mocker.patch("cli._copy_changelog_to_docs")
mocker.patch("cli._copy_readme_to_docs")

handle_generate()

Expand Down Expand Up @@ -1572,53 +1576,40 @@ def test_copy_readme_to_docs(mocker):
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_makedirs.assert_called_with(Path(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."""



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

# 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")
mock_os_remove.assert_called_once_with(expected_destination)


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."""
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", return_value=True)
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_os_lexists = mocker.patch("os.path.lexists", return_value=False)
mock_open = mocker.patch(
"builtins.open", mocker.mock_open(read_data="dummy content")
)
Expand All @@ -1627,20 +1618,91 @@ def test_copy_readme_to_docs_destination_path_is_symlink(mocker):
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)
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_file_to_docs_docs_path_is_symlink(mocker):
"""Tests that the file is copied to the docs directory, handling docs_path being a symlink."""
mock_makedirs = mocker.patch("os.makedirs")
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")
)

def test_copy_readme_to_docs_source_not_exists(mocker):
output = "output"
library_id = "google-cloud-language"
filename = "README.rst"
docs_path = f"{output}/packages/{library_id}/docs"

def islink_side_effect(path):
if path == docs_path:
return True
return False

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

_copy_file_to_docs(output, library_id, filename)

mock_os_remove.assert_called_once_with(docs_path)


def test_copy_changelog_to_docs_handles_symlink(mocker):
"""Tests that the CHANGELOG.md is created at the source and then copied to docs, handling symlinks."""
mock_makedirs = mocker.patch("os.makedirs")
mock_lexists = mocker.patch("os.path.lexists", return_value=False)
mock_write_text = mocker.patch("cli._write_text_file")
mock_copy_file = mocker.patch("cli._copy_file_to_docs")

output = "output"
library_id = "google-cloud-language"
source_path = f"{output}/packages/{library_id}/CHANGELOG.md"

_copy_changelog_to_docs(output, library_id)
mock_lexists.assert_any_call(source_path)
mock_write_text.assert_called_once_with(source_path, "# Changelog\n")
mock_copy_file.assert_called_once_with(output, library_id, "CHANGELOG.md")
mock_copy_file.assert_called_once_with(output, library_id, "CHANGELOG.md")


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_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="# Changelog\n"))

output = "output"
library_id = "google-cloud-language"
expected_destination = "output/packages/google-cloud-language/docs/CHANGELOG.md"
expected_docs_path = "output/packages/google-cloud-language/docs"

def islink_side_effect(path):
return path == expected_destination

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

_copy_changelog_to_docs(output, library_id)

mock_os_remove.assert_called_once_with(expected_destination)
mock_makedirs.assert_called_with(Path(expected_docs_path), exist_ok=True)
mock_open.assert_any_call(expected_destination, "w")
mock_open().write.assert_called_with("# Changelog\n")
"""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")
)
mock_open = mocker.patch("builtins.open", mocker.mock_open(read_data="dummy content"))

output = "output"
library_id = "google-cloud-language"
Expand Down
4 changes: 2 additions & 2 deletions .librarian/state.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -1717,7 +1717,7 @@ libraries:
tag_format: '{id}-v{version}'
- id: google-cloud-dlp
version: 3.33.0
last_generated_commit: 3322511885371d2b2253f209ccc3aa60d4100cfd
last_generated_commit: 1b5c44879f3281d05731a0bf3fc0345ff4463eed
apis:
- path: google/privacy/dlp/v2
service_config: dlp_v2.yaml
Expand Down
Loading
Loading