@@ -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
556583def _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+
601648def _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+
648764def _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
677807def _run_nox_sessions (library_id : str , repo : str ):
0 commit comments