33import argparse
44import os
55import libcst as cst
6- from itertools import chain
6+ import pathlib
7+ import sys
78from typing import (Any, Callable, Dict, List, Sequence, Tuple)
89
910
@@ -21,14 +22,18 @@ def partition(
2122 return results[1], results[0]
2223
2324
24- class {{ service.client_name }}CallTransformer(cst.CSTTransformer):
25+ class {{ api.naming.module_name }}CallTransformer(cst.CSTTransformer):
2526 CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata')
26-
27+ {% with all_methods = [] -%}
28+ {% for service in api .services .values () %}{% for method in service .methods .values () -%}
29+ {% do all_methods .append (method ) -%}
30+ {% endfor %}{% endfor -%}
2731 METHOD_TO_PARAMS: Dict[str, Tuple[str]] = {
28- {% for method in service . methods . values ( ) -%}
32+ {% for method in all_methods | sort ( attribute = 'name' )| unique ( attribute = 'name' ) -%}
2933 '{{ method.name|snake_case }}': ({% for field in method .legacy_flattened_fields .values () %} '{{ field.name }}', {% endfor %} ),
30- {% endfor -%}
34+ {% endfor -%}
3135 }
36+ {% endwith %}
3237
3338 def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode:
3439 try:
@@ -59,11 +64,11 @@ class {{ service.client_name }}CallTransformer(cst.CSTTransformer):
5964 value=cst.Dict([
6065 cst.DictElement(
6166 cst.SimpleString("'{}'".format(name)),
62- {# Inline comments and formatting are currently stripped out. #}
63- {# My current attempts at preverving comments and formatting #}
64- {# keep the comments, but the formatting is run through a log #}
65- {# chipper, and an extra comma gets added, which causes a #}
66- {# parse error. #}
67+ {# Inline comments and formatting are currently stripped out. - #}
68+ {# My current attempts at preverving comments and formatting - #}
69+ {# keep the comments, but the formatting is run through a log - #}
70+ {# chipper, and an extra comma gets added, which causes a - #}
71+ {# parse error. - #}
6772 cst.Element(value=arg.value)
6873 )
6974 # Note: the args + kwargs looks silly, but keep in mind that
@@ -79,31 +84,45 @@ class {{ service.client_name }}CallTransformer(cst.CSTTransformer):
7984
8085
8186def fix_files(
82- dirs: Sequence[str] ,
83- files: Sequence[str] ,
87+ in_dir: pathlib.Path ,
88+ out_dir: pathlib.Path ,
8489 *,
85- transformer={{ service.client_name }}CallTransformer(),
90+ transformer={{ api.naming.module_name }}CallTransformer(),
8691):
87- pyfile_gen = chain(
88- (os.path.join(root, f)
89- for d in dirs
90- for root, _, files in os.walk(d)
91- for f in files if os.path.splitext(f)[1] == ".py"),
92- files)
92+ """Duplicate the input dir to the output dir, fixing file method calls.
93+
94+ Preconditions:
95+ * in_dir is a real directory
96+ * out_dir is a real, empty directory
97+ """
98+ pyfile_gen = (
99+ pathlib.Path(os.path.join(root, f))
100+ for root, _, files in os.walk(in_dir)
101+ for f in files if os.path.splitext(f)[1] == ".py"
102+ )
93103
94104 for fpath in pyfile_gen:
95- with open(fpath, 'r+ ') as f:
105+ with open(fpath, 'r') as f:
96106 src = f.read()
97- tree = cst.parse_module(src)
98- updated = tree.visit(transformer)
99- f.seek(0)
100- f.truncate()
107+
108+ # Parse the code and insert method call fixes.
109+ tree = cst.parse_module(src)
110+ updated = tree.visit(transformer)
111+
112+ # Create the path and directory structure for the new file.
113+ updated_path = out_dir.joinpath(fpath.relative_to(in_dir))
114+ updated_path.parent.mkdir(parents=True, exist_ok=True)
115+
116+ # Generate the updated source file at the corresponding path.
117+ with open(updated_path, 'w') as f:
101118 f.write(updated.code)
102119
103120
104121if __name__ == '__main__':
105122 parser = argparse.ArgumentParser(
106- description="""Fix up source that uses the {{ service.name }} client library.
123+ description="""Fix up source that uses the {{ api.naming.module_name }} client library.
124+
125+ The existing sources are NOT overwritten but are copied to output_dir with changes made.
107126
108127Note: This tool operates at a best-effort level at converting positional
109128 parameters in client method calls to keyword based parameters.
@@ -114,32 +133,40 @@ Note: This tool operates at a best-effort level at converting positional
114133
115134 These all constitute false negatives. The tool will also detect false
116135 positives when an API method shares a name with another method.
117-
118- Be sure to back up your source files before running this tool and to compare the diffs.
119136""")
120- group = parser.add_mutually_exclusive_group(required=True)
121- group.add_argument(
137+ parser.add_argument(
122138 '-d',
123- metavar='dir',
124- dest='dirs',
125- action='append',
126- help='a directory to walk for python files to fix up',
139+ dest='input_dir',
140+ help='the input directory to walk for python files to fix up',
127141 )
128- group.add_argument(
129- '-f',
130- metavar='file',
131- dest='files',
132- action='append',
142+ parser.add_argument(
143+ '-o',
144+ dest='output_dir',
133145 help='a file to fix up via un-flattening',
134146 )
135147 args = parser.parse_args()
136- print(
137- """It is strongly, strongly recommended that you commit outstanding changes and
138- back up your source tree before running this conversion script.
139- Please take a moment to do that now if you haven't already.
140- """
141- )
142- resp = input("Really attempt to convert sources? yes/[no]: ")
143- if resp == "yes":
144- fix_files(args.dirs or [], args.files or [])
148+ input_dir = pathlib.Path(args.input_dir)
149+ output_dir = pathlib.Path(args.output_dir)
150+ if not input_dir.is_dir():
151+ print(
152+ f"input directory '{input_dir}' does not exist or is not a directory",
153+ file=sys.stderr,
154+ )
155+ sys.exit(-1)
156+
157+ if not output_dir.is_dir():
158+ print(
159+ f"output directory '{output_dir}' does not exist or is not a directory",
160+ file=sys.stderr,
161+ )
162+ sys.exit(-1)
163+
164+ if os.listdir(output_dir):
165+ print(
166+ f"output directory '{output_dir}' is not empty",
167+ file=sys.stderr,
168+ )
169+ sys.exit(-1)
170+
171+ fix_files(input_dir, output_dir)
145172{% endblock %}
0 commit comments