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
53 changes: 42 additions & 11 deletions .generator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +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.
Expand Down Expand Up @@ -500,8 +499,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 +511,19 @@ 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
134 changes: 127 additions & 7 deletions .generator/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
_write_json_file,
_write_text_file,
_copy_readme_to_docs,
_copy_changelog_to_docs,
handle_build,
handle_configure,
handle_generate,
Expand Down Expand Up @@ -1572,7 +1573,7 @@ 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")

Expand All @@ -1581,7 +1582,6 @@ 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(
Expand All @@ -1596,18 +1596,27 @@ def test_copy_readme_to_docs_handles_symlink(mocker):

output = "output"
library_id = "google-cloud-language"
_copy_readme_to_docs(output, library_id)
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"

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

Expand Down Expand Up @@ -1654,3 +1663,114 @@ def test_copy_readme_to_docs_source_not_exists(mocker):
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 created at the source and then copied to docs, handling symlinks."""
mock_makedirs = mocker.patch("os.makedirs")
mock_os_remove = mocker.patch("os.remove")
mock_os_islink = mocker.patch("os.path.islink")
mock_os_lexists = mocker.patch("os.path.lexists")
mock_write_text_file = mocker.patch("cli._write_text_file")
mock_read_text_file = mocker.patch("cli._read_text_file", return_value="# Changelog\n")

output = "output"
library_id = "google-cloud-language"
source_changelog_path = f"{output}/packages/{library_id}/CHANGELOG.md"
docs_path = f"{output}/packages/{library_id}/docs"
destination_changelog_path = f"{docs_path}/CHANGELOG.md"

# Scenario: Source CHANGELOG.md does not exist initially, docs_path is a symlink
mock_os_lexists.side_effect = [False, True] # First for source_changelog_path, second for source_path in _copy_file_to_docs

def islink_side_effect(path):
if path == destination_changelog_path:
return False
if path == 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)

# Assert that source CHANGELOG.md was checked for existence
mock_os_lexists.assert_any_call(source_changelog_path)

# Assert that source CHANGELOG.md was created with default content
mock_write_text_file.assert_any_call(source_changelog_path, "# Changelog\n")

# Assert that _copy_file_to_docs was called to copy from source to destination
mock_read_text_file.assert_any_call(source_changelog_path)
mock_os_islink.assert_any_call(destination_changelog_path)
mock_os_islink.assert_any_call(docs_path)
mock_os_remove.assert_called_once_with(docs_path)
mock_makedirs.assert_called_once_with(Path(docs_path), exist_ok=True)
mock_write_text_file.assert_any_call(destination_changelog_path, "# Changelog\n")


def test_copy_changelog_to_docs_source_not_exists(mocker):
"""Tests that CHANGELOG.md is created at source and then copied to docs."""
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_called_once_with(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")


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):
if path == expected_destination:
return True
if path == expected_docs_path:
return False
return False

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

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()
21 changes: 20 additions & 1 deletion .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 @@ -2092,6 +2092,25 @@ libraries:
remove_regex:
- packages/google-cloud-gke-multicloud
tag_format: '{id}-v{version}'
- id: google-cloud-gkerecommender
version: 0.0.0
last_generated_commit: c288189b43c016dd3cf1ec73ce3cadee8b732f07
apis:
- path: google/cloud/gkerecommender/v1
service_config: gkerecommender_v1.yaml
source_roots:
- packages/google-cloud-gkerecommender
preserve_regex:
- packages/google-cloud-gkerecommender/CHANGELOG.md
- docs/CHANGELOG.md
- docs/README.rst
- samples/README.txt
- scripts/client-post-processing
- samples/snippets/README.rst
- tests/system
remove_regex:
- packages/google-cloud-gkerecommender
tag_format: '{id}-v{version}'
- id: google-cloud-gsuiteaddons
version: 0.3.18
last_generated_commit: 3322511885371d2b2253f209ccc3aa60d4100cfd
Expand Down
13 changes: 13 additions & 0 deletions packages/google-cloud-gkerecommender/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[run]
branch = True

[report]
show_missing = True
omit =
google/cloud/gkerecommender/__init__.py
google/cloud/gkerecommender/gapic_version.py
exclude_lines =
# Re-enable the standard pragma
pragma: NO COVER
# Ignore debug-only repr
def __repr__
34 changes: 34 additions & 0 deletions packages/google-cloud-gkerecommender/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[flake8]
# TODO(https://github.com/googleapis/gapic-generator-python/issues/2333):
# Resolve flake8 lint issues
ignore = E203, E231, E266, E501, W503
exclude =
# TODO(https://github.com/googleapis/gapic-generator-python/issues/2333):
# Ensure that generated code passes flake8 lint
**/gapic/**
**/services/**
**/types/**
# Exclude Protobuf gencode
*_pb2.py

# Standard linting exemptions.
**/.nox/**
__pycache__,
.git,
*.pyc,
conf.py
16 changes: 16 additions & 0 deletions packages/google-cloud-gkerecommender/.repo-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "google-cloud-gkerecommender",
"name_pretty": "GKE Recommender API",
"api_description": "GKE Recommender API",
"product_documentation": "https://cloud.google.com/kubernetes-engine/docs/how-to/machine-learning/inference-quickstart",
"client_documentation": "https://cloud.google.com/python/docs/reference/google-cloud-gkerecommender/latest",
"issue_tracker": "https://issuetracker.google.com/issues/new?component=1790908",
"release_level": "preview",
"language": "python",
"library_type": "GAPIC_AUTO",
"repo": "googleapis/google-cloud-python",
"distribution_name": "google-cloud-gkerecommender",
"api_id": "gkerecommender.googleapis.com",
"default_version": "v1",
"api_shortname": "gkerecommender"
}
1 change: 1 addition & 0 deletions packages/google-cloud-gkerecommender/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
Loading
Loading