Skip to content

Commit a0b97b0

Browse files
committed
Merge branch 'add-support-for-proto-only-library' into onboard-google-cloud-audit-log
2 parents b308dd7 + 75a3146 commit a0b97b0

File tree

4 files changed

+300
-29
lines changed

4 files changed

+300
-29
lines changed

.generator/cli.py

Lines changed: 143 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,13 @@ def _run_post_processor(output: str, library_id: str):
300300
python_mono_repo.owlbot_main(path_to_library)
301301
else:
302302
raise SYNTHTOOL_IMPORT_ERROR # pragma: NO COVER
303+
304+
# If there is no noxfile, run `isort`` and `black` on the output.
305+
# This is required for proto-only libraries which are not GAPIC.
306+
if not Path(f"{output}/{path_to_library}/noxfile.py").exists():
307+
subprocess.run(["isort", output])
308+
subprocess.run(["black", output])
309+
303310
logger.info("Python post-processor ran successfully.")
304311

305312

@@ -323,6 +330,23 @@ def _copy_files_needed_for_post_processing(output: str, input: str, library_id:
323330
os.makedirs(
324331
f"{output}/{path_to_library}/scripts/client-post-processing", exist_ok=True
325332
)
333+
# TODO(https://github.com/googleapis/synthtool/pull/2126): Remove once this PR is merged
334+
# This is needed to avoid the following error for proto-only libraries
335+
# Traceback (most recent call last):
336+
# File "/app/./cli.py", line 535, in handle_generate
337+
# _run_post_processor(output, library_id)
338+
# File "/app/./cli.py", line 300, in _run_post_processor
339+
# python_mono_repo.owlbot_main(path_to_library)
340+
# File "/usr/local/lib/python3.9/site-packages/synthtool/languages/python_mono_repo.py", line 310, in owlbot_main
341+
# create_symlink_docs_readme(package_dir)
342+
# File "/usr/local/lib/python3.9/site-packages/synthtool/languages/python_mono_repo.py", line 102, in create_symlink_docs_readme
343+
# create_symlink_in_docs_dir(package_dir, "README.rst")
344+
# File "/usr/local/lib/python3.9/site-packages/synthtool/languages/python_mono_repo.py", line 82, in create_symlink_in_docs_dir
345+
# os.chdir(f"{package_dir}/docs")
346+
# FileNotFoundError: [Errno 2] No such file or directory: 'packages/google-cloud-access-context-manager/docs'
347+
os.makedirs(
348+
f"{output}/{path_to_library}/docs", exist_ok=True
349+
)
326350
# TODO(https://github.com/googleapis/librarian/issues/2334):
327351
# if `.repo-metadata.json` for a library exists in
328352
# `.librarian/generator-input`, then we override the generated `.repo-metadata.json`
@@ -541,16 +565,19 @@ def _read_bazel_build_py_rule(api_path: str, source: str) -> Dict:
541565
source (str): Path to the directory containing API protos.
542566
543567
Returns:
544-
Dict: A dictionary containing the parsed attributes of the `_py_gapic` rule.
568+
Dict: A dictionary containing the parsed attributes of the `_py_gapic` rule, if found.
545569
"""
546570
build_file_path = f"{source}/{api_path}/BUILD.bazel"
547571
content = _read_text_file(build_file_path)
548572

549573
result = parse_googleapis_content.parse_content(content)
550574
py_gapic_entries = [key for key in result.keys() if key.endswith("_py_gapic")]
551575

552-
# Assuming only one _py_gapic rule per BUILD file for a given language
553-
return result[py_gapic_entries[0]]
576+
# Assuming at most one _py_gapic rule per BUILD file for a given language
577+
if len(py_gapic_entries) > 0:
578+
return result[py_gapic_entries[0]]
579+
else:
580+
return {}
554581

555582

556583
def _get_api_generator_options(
@@ -598,6 +625,26 @@ def _get_api_generator_options(
598625
return generator_options
599626

600627

628+
def _construct_protoc_command(api_path: str, tmp_dir: str) -> str:
629+
"""
630+
Constructs the full protoc command string.
631+
632+
Args:
633+
api_path (str): The relative path to the API directory.
634+
tmp_dir (str): The temporary directory for protoc output.
635+
636+
Returns:
637+
str: The complete protoc command string suitable for shell execution.
638+
"""
639+
command_parts = [
640+
f"protoc {api_path}/*.proto",
641+
f"--python_out={tmp_dir}",
642+
f"--pyi_out={tmp_dir}",
643+
]
644+
645+
return " ".join(command_parts)
646+
647+
601648
def _determine_generator_command(
602649
api_path: str, tmp_dir: str, generator_options: List[str]
603650
) -> str:
@@ -626,7 +673,7 @@ def _determine_generator_command(
626673
return " ".join(command_parts)
627674

628675

629-
def _run_generator_command(generator_command: str, source: str):
676+
def _run_protoc_command(generator_command: str, source: str):
630677
"""
631678
Executes the protoc generation command using subprocess.
632679
@@ -645,6 +692,75 @@ def _run_generator_command(generator_command: str, source: str):
645692
)
646693

647694

695+
def _get_staging_child_directory(api_path: str, is_proto_only_library: bool) -> str:
696+
"""
697+
Determines the correct sub-path within 'owl-bot-staging' for the generated code.
698+
699+
For proto-only libraries, the structure is usually just the proto directory,
700+
e.g., 'thing-py/google/thing'.
701+
For GAPIC libraries, it's typically the version segment, e.g., 'v1'.
702+
703+
Args:
704+
api_path (str): The relative path to the API directory (e.g., 'google/cloud/language/v1').
705+
is_proto_only_library(bool): True, if this is a proto-only library.
706+
707+
Returns:
708+
str: The sub-directory name to use for staging.
709+
"""
710+
711+
version_candidate = api_path.split("/")[-1]
712+
if version_candidate.startswith("v") and not is_proto_only_library:
713+
return version_candidate
714+
else:
715+
# Fallback for non-'v' version segment
716+
return f"{os.path.basename(api_path)}-py/{api_path}"
717+
718+
719+
def _stage_proto_only_library(
720+
api_path: str, source_dir: str, tmp_dir: str, staging_dir: str
721+
) -> None:
722+
"""
723+
Handles staging for proto-only libraries (e.g., common protos).
724+
725+
This involves copying the generated python files and the original proto files.
726+
727+
Args:
728+
api_path (str): The relative path to the API directory.
729+
source_dir (str): Path to the directory containing API protos.
730+
tmp_dir (str): The temporary directory where protoc output was placed.
731+
staging_dir (str): The final destination for the staged code.
732+
"""
733+
# 1. Copy the generated Python files (e.g., *_pb2.py) from the protoc output
734+
# The generated Python files are placed under a directory corresponding to `api_path`
735+
# inside the temporary directory, since the protoc command ran with `api_path`
736+
# specified.
737+
shutil.copytree(f"{tmp_dir}/{api_path}", staging_dir, dirs_exist_ok=True)
738+
739+
# 2. Copy the original proto files to the staging directory
740+
# This is typically done for proto-only libraries so that the protos are included
741+
# in the distributed package.
742+
proto_glob_path = f"{source_dir}/{api_path}/*.proto"
743+
for proto_file in glob.glob(proto_glob_path):
744+
# The glob is expected to find the file inside the source_dir.
745+
# We copy only the filename to the target staging directory.
746+
shutil.copyfile(proto_file, f"{staging_dir}/{os.path.basename(proto_file)}")
747+
748+
749+
def _stage_gapic_library(tmp_dir: str, staging_dir: str) -> None:
750+
"""
751+
Handles staging for GAPIC client libraries.
752+
753+
This involves copying all contents from the temporary output directory.
754+
755+
Args:
756+
tmp_dir (str): The temporary directory where protoc/GAPIC generator output was placed.
757+
staging_dir (str): The final destination for the staged code.
758+
"""
759+
# For GAPIC, the generator output is flat in `tmp_dir` and includes all
760+
# necessary files like setup.py, client library, etc.
761+
shutil.copytree(tmp_dir, staging_dir)
762+
763+
648764
def _generate_api(
649765
api_path: str, library_id: str, source: str, output: str, gapic_version: str
650766
):
@@ -660,18 +776,32 @@ def _generate_api(
660776
in a format which follows PEP-440.
661777
"""
662778
py_gapic_config = _read_bazel_build_py_rule(api_path, source)
663-
generator_options = _get_api_generator_options(
664-
api_path, py_gapic_config, gapic_version=gapic_version
665-
)
779+
is_proto_only_library = len(py_gapic_config) == 0
666780

667781
with tempfile.TemporaryDirectory() as tmp_dir:
668-
generator_command = _determine_generator_command(
669-
api_path, tmp_dir, generator_options
782+
# 1. Determine the command for code generation
783+
if is_proto_only_library:
784+
command = _construct_protoc_command(api_path, tmp_dir)
785+
else:
786+
generator_options = _get_api_generator_options(
787+
api_path, py_gapic_config, gapic_version=gapic_version
788+
)
789+
command = _determine_generator_command(api_path, tmp_dir, generator_options)
790+
791+
# 2. Execute the code generation command
792+
_run_protoc_command(command, source)
793+
794+
# 3. Determine staging location
795+
staging_child_directory = _get_staging_child_directory(api_path, is_proto_only_library)
796+
staging_dir = os.path.join(
797+
output, "owl-bot-staging", library_id, staging_child_directory
670798
)
671-
_run_generator_command(generator_command, source)
672-
api_version = api_path.split("/")[-1]
673-
staging_dir = os.path.join(output, "owl-bot-staging", library_id, api_version)
674-
shutil.copytree(tmp_dir, staging_dir)
799+
800+
# 4. Stage the generated code
801+
if is_proto_only_library:
802+
_stage_proto_only_library(api_path, source, tmp_dir, staging_dir)
803+
else:
804+
_stage_gapic_library(tmp_dir, staging_dir)
675805

676806

677807
def _run_nox_sessions(library_id: str, repo: str):

.generator/requirements-test.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ pytest-mock
1818
gcp-synthtool @ git+https://github.com/googleapis/synthtool@5aa438a342707842d11fbbb302c6277fbf9e4655
1919
starlark-pyo3>=2025.1
2020
build
21+
black==23.7.0
22+
isort==5.11.0

.generator/requirements.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ gapic-generator>=1.27.0
33
nox
44
starlark-pyo3>=2025.1
55
build
6+
black==23.7.0
7+
isort==5.11.0

0 commit comments

Comments
 (0)