1616import sys
1717import os
1818
19+ def is_call_to (node , name_parts ):
20+ """
21+ Checks if an AST node is a call to a specific function.
22+ name_parts: list of strings, e.g. ['s', 'move'] for s.move()
23+ or ['java', 'common_templates']
24+ """
25+ if not isinstance (node , ast .Call ):
26+ return False
27+
28+ func = node .func
29+ # Handle attribute access (e.g. s.move)
30+ if isinstance (func , ast .Attribute ):
31+ if len (name_parts ) == 2 :
32+ # Check object and attr
33+ obj = func .value
34+ if isinstance (obj , ast .Name ) and obj .id == name_parts [0 ] and func .attr == name_parts [1 ]:
35+ return True
36+ elif len (name_parts ) == 1 :
37+ if func .attr == name_parts [0 ]:
38+ return True
39+ # Handle direct name (e.g. if imported directly, though less common for these)
40+ elif isinstance (func , ast .Name ):
41+ if len (name_parts ) == 1 and func .id == name_parts [0 ]:
42+ return True
43+
44+ return False
45+
46+ def extract_excludes_from_call (call_node ):
47+ excludes = []
48+ for keyword in call_node .keywords :
49+ if keyword .arg == 'excludes' :
50+ if isinstance (keyword .value , ast .List ):
51+ for elt in keyword .value .elts :
52+ if isinstance (elt , ast .Constant ): # Python 3.8+
53+ excludes .append (elt .value )
54+ elif isinstance (elt , ast .Str ): # Python < 3.8
55+ excludes .append (elt .s )
56+ break
57+ return excludes
58+
1959def extract_info (source_code ):
20- """Extracts excludes and replacement calls from the source owlbot.py."""
2160 excludes = []
22- replacements = []
23-
61+ loop_body_lines = []
62+ top_level_lines = []
63+
2464 try :
2565 tree = ast .parse (source_code )
2666 except SyntaxError :
27- return excludes , replacements
28-
29- for node in ast .walk (tree ):
30- # Look for s.replace(...) calls
31- if isinstance (node , ast .Call ):
32- if isinstance (node .func , ast .Attribute ) and node .func .attr == 'replace' :
33- # We found a replace call. We want to reconstruct the source code for this call.
34- # However, ast.unparse is only available in Python 3.9+.
35- # If we are on an older python, we might need a workaround or just grab the line range.
36- # Since we likely have 3.9+, let's try ast.unparse if available, otherwise strict extraction.
37- if sys .version_info >= (3 , 9 ):
38- replacements .append (ast .unparse (node ))
39- else :
40- # Fallback for older python: just comment it needs manual migration or try simple extraction
41- # For now assume 3.9+ which is standard in this environment
42- pass
43-
44- # Look for java.common_templates or common.java_library calls to find excludes
45- if isinstance (node , ast .Call ):
46- is_common_templates = False
47- if isinstance (node .func , ast .Attribute ):
48- if node .func .attr == 'common_templates' :
49- is_common_templates = True
50- elif node .func .attr == 'java_library' : # older name
51- is_common_templates = True
67+ return excludes , top_level_lines , loop_body_lines
68+
69+ for node in tree .body :
70+ # Some nodes are wrapped in Expr, e.g. s.remove_staging_dirs()
71+ inner_node = node
72+ if isinstance (node , ast .Expr ):
73+ inner_node = node .value
74+
75+ # Ignore standard imports (we will inject them)
76+ if isinstance (node , (ast .Import , ast .ImportFrom )):
77+ # We assume we only care about synthtool/java imports which we regenerate.
78+ # If there are other imports, we should probably preserve them.
79+ # Heuristic: if it mentions 'synthtool', ignore it.
80+ if isinstance (node , ast .Import ):
81+ if any ('synthtool' in alias .name for alias in node .names ):
82+ continue
83+ if isinstance (node , ast .ImportFrom ):
84+ if node .module and 'synthtool' in node .module :
85+ continue
86+ # Preserve other imports
87+ if sys .version_info >= (3 , 9 ):
88+ top_level_lines .append (ast .unparse (node ))
89+ continue
90+
91+ # Check for java.common_templates (top level)
92+ if is_call_to (inner_node , ['java' , 'common_templates' ]) or is_call_to (inner_node , ['common' , 'java_library' ]):
93+ excludes .extend (extract_excludes_from_call (inner_node ))
94+ continue
95+
96+ # Check for s.remove_staging_dirs()
97+ if is_call_to (inner_node , ['s' , 'remove_staging_dirs' ]):
98+ continue
99+
100+ # Check for the main loop: for library in s.get_staging_dirs():
101+ if isinstance (node , ast .For ):
102+ is_staging_loop = False
103+ if isinstance (node .iter , ast .Call ):
104+ # Check for s.get_staging_dirs()
105+ if is_call_to (node .iter , ['s' , 'get_staging_dirs' ]):
106+ is_staging_loop = True
52107
53- if is_common_templates :
54- for keyword in node .keywords :
55- if keyword .arg == 'excludes' :
56- if isinstance (keyword .value , ast .List ):
57- for elt in keyword .value .elts :
58- if isinstance (elt , ast .Constant ): # Python 3.8+
59- excludes .append (elt .value )
60- elif isinstance (elt , ast .Str ): # Python < 3.8
61- excludes .append (elt .s )
62- break
63- return excludes , replacements
64-
65-
66- def generate_target_content (excludes , replacements , standard_excludes = None ):
67- # Default excludes for monorepo if no template is provided
108+ if is_staging_loop :
109+ # Extract body
110+ for child in node .body :
111+ child_inner = child
112+ if isinstance (child , ast .Expr ):
113+ child_inner = child .value
114+
115+ # Ignore s.move(library)
116+ if is_call_to (child_inner , ['s' , 'move' ]):
117+ continue
118+ # Check for nested common_templates (rare but possible)
119+ if is_call_to (child_inner , ['java' , 'common_templates' ]) or is_call_to (child_inner , ['common' , 'java_library' ]):
120+ excludes .extend (extract_excludes_from_call (child_inner ))
121+ continue
122+
123+ if sys .version_info >= (3 , 9 ):
124+ loop_body_lines .append (ast .unparse (child ))
125+ continue
126+ # else fall through to preserve other loops
127+
128+ # Preserve everything else (constants, functions, other logic)
129+ if sys .version_info >= (3 , 9 ):
130+ top_level_lines .append (ast .unparse (node ))
131+
132+ return excludes , top_level_lines , loop_body_lines
133+
134+ def generate_target_content (excludes , top_level_lines , loop_body_lines , standard_excludes = None ):
68135 if standard_excludes is None :
69136 standard_excludes = {
70137 ".github/*" ,
@@ -80,44 +147,47 @@ def generate_target_content(excludes, replacements, standard_excludes=None):
80147 ".gitignore"
81148 }
82149
83- # Merge excludes
84- # User requested to ignore source excludes and strictly use standard excludes
85150 final_excludes = sorted (list (set (standard_excludes )))
86-
87- # Format replacements with indentation
88- formatted_replacements = ""
89- for rep in replacements :
90- formatted_replacements += " " + rep + "\n "
91-
92- excludes_str = ",\n " .join ([f'"{ e } "' for e in final_excludes ])
93-
94- content = f"""# Copyright 2026 Google LLC
95- #
96- # Licensed under the Apache License, Version 2.0 (the "License");
97- # you may not use this file except in compliance with the License.
98- # You may obtain a copy of the License at
99- #
100- # https://www.apache.org/licenses/LICENSE-2.0
101- #
102- # Unless required by applicable law or agreed to in writing, software
103- # distributed under the License is distributed on an "AS IS" BASIS,
104- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
105- # See the License for the specific language governing permissions and
106- # limitations under the License.
151+ excludes_str = ",\n " .join ([f'"{ e } "' for e in final_excludes ])
107152
108- import synthtool as s
109- from synthtool.languages import java
153+ # Reconstruct content
154+ lines = []
155+ lines .append ("# Copyright 2026 Google LLC" )
156+ lines .append ("#" )
157+ lines .append ("# Licensed under the Apache License, Version 2.0 (the \" License\" );" )
158+ lines .append ("# you may not use this file except in compliance with the License." )
159+ lines .append ("# You may obtain a copy of the License at" )
160+ lines .append ("#" )
161+ lines .append ("# https://www.apache.org/licenses/LICENSE-2.0" )
162+ lines .append ("#" )
163+ lines .append ("# Unless required by applicable law or agreed to in writing, software" )
164+ lines .append ("# distributed under the License is distributed on an \" AS IS\" BASIS," )
165+ lines .append ("# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." )
166+ lines .append ("# See the License for the specific language governing permissions and" )
167+ lines .append ("# limitations under the License." )
168+ lines .append ("" )
169+ lines .append ("import synthtool as s" )
170+ lines .append ("from synthtool.languages import java" )
171+ lines .append ("" )
172+
173+ if top_level_lines :
174+ lines .extend (top_level_lines )
175+ lines .append ("" )
110176
111- for library in s.get_staging_dirs():
112- # put any special-case replacements here
113- s.move(library)
114- { formatted_replacements }
115- s.remove_staging_dirs()
116- java.common_templates(monorepo=True, excludes=[
117- { excludes_str }
118- ])
119- """
120- return content
177+ lines .append ("for library in s.get_staging_dirs():" )
178+ lines .append (" # put any special-case replacements here" )
179+ lines .append (" s.move(library)" )
180+ for l in loop_body_lines :
181+ # Indent loop body
182+ for sl in l .split ('\n ' ):
183+ lines .append (" " + sl )
184+
185+ lines .append ("s.remove_staging_dirs()" )
186+ lines .append (f"java.common_templates(monorepo=True, excludes=[" )
187+ lines .append (f" { excludes_str } " )
188+ lines .append ("])" )
189+
190+ return "\n " .join (lines ) + "\n "
121191
122192def main ():
123193 if len (sys .argv ) < 3 :
@@ -135,21 +205,22 @@ def main():
135205 with open (source_file , 'r' ) as f :
136206 source_code = f .read ()
137207
138- excludes , replacements = extract_info (source_code )
208+ excludes , top_level_lines , loop_body_lines = extract_info (source_code )
139209
140210 standard_excludes = None
141211 if template_file :
142212 if os .path .exists (template_file ):
143213 with open (template_file , 'r' ) as f :
144214 template_code = f .read ()
145- template_excludes , _ = extract_info (template_code )
215+ template_excludes , _ , _ = extract_info (template_code )
146216 standard_excludes = template_excludes
147217 else :
148- print (f"Template file { template_file } not found. using default excludes." )
218+ print (f"Template file { template_file } not found using default excludes." )
149219
150- target_content = generate_target_content (excludes , replacements , standard_excludes )
220+ target_content = generate_target_content (excludes , top_level_lines , loop_body_lines , standard_excludes )
151221
152- os .makedirs (os .path .dirname (target_file ), exist_ok = True )
222+ if os .path .dirname (target_file ):
223+ os .makedirs (os .path .dirname (target_file ), exist_ok = True )
153224 with open (target_file , 'w' ) as f :
154225 f .write (target_content )
155226
0 commit comments