Skip to content

Commit 9cb3dac

Browse files
committed
chore(librarian): add support for proto-only library
1 parent cb583ad commit 9cb3dac

File tree

4 files changed

+281
-29
lines changed

4 files changed

+281
-29
lines changed

.generator/cli.py

Lines changed: 128 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

@@ -541,16 +548,19 @@ def _read_bazel_build_py_rule(api_path: str, source: str) -> Dict:
541548
source (str): Path to the directory containing API protos.
542549
543550
Returns:
544-
Dict: A dictionary containing the parsed attributes of the `_py_gapic` rule.
551+
Dict: A dictionary containing the parsed attributes of the `_py_gapic` rule, if found.
545552
"""
546553
build_file_path = f"{source}/{api_path}/BUILD.bazel"
547554
content = _read_text_file(build_file_path)
548555

549556
result = parse_googleapis_content.parse_content(content)
550557
py_gapic_entries = [key for key in result.keys() if key.endswith("_py_gapic")]
551558

552-
# Assuming only one _py_gapic rule per BUILD file for a given language
553-
return result[py_gapic_entries[0]]
559+
# Assuming at most one _py_gapic rule per BUILD file for a given language
560+
if len(py_gapic_entries) > 0:
561+
return result[py_gapic_entries[0]]
562+
else:
563+
return {}
554564

555565

556566
def _get_api_generator_options(
@@ -598,6 +608,26 @@ def _get_api_generator_options(
598608
return generator_options
599609

600610

611+
def _construct_protoc_command(api_path: str, tmp_dir: str) -> str:
612+
"""
613+
Constructs the full protoc command string.
614+
615+
Args:
616+
api_path (str): The relative path to the API directory.
617+
tmp_dir (str): The temporary directory for protoc output.
618+
619+
Returns:
620+
str: The complete protoc command string suitable for shell execution.
621+
"""
622+
command_parts = [
623+
f"protoc {api_path}/*.proto",
624+
f"--python_out={tmp_dir}",
625+
f"--pyi_out={tmp_dir}",
626+
]
627+
628+
return " ".join(command_parts)
629+
630+
601631
def _determine_generator_command(
602632
api_path: str, tmp_dir: str, generator_options: List[str]
603633
) -> str:
@@ -626,7 +656,7 @@ def _determine_generator_command(
626656
return " ".join(command_parts)
627657

628658

629-
def _run_generator_command(generator_command: str, source: str):
659+
def _run_protoc_command(generator_command: str, source: str):
630660
"""
631661
Executes the protoc generation command using subprocess.
632662
@@ -645,6 +675,75 @@ def _run_generator_command(generator_command: str, source: str):
645675
)
646676

647677

678+
def _get_staging_child_directory(api_path: str, is_proto_only: bool) -> str:
679+
"""
680+
Determines the correct sub-path within 'owl-bot-staging' for the generated code.
681+
682+
For proto-only libraries, the structure is usually just the proto directory,
683+
e.g., 'google/protobuf/struct'.
684+
For GAPIC libraries, it's typically the version segment, e.g., 'v1'.
685+
686+
Args:
687+
api_path (str): The relative path to the API directory (e.g., 'google/cloud/language/v1').
688+
is_proto_only (bool): True if the library is proto-only (no GAPIC rule).
689+
690+
Returns:
691+
str: The sub-directory name to use for staging.
692+
"""
693+
694+
version_candidate = api_path.split("/")[-1]
695+
if version_candidate.startswith("v"):
696+
return version_candidate
697+
else:
698+
# Fallback for non-'v' version segment
699+
return f"{os.path.basename(api_path)}-py/{api_path}"
700+
701+
702+
def _stage_proto_only_library(
703+
api_path: str, source_dir: str, tmp_dir: str, staging_dir: str
704+
) -> None:
705+
"""
706+
Handles staging for proto-only libraries (e.g., common protos).
707+
708+
This involves copying the generated python files and the original proto files.
709+
710+
Args:
711+
api_path (str): The relative path to the API directory.
712+
source_dir (str): Path to the directory containing API protos.
713+
tmp_dir (str): The temporary directory where protoc output was placed.
714+
staging_dir (str): The final destination for the staged code.
715+
"""
716+
# 1. Copy the generated Python files (e.g., *_pb2.py) from the protoc output
717+
# The generated Python files are placed under a directory corresponding to `api_path`
718+
# inside the temporary directory, since the protoc command ran with `api_path`
719+
# specified.
720+
shutil.copytree(f"{tmp_dir}/{api_path}", staging_dir, dirs_exist_ok=True)
721+
722+
# 2. Copy the original proto files to the staging directory
723+
# This is typically done for proto-only libraries so that the protos are included
724+
# in the distributed package.
725+
proto_glob_path = f"{source_dir}/{api_path}/*.proto"
726+
for proto_file in glob.glob(proto_glob_path):
727+
# The glob is expected to find the file inside the source_dir.
728+
# We copy only the filename to the target staging directory.
729+
shutil.copyfile(proto_file, f"{staging_dir}/{os.path.basename(proto_file)}")
730+
731+
732+
def _stage_gapic_library(tmp_dir: str, staging_dir: str) -> None:
733+
"""
734+
Handles staging for GAPIC client libraries.
735+
736+
This involves copying all contents from the temporary output directory.
737+
738+
Args:
739+
tmp_dir (str): The temporary directory where protoc/GAPIC generator output was placed.
740+
staging_dir (str): The final destination for the staged code.
741+
"""
742+
# For GAPIC, the generator output is flat in `tmp_dir` and includes all
743+
# necessary files like setup.py, client library, etc.
744+
shutil.copytree(tmp_dir, staging_dir)
745+
746+
648747
def _generate_api(
649748
api_path: str, library_id: str, source: str, output: str, gapic_version: str
650749
):
@@ -660,18 +759,34 @@ def _generate_api(
660759
in a format which follows PEP-440.
661760
"""
662761
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-
)
762+
is_proto_only_library = len(py_gapic_config) == 0
666763

667764
with tempfile.TemporaryDirectory() as tmp_dir:
668-
generator_command = _determine_generator_command(
669-
api_path, tmp_dir, generator_options
765+
# 1. Determine the command for code generation
766+
if is_proto_only_library:
767+
command = _construct_protoc_command(api_path, tmp_dir)
768+
else:
769+
generator_options = _get_api_generator_options(
770+
api_path, py_gapic_config, gapic_version=gapic_version
771+
)
772+
command = _determine_generator_command(api_path, tmp_dir, generator_options)
773+
774+
# 2. Execute the code generation command
775+
_run_protoc_command(command, source)
776+
777+
# 3. Determine staging location
778+
staging_child_directory = _get_staging_child_directory(
779+
api_path, is_proto_only_library
780+
)
781+
staging_dir = os.path.join(
782+
output, "owl-bot-staging", library_id, staging_child_directory
670783
)
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)
784+
785+
# 4. Stage the generated code
786+
if is_proto_only_library:
787+
_stage_proto_only_library(api_path, source, tmp_dir, staging_dir)
788+
else:
789+
_stage_gapic_library(tmp_dir, staging_dir)
675790

676791

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