Skip to content

Commit dafc0fd

Browse files
authored
feat: implement basic librarian configure command (googleapis#14518)
Towards googleapis/librarian#466 🦕
1 parent a336184 commit dafc0fd

File tree

2 files changed

+233
-12
lines changed

2 files changed

+233
-12
lines changed

.generator/cli.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
BUILD_REQUEST_FILE = "build-request.json"
4747
GENERATE_REQUEST_FILE = "generate-request.json"
48+
CONFIGURE_REQUEST_FILE = "configure-request.json"
4849
RELEASE_INIT_REQUEST_FILE = "release-init-request.json"
4950
STATE_YAML_FILE = "state.yaml"
5051

@@ -114,6 +115,103 @@ def _write_json_file(path: str, updated_content: Dict):
114115
json.dump(updated_content, f, indent=2)
115116
f.write("\n")
116117

118+
def _add_new_library_source_roots(library_config: Dict, library_id: str) -> None:
119+
"""Adds the default source_roots to the library configuration if not present.
120+
121+
Args:
122+
library_config(Dict): The library configuration.
123+
library_id(str): The id of the library.
124+
"""
125+
if library_config["source_roots"] is None:
126+
library_config["source_roots"] = [f"packages/{library_id}"]
127+
128+
129+
def _add_new_library_preserve_regex(library_config: Dict, library_id: str) -> None:
130+
"""Adds the default preserve_regex to the library configuration if not present.
131+
132+
Args:
133+
library_config(Dict): The library configuration.
134+
library_id(str): The id of the library.
135+
"""
136+
if library_config["preserve_regex"] is None:
137+
library_config["preserve_regex"] = [
138+
f"packages/{library_id}/CHANGELOG.md",
139+
"docs/CHANGELOG.md",
140+
"docs/README.rst",
141+
"samples/README.txt",
142+
"tar.gz",
143+
"gapic_version.py",
144+
"scripts/client-post-processing",
145+
"samples/snippets/README.rst",
146+
"tests/system",
147+
]
148+
149+
150+
def _add_new_library_remove_regex(library_config: Dict, library_id: str) -> None:
151+
"""Adds the default remove_regex to the library configuration if not present.
152+
153+
Args:
154+
library_config(Dict): The library configuration.
155+
library_id(str): The id of the library.
156+
"""
157+
if library_config["remove_regex"] is None:
158+
library_config["remove_regex"] = [f"packages/{library_id}"]
159+
160+
161+
def _add_new_library_tag_format(library_config: Dict) -> None:
162+
"""Adds the default tag_format to the library configuration if not present.
163+
164+
Args:
165+
library_config(Dict): The library configuration.
166+
"""
167+
if "tag_format" not in library_config:
168+
library_config["tag_format"] = "{{id}}-v{{version}}"
169+
170+
171+
def _get_new_library_config(request_data: Dict) -> Dict:
172+
"""Finds and returns the configuration for a new library.
173+
174+
Args:
175+
request_data(Dict): The request data from which to extract the new
176+
library config.
177+
178+
Returns:
179+
Dict: The unmodified configuration of a new library, or an empty
180+
dictionary if not found.
181+
"""
182+
for library_config in request_data.get("libraries", []):
183+
all_apis = library_config.get("apis", [])
184+
for api in all_apis:
185+
if api.get("status") == "new":
186+
return library_config
187+
return {}
188+
189+
190+
def _prepare_new_library_config(library_config: Dict) -> Dict:
191+
"""
192+
Prepares the new library's configuration by removing temporary keys and
193+
adding default values.
194+
195+
Args:
196+
library_config (Dict): The raw library configuration.
197+
198+
Returns:
199+
Dict: The prepared library configuration.
200+
"""
201+
# remove status key from new library config.
202+
all_apis = library_config.get("apis", [])
203+
for api in all_apis:
204+
if "status" in api:
205+
del api["status"]
206+
207+
library_id = _get_library_id(library_config)
208+
_add_new_library_source_roots(library_config, library_id)
209+
_add_new_library_preserve_regex(library_config, library_id)
210+
_add_new_library_remove_regex(library_config, library_id)
211+
_add_new_library_tag_format(library_config)
212+
213+
return library_config
214+
117215

118216
def handle_configure(
119217
librarian: str = LIBRARIAN_DIR,
@@ -144,7 +242,17 @@ def handle_configure(
144242
Raises:
145243
ValueError: If configuring a new library fails.
146244
"""
147-
# TODO(https://github.com/googleapis/librarian/issues/466): Implement configure command and update docstring.
245+
try:
246+
# configure-request.json contains the library definitions.
247+
request_data = _read_json_file(f"{librarian}/{CONFIGURE_REQUEST_FILE}")
248+
new_library_config = _get_new_library_config(request_data)
249+
prepared_config = _prepare_new_library_config(new_library_config)
250+
251+
# Write the new library configuration to configure-response.json.
252+
_write_json_file(f"{librarian}/configure-response.json", prepared_config)
253+
254+
except Exception as e:
255+
raise ValueError("Configuring a new library failed.") from e
148256
logger.info("'configure' command executed.")
149257

150258

.generator/test_cli.py

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from cli import (
2929
GENERATE_REQUEST_FILE,
3030
BUILD_REQUEST_FILE,
31+
CONFIGURE_REQUEST_FILE,
3132
RELEASE_INIT_REQUEST_FILE,
3233
SOURCE_DIR,
3334
STATE_YAML_FILE,
@@ -43,7 +44,9 @@
4344
_get_library_dist_name,
4445
_get_library_id,
4546
_get_libraries_to_prepare_for_release,
47+
_get_new_library_config,
4648
_get_previous_version,
49+
_prepare_new_library_config,
4750
_process_changelog,
4851
_process_version_file,
4952
_read_bazel_build_py_rule,
@@ -160,6 +163,35 @@ def mock_build_request_file(tmp_path, monkeypatch):
160163
return request_file
161164

162165

166+
@pytest.fixture
167+
def mock_configure_request_data():
168+
"""Returns mock data for configure-request.json."""
169+
return {
170+
"libraries": [
171+
{
172+
"id": "google-cloud-language",
173+
"apis": [{"path": "google/cloud/language/v1", "status": "new"}],
174+
}
175+
]
176+
}
177+
178+
179+
@pytest.fixture
180+
def mock_configure_request_file(tmp_path, monkeypatch, mock_configure_request_data):
181+
"""Creates the mock request file at the correct path inside a temp dir."""
182+
# Create the path as expected by the script: .librarian/configure-request.json
183+
request_path = f"{LIBRARIAN_DIR}/{CONFIGURE_REQUEST_FILE}"
184+
request_dir = tmp_path / os.path.dirname(request_path)
185+
request_dir.mkdir(parents=True, exist_ok=True)
186+
request_file = request_dir / os.path.basename(request_path)
187+
188+
request_file.write_text(json.dumps(mock_configure_request_data))
189+
190+
# Change the current working directory to the temp path for the test.
191+
monkeypatch.chdir(tmp_path)
192+
return request_file
193+
194+
163195
@pytest.fixture
164196
def mock_build_bazel_file(tmp_path, monkeypatch):
165197
"""Creates the mock BUILD.bazel file at the correct path inside a temp dir."""
@@ -236,6 +268,98 @@ def mock_state_file(tmp_path, monkeypatch):
236268
return request_file
237269

238270

271+
def test_handle_configure_success(mock_configure_request_file, mocker):
272+
"""Tests the successful execution path of handle_configure."""
273+
mock_write_json = mocker.patch("cli._write_json_file")
274+
mock_prepare_config = mocker.patch(
275+
"cli._prepare_new_library_config", return_value={"id": "prepared"}
276+
)
277+
278+
handle_configure()
279+
280+
mock_prepare_config.assert_called_once()
281+
mock_write_json.assert_called_once_with(
282+
f"{LIBRARIAN_DIR}/configure-response.json", {"id": "prepared"}
283+
)
284+
285+
286+
def test_handle_configure_no_new_library(mocker):
287+
"""Tests that handle_configure fails if no new library is found."""
288+
mocker.patch("cli._read_json_file", return_value={"libraries": []})
289+
# The call to _prepare_new_library_config with an empty dict will raise a ValueError
290+
# because _get_library_id will fail.
291+
with pytest.raises(ValueError, match="Configuring a new library failed."):
292+
handle_configure()
293+
294+
295+
def test_get_new_library_config_found(mock_configure_request_data):
296+
"""Tests that the new library configuration is returned when found."""
297+
config = _get_new_library_config(mock_configure_request_data)
298+
assert config["id"] == "google-cloud-language"
299+
# Assert that the config is NOT modified
300+
assert "status" in config["apis"][0]
301+
302+
303+
def test_get_new_library_config_not_found():
304+
"""Tests that an empty dictionary is returned when no new library is found."""
305+
request_data = {
306+
"libraries": [
307+
{"id": "existing-library", "apis": [{"path": "path/v1", "status": "existing"}]},
308+
]
309+
}
310+
config = _get_new_library_config(request_data)
311+
assert config == {}
312+
313+
314+
def test_get_new_library_config_empty_input():
315+
"""Tests that an empty dictionary is returned for empty input."""
316+
config = _get_new_library_config({})
317+
assert config == {}
318+
319+
320+
def test_prepare_new_library_config():
321+
"""Tests the preparation of a new library's configuration."""
322+
raw_config = {
323+
"id": "google-cloud-language",
324+
"apis": [{"path": "google/cloud/language/v1", "status": "new"}],
325+
"source_roots": None,
326+
"preserve_regex": None,
327+
"remove_regex": None,
328+
}
329+
330+
prepared_config = _prepare_new_library_config(raw_config)
331+
332+
# Check that status is removed
333+
assert "status" not in prepared_config["apis"][0]
334+
# Check that defaults are added
335+
assert prepared_config["source_roots"] == ["packages/google-cloud-language"]
336+
assert "packages/google-cloud-language/CHANGELOG.md" in prepared_config["preserve_regex"]
337+
assert prepared_config["remove_regex"] == ["packages/google-cloud-language"]
338+
assert prepared_config["tag_format"] == "{{id}}-v{{version}}"
339+
340+
341+
def test_prepare_new_library_config_preserves_existing_values():
342+
"""Tests that existing values in the config are not overwritten."""
343+
raw_config = {
344+
"id": "google-cloud-language",
345+
"apis": [{"path": "google/cloud/language/v1", "status": "new"}],
346+
"source_roots": ["packages/google-cloud-language-custom"],
347+
"preserve_regex": ["custom/regex"],
348+
"remove_regex": ["custom/remove"],
349+
"tag_format": "custom-format-{{version}}",
350+
}
351+
352+
prepared_config = _prepare_new_library_config(raw_config)
353+
354+
# Check that status is removed
355+
assert "status" not in prepared_config["apis"][0]
356+
# Check that existing values are preserved
357+
assert prepared_config["source_roots"] == ["packages/google-cloud-language-custom"]
358+
assert prepared_config["preserve_regex"] == ["custom/regex"]
359+
assert prepared_config["remove_regex"] == ["custom/remove"]
360+
assert prepared_config["tag_format"] == "custom-format-{{version}}"
361+
362+
239363
def test_get_library_id_success():
240364
"""Tests that _get_library_id returns the correct ID when present."""
241365
request_data = {"id": "test-library", "name": "Test Library"}
@@ -261,17 +385,6 @@ def test_get_library_id_empty_id():
261385
_get_library_id(request_data)
262386

263387

264-
def test_handle_configure_success(caplog, mock_generate_request_file):
265-
"""
266-
Tests the successful execution path of handle_configure.
267-
"""
268-
caplog.set_level(logging.INFO)
269-
270-
handle_configure()
271-
272-
assert "'configure' command executed." in caplog.text
273-
274-
275388
def test_run_post_processor_success(mocker, caplog):
276389
"""
277390
Tests that the post-processor helper calls the correct command.

0 commit comments

Comments
 (0)