@@ -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
556566def _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+
601632def _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+
648755def _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
677800def _run_nox_sessions (library_id : str , repo : str ):
0 commit comments