feat: implement native asyncio support via Cross-Sync (#1509) · googleapis/google-cloud-python@f822fd7
1+# Copyright 2024 Google LLC
2+#
3+# Licensed under the Apache License, Version 2.0 (the "License");
4+# you may not use this file except in compliance with the License.
5+# You may obtain a copy of the License at
6+#
7+# http://www.apache.org/licenses/LICENSE-2.0
8+#
9+# Unless required by applicable law or agreed to in writing, software
10+# distributed under the License is distributed on an "AS IS" BASIS,
11+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+# See the License for the specific language governing permissions and
13+# limitations under the License.
14+from __future__ import annotations
15+from typing import Sequence
16+import ast
17+"""
18+Entrypoint for initiating an async -> sync conversion using CrossSync
19+20+Finds all python files rooted in a given directory, and uses
21+transformers.CrossSyncFileProcessor to handle any files marked with
22+__CROSS_SYNC_OUTPUT__
23+"""
24+25+26+def extract_header_comments(file_path) -> str:
27+"""
28+ Extract the file header. Header is defined as the top-level
29+ comments before any code or imports
30+ """
31+header = []
32+with open(file_path, "r", encoding="utf-8-sig") as f:
33+for line in f:
34+if line.startswith("#") or line.strip() == "":
35+header.append(line)
36+else:
37+break
38+header.append("\n# This file is automatically generated by CrossSync. Do not edit manually.\n\n")
39+return "".join(header)
40+41+42+class CrossSyncOutputFile:
43+44+def __init__(self, output_path: str, ast_tree, header: str | None = None):
45+self.output_path = output_path
46+self.tree = ast_tree
47+self.header = header or ""
48+49+def render(self, with_formatter=True, save_to_disk: bool = True) -> str:
50+"""
51+ Render the file to a string, and optionally save to disk
52+53+ Args:
54+ with_formatter: whether to run the output through black before returning
55+ save_to_disk: whether to write the output to the file path
56+ """
57+full_str = self.header + ast.unparse(self.tree)
58+if with_formatter:
59+import black # type: ignore
60+import autoflake # type: ignore
61+62+full_str = black.format_str(
63+autoflake.fix_code(full_str, remove_all_unused_imports=True),
64+mode=black.FileMode(),
65+ )
66+if save_to_disk:
67+import os
68+os.makedirs(os.path.dirname(self.output_path), exist_ok=True)
69+with open(self.output_path, "w") as f:
70+f.write(full_str)
71+return full_str
72+73+74+def convert_files_in_dir(directory: str) -> set[CrossSyncOutputFile]:
75+import glob
76+from transformers import CrossSyncFileProcessor
77+78+# find all python files in the directory
79+files = glob.glob(directory + "/**/*.py", recursive=True)
80+# keep track of the output files pointed to by the annotated classes
81+artifacts: set[CrossSyncOutputFile] = set()
82+file_transformer = CrossSyncFileProcessor()
83+# run each file through ast transformation to find all annotated classes
84+for file_path in files:
85+with open(file_path, encoding="utf-8-sig") as f:
86+ast_tree = ast.parse(f.read())
87+output_path = file_transformer.get_output_path(ast_tree)
88+if output_path is not None:
89+# contains __CROSS_SYNC_OUTPUT__ annotation
90+converted_tree = file_transformer.visit(ast_tree)
91+header = extract_header_comments(file_path)
92+artifacts.add(CrossSyncOutputFile(output_path, converted_tree, header))
93+# return set of output artifacts
94+return artifacts
95+96+97+def save_artifacts(artifacts: Sequence[CrossSyncOutputFile]):
98+for a in artifacts:
99+a.render(save_to_disk=True)
100+101+102+if __name__ == "__main__":
103+import sys
104+105+if len(sys.argv) < 2:
106+print("Usage: python .cross_sync/generate.py <directory>")
107+sys.exit(1)
108+109+search_root = sys.argv[1]
110+outputs = convert_files_in_dir(search_root)
111+print(f"Generated {len(outputs)} artifacts: {[a.output_path for a in outputs]}")
112+save_artifacts(outputs)