Skip to content

Commit 6b8c9cf

Browse files
authored
feat: add support to release handwritten clients (#14728)
This PR adds support to release handwritten clients using librarian such as `google-auth` and `google-cloud-pubsub`.
1 parent 37ac520 commit 6b8c9cf

File tree

3 files changed

+117
-30
lines changed

3 files changed

+117
-30
lines changed

.generator/cli.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def handle_configure(
228228
source: str = SOURCE_DIR,
229229
repo: str = REPO_DIR,
230230
input: str = INPUT_DIR,
231-
output: str = OUTPUT_DIR
231+
output: str = OUTPUT_DIR,
232232
):
233233
"""Onboards a new library by completing its configuration.
234234
@@ -259,7 +259,7 @@ def handle_configure(
259259
# configure-request.json contains the library definitions.
260260
request_data = _read_json_file(f"{librarian}/{CONFIGURE_REQUEST_FILE}")
261261
new_library_config = _get_new_library_config(request_data)
262-
262+
263263
_update_global_changelog(
264264
f"{repo}/CHANGELOG.md",
265265
f"{output}/CHANGELOG.md",
@@ -1110,7 +1110,9 @@ def _process_version_file(content, version, version_path) -> str:
11101110
11111111
Returns: A string with the modified content.
11121112
"""
1113-
if version_path.name.endswith("gapic_version.py"):
1113+
if version_path.name.endswith("gapic_version.py") or version_path.name.endswith(
1114+
"version.py"
1115+
):
11141116
pattern = r"(__version__\s*=\s*[\"'])([^\"']+)([\"'].*)"
11151117
else:
11161118
pattern = r"(version\s*=\s*[\"'])([^\"']+)([\"'].*)"
@@ -1126,7 +1128,7 @@ def _process_version_file(content, version, version_path) -> str:
11261128
def _update_version_for_library(
11271129
repo: str, output: str, path_to_library: str, version: str
11281130
):
1129-
"""Updates the version string in `**/gapic_version.py`, `setup.py`,
1131+
"""Updates the version string in `**/gapic_version.py`, `**/version.py`, `setup.py`,
11301132
`pyproject.toml` and `samples/**/snippet_metadata.json` for a
11311133
given library, if applicable.
11321134
@@ -1140,12 +1142,31 @@ def _update_version_for_library(
11401142
version(str): The new version of the library
11411143
11421144
Raises: `ValueError` if a version string could not be located in `**/gapic_version.py`
1143-
within the given library.
1145+
or `**/version.py` within the given library.
11441146
"""
11451147

1146-
# Find and update gapic_version.py files
1147-
version_files = list(Path(f"{repo}/{path_to_library}").rglob("**/gapic_version.py"))
1148-
if len(version_files) == 0:
1148+
# Find and update version.py or gapic_version.py files
1149+
search_base = Path(f"{repo}/{path_to_library}")
1150+
version_files = list(search_base.rglob("**/gapic_version.py"))
1151+
excluded_dirs = {
1152+
".nox",
1153+
".venv",
1154+
"venv",
1155+
"site-packages",
1156+
".git",
1157+
"build",
1158+
"dist",
1159+
"__pycache__",
1160+
}
1161+
version_files.extend(
1162+
[
1163+
p
1164+
for p in search_base.rglob("**/version.py")
1165+
if not any(part in excluded_dirs for part in p.parts)
1166+
]
1167+
)
1168+
1169+
if not version_files:
11491170
# Fallback to `pyproject.toml`` or `setup.py``. Proto-only libraries have
11501171
# version information in `setup.py` or `pyproject.toml` instead of `gapic_version.py`.
11511172
pyproject_toml = Path(f"{repo}/{path_to_library}/pyproject.toml")
@@ -1161,7 +1182,7 @@ def _update_version_for_library(
11611182

11621183
# Find and update snippet_metadata.json files
11631184
snippet_metadata_files = Path(f"{repo}/{path_to_library}").rglob(
1164-
"samples/**/*.json"
1185+
"samples/**/*snippet*.json"
11651186
)
11661187
for metadata_file in snippet_metadata_files:
11671188
output_path = f"{output}/{metadata_file.relative_to(repo)}"
@@ -1301,6 +1322,7 @@ def _update_changelog_for_library(
13011322
version: str,
13021323
previous_version: str,
13031324
library_id: str,
1325+
relative_path: str,
13041326
):
13051327
"""Prepends a new release entry with multiple, grouped changes, to a changelog.
13061328
@@ -1317,8 +1339,6 @@ def _update_changelog_for_library(
13171339
library_id(str): The id of the library where the changelog should
13181340
be updated.
13191341
"""
1320-
1321-
relative_path = f"packages/{library_id}/CHANGELOG.md"
13221342
changelog_src = f"{repo}/{relative_path}"
13231343
changelog_dest = f"{output}/{relative_path}"
13241344
updated_content = _process_changelog(
@@ -1331,6 +1351,19 @@ def _update_changelog_for_library(
13311351
_write_text_file(changelog_dest, updated_content)
13321352

13331353

1354+
def _is_mono_repo(repo: str) -> bool:
1355+
"""Determines if a library is generated or handwritten.
1356+
1357+
Args:
1358+
repo(str): This directory will contain all directories that make up a
1359+
library, the .librarian folder, and any global file declared in
1360+
the config.yaml.
1361+
1362+
Returns: True if the library is generated, False otherwise.
1363+
"""
1364+
return Path(f"{repo}/packages").exists()
1365+
1366+
13341367
def handle_release_init(
13351368
librarian: str = LIBRARIAN_DIR, repo: str = REPO_DIR, output: str = OUTPUT_DIR
13361369
):
@@ -1358,27 +1391,30 @@ def handle_release_init(
13581391
`release-init-request.json` file in the given
13591392
librarian directory cannot be read.
13601393
"""
1361-
13621394
try:
1395+
is_mono_repo = _is_mono_repo(repo)
1396+
13631397
# Read a release-init-request.json file
13641398
request_data = _read_json_file(f"{librarian}/{RELEASE_INIT_REQUEST_FILE}")
13651399
libraries_to_prep_for_release = _get_libraries_to_prepare_for_release(
13661400
request_data
13671401
)
13681402

1369-
_update_global_changelog(
1370-
f"{repo}/CHANGELOG.md",
1371-
f"{output}/CHANGELOG.md",
1372-
libraries_to_prep_for_release,
1373-
)
1403+
if is_mono_repo:
1404+
1405+
# only a mono repo has a global changelog
1406+
_update_global_changelog(
1407+
f"{repo}/CHANGELOG.md",
1408+
f"{output}/CHANGELOG.md",
1409+
libraries_to_prep_for_release,
1410+
)
13741411

13751412
# Prepare the release for each library by updating the
13761413
# library specific version files and library specific changelog.
13771414
for library_release_data in libraries_to_prep_for_release:
13781415
version = library_release_data["version"]
13791416
library_id = library_release_data["id"]
13801417
library_changes = library_release_data["changes"]
1381-
path_to_library = f"packages/{library_id}"
13821418

13831419
# Get previous version from state.yaml
13841420
previous_version = _get_previous_version(library_id, librarian)
@@ -1388,6 +1424,13 @@ def handle_release_init(
13881424
f"{library_id} version: {previous_version}\n"
13891425
)
13901426

1427+
if is_mono_repo:
1428+
path_to_library = f"packages/{library_id}"
1429+
changelog_relative_path = f"packages/{library_id}/CHANGELOG.md"
1430+
else:
1431+
path_to_library = "."
1432+
changelog_relative_path = "CHANGELOG.md"
1433+
13911434
_update_version_for_library(repo, output, path_to_library, version)
13921435
_update_changelog_for_library(
13931436
repo,
@@ -1396,6 +1439,7 @@ def handle_release_init(
13961439
version,
13971440
previous_version,
13981441
library_id,
1442+
relative_path=changelog_relative_path,
13991443
)
14001444

14011445
except Exception as e:

.generator/parse_googleapis_content.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
"glob",
104104
)
105105

106+
106107
def parse_content(content: str) -> dict:
107108
"""Parses content from BUILD.bazel and returns a dictionary
108109
containing bazel rules and arguments.

.generator/test_cli.py

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,24 @@ def test_handle_release_init_success(mocker, mock_release_init_request_file):
856856
handle_release_init()
857857

858858

859+
def test_handle_release_init_is_generated_success(
860+
mocker, mock_release_init_request_file
861+
):
862+
"""
863+
Tests that `handle_release_init` calls `_update_global_changelog` when the
864+
`packages` directory exists.
865+
"""
866+
mocker.patch("pathlib.Path.exists", return_value=True)
867+
mock_update_global_changelog = mocker.patch("cli._update_global_changelog")
868+
mocker.patch("cli._update_version_for_library")
869+
mocker.patch("cli._get_previous_version", return_value="1.2.2")
870+
mocker.patch("cli._update_changelog_for_library")
871+
872+
handle_release_init()
873+
874+
mock_update_global_changelog.assert_called_once()
875+
876+
859877
def test_handle_release_init_fail_value_error_file():
860878
"""
861879
Tests that handle_release_init fails to read `librarian/release-init-request.json`.
@@ -970,9 +988,12 @@ def test_update_global_changelog(mocker, mock_release_init_request_file):
970988
def test_update_version_for_library_success_gapic(mocker):
971989
m = mock_open()
972990

973-
mock_rglob = mocker.patch(
974-
"pathlib.Path.rglob", return_value=[pathlib.Path("repo/gapic_version.py")]
975-
)
991+
mock_rglob = mocker.patch("pathlib.Path.rglob")
992+
mock_rglob.side_effect = [
993+
[pathlib.Path("repo/gapic_version.py")], # 1st call (gapic_version.py)
994+
[], # 2nd call (version.py)
995+
[pathlib.Path("repo/samples/snippet_metadata.json")], # 3rd call (snippets)
996+
]
976997
mock_shutil_copy = mocker.patch("shutil.copy")
977998
mock_content = '__version__ = "1.2.2"'
978999
mock_json_metadata = {"clientLibrary": {"version": "0.1.0"}}
@@ -1002,7 +1023,11 @@ def test_update_version_for_library_success_proto_only_setup_py(mocker):
10021023
m = mock_open()
10031024

10041025
mock_rglob = mocker.patch("pathlib.Path.rglob")
1005-
mock_rglob.side_effect = [[], [pathlib.Path("repo/setup.py")]]
1026+
mock_rglob.side_effect = [
1027+
[],
1028+
[pathlib.Path("repo/setup.py")],
1029+
[pathlib.Path("repo/samples/snippet_metadata.json")],
1030+
]
10061031
mock_shutil_copy = mocker.patch("shutil.copy")
10071032
mock_content = 'version = "1.2.2"'
10081033
mock_json_metadata = {"clientLibrary": {"version": "0.1.0"}}
@@ -1028,12 +1053,16 @@ def test_update_version_for_library_success_proto_only_setup_py(mocker):
10281053
)
10291054

10301055

1031-
def test_update_version_for_library_success_proto_only_py_project_toml(mocker):
1056+
def test_update_version_for_library_success_proto_only_pyproject_toml(mocker):
10321057
m = mock_open()
10331058

1034-
mock_path_exists = mocker.patch("pathlib.Path.exists")
1059+
mock_path_exists = mocker.patch("pathlib.Path.exists", return_value=True)
10351060
mock_rglob = mocker.patch("pathlib.Path.rglob")
1036-
mock_rglob.side_effect = [[], [pathlib.Path("repo/pyproject.toml")]]
1061+
mock_rglob.side_effect = [
1062+
[], # gapic_version.py
1063+
[], # version.py
1064+
[pathlib.Path("repo/samples/snippet_metadata.json")],
1065+
]
10371066
mock_shutil_copy = mocker.patch("shutil.copy")
10381067
mock_content = 'version = "1.2.2"'
10391068
mock_json_metadata = {"clientLibrary": {"version": "0.1.0"}}
@@ -1108,6 +1137,7 @@ def test_update_changelog_for_library_success(mocker):
11081137
"1.2.3",
11091138
"1.2.2",
11101139
"google-cloud-language",
1140+
"CHANGELOG.md",
11111141
)
11121142

11131143

@@ -1157,6 +1187,7 @@ def test_update_changelog_for_library_failure(mocker):
11571187
"1.2.3",
11581188
"1.2.2",
11591189
"google-cloud-language",
1190+
"CHANGELOG.md",
11601191
)
11611192

11621193

@@ -1524,7 +1555,9 @@ def test_copy_readme_to_docs(mocker):
15241555
mock_os_islink = mocker.patch("os.path.islink", return_value=False)
15251556
mock_os_remove = mocker.patch("os.remove")
15261557
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"))
1558+
mock_open = mocker.patch(
1559+
"builtins.open", mocker.mock_open(read_data="dummy content")
1560+
)
15281561

15291562
output = "output"
15301563
library_id = "google-cloud-language"
@@ -1551,10 +1584,15 @@ def test_copy_readme_to_docs_handles_symlink(mocker):
15511584
mock_os_islink = mocker.patch("os.path.islink")
15521585
mock_os_remove = mocker.patch("os.remove")
15531586
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"))
1587+
mock_open = mocker.patch(
1588+
"builtins.open", mocker.mock_open(read_data="dummy content")
1589+
)
15551590

15561591
# Simulate docs_path being a symlink
1557-
mock_os_islink.side_effect = [False, True] # First call for destination_path, second for docs_path
1592+
mock_os_islink.side_effect = [
1593+
False,
1594+
True,
1595+
] # First call for destination_path, second for docs_path
15581596

15591597
output = "output"
15601598
library_id = "google-cloud-language"
@@ -1581,7 +1619,9 @@ def test_copy_readme_to_docs_destination_path_is_symlink(mocker):
15811619
mock_os_islink = mocker.patch("os.path.islink", return_value=True)
15821620
mock_os_remove = mocker.patch("os.remove")
15831621
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"))
1622+
mock_open = mocker.patch(
1623+
"builtins.open", mocker.mock_open(read_data="dummy content")
1624+
)
15851625

15861626
output = "output"
15871627
library_id = "google-cloud-language"
@@ -1598,7 +1638,9 @@ def test_copy_readme_to_docs_source_not_exists(mocker):
15981638
mock_os_islink = mocker.patch("os.path.islink")
15991639
mock_os_remove = mocker.patch("os.remove")
16001640
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"))
1641+
mock_open = mocker.patch(
1642+
"builtins.open", mocker.mock_open(read_data="dummy content")
1643+
)
16021644

16031645
output = "output"
16041646
library_id = "google-cloud-language"

0 commit comments

Comments
 (0)