Skip to content

Commit 7088dbf

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

File tree

3 files changed

+287
-29
lines changed

3 files changed

+287
-29
lines changed

.generator/cli.py

Lines changed: 136 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, we'll need to run isort and black manually on the output.
305+
# This happens 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,27 @@ 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+
generator_options (List[str]): Extracted generator options.
619+
620+
Returns:
621+
str: The complete protoc command string suitable for shell execution.
622+
"""
623+
command_parts = [
624+
f"protoc {api_path}/*.proto",
625+
f"--python_out={tmp_dir}",
626+
f"--pyi_out={tmp_dir}",
627+
]
628+
629+
return " ".join(command_parts)
630+
631+
601632
def _determine_generator_command(
602633
api_path: str, tmp_dir: str, generator_options: List[str]
603634
) -> str:
@@ -626,7 +657,7 @@ def _determine_generator_command(
626657
return " ".join(command_parts)
627658

628659

629-
def _run_generator_command(generator_command: str, source: str):
660+
def _run_protoc_command(generator_command: str, source: str):
630661
"""
631662
Executes the protoc generation command using subprocess.
632663
@@ -645,6 +676,82 @@ def _run_generator_command(generator_command: str, source: str):
645676
)
646677

647678

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

667772
with tempfile.TemporaryDirectory() as tmp_dir:
668-
generator_command = _determine_generator_command(
669-
api_path, tmp_dir, generator_options
773+
# 1. Determine the command for code generation
774+
if is_proto_only_library:
775+
command = _construct_protoc_command(api_path, tmp_dir)
776+
else:
777+
generator_options = _get_api_generator_options(
778+
api_path, py_gapic_config, gapic_version=gapic_version
779+
)
780+
command = _determine_generator_command(api_path, tmp_dir, generator_options)
781+
782+
# 2. Execute the code generation command
783+
_run_protoc_command(command, source)
784+
785+
# 3. Determine staging location
786+
staging_child_directory = _get_staging_child_directory(
787+
api_path, is_proto_only_library
788+
)
789+
staging_dir = os.path.join(
790+
output, "owl-bot-staging", library_id, staging_child_directory
670791
)
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)
792+
793+
# 4. Stage the generated code
794+
if is_proto_only_library:
795+
_stage_proto_only_library(api_path, source, tmp_dir, staging_dir)
796+
else:
797+
_stage_gapic_library(tmp_dir, staging_dir)
675798

676799

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

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