Skip to content

Commit 1161d08

Browse files
committed
Improve generate script
- Fix issue with __pycache__ dirs getting picked up - parallelise code generation with asyncio for 3x speedup - silence protoc output unless -v option is supplied - Use pathlib ;)
1 parent 4b6f55d commit 1161d08

File tree

3 files changed

+84
-77
lines changed

3 files changed

+84
-77
lines changed

betterproto/tests/generate.py

+62-46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
2-
import glob
2+
import asyncio
33
import os
4+
from pathlib import Path
45
import shutil
56
import subprocess
67
import sys
@@ -20,91 +21,106 @@
2021
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
2122

2223

23-
def clear_directory(path: str):
24-
for file_or_directory in glob.glob(os.path.join(path, "*")):
25-
if os.path.isdir(file_or_directory):
24+
def clear_directory(dir_path: Path):
25+
for file_or_directory in dir_path.glob("*"):
26+
if file_or_directory.is_dir():
2627
shutil.rmtree(file_or_directory)
2728
else:
28-
os.remove(file_or_directory)
29+
file_or_directory.unlink()
2930

3031

31-
def generate(whitelist: Set[str]):
32-
path_whitelist = {os.path.realpath(e) for e in whitelist if os.path.exists(e)}
33-
name_whitelist = {e for e in whitelist if not os.path.exists(e)}
32+
async def generate(whitelist: Set[str], verbose: bool):
33+
test_case_names = set(get_directories(inputs_path)) - {"__pycache__"}
3434

35-
test_case_names = set(get_directories(inputs_path))
36-
37-
failed_test_cases = []
35+
path_whitelist = set()
36+
name_whitelist = set()
37+
for item in whitelist:
38+
if item in test_case_names:
39+
name_whitelist.add(item)
40+
continue
41+
path_whitelist.add(item)
3842

43+
generation_tasks = []
3944
for test_case_name in sorted(test_case_names):
40-
test_case_input_path = os.path.realpath(
41-
os.path.join(inputs_path, test_case_name)
42-
)
43-
45+
test_case_input_path = inputs_path.joinpath(test_case_name).resolve()
4446
if (
4547
whitelist
46-
and test_case_input_path not in path_whitelist
48+
and str(test_case_input_path) not in path_whitelist
4749
and test_case_name not in name_whitelist
4850
):
4951
continue
52+
generation_tasks.append(
53+
generate_test_case_output(test_case_input_path, test_case_name, verbose)
54+
)
5055

51-
print(f"Generating output for {test_case_name}")
52-
try:
53-
generate_test_case_output(test_case_name, test_case_input_path)
54-
except subprocess.CalledProcessError as e:
55-
failed_test_cases.append(test_case_name)
56-
57-
if failed_test_cases:
58-
sys.stderr.write("\nFailed to generate the following test cases:\n")
59-
for failed_test_case in failed_test_cases:
60-
sys.stderr.write(f"- {failed_test_case}\n")
56+
# Wait for all subprocs and match any failures to names to report
57+
for test_case_name, result in zip(
58+
sorted(test_case_names), await asyncio.gather(*generation_tasks)
59+
):
60+
if result != 0:
61+
sys.stderr.write(f"\nFailed to generate test case: {test_case_name}\n")
6162

6263

63-
def generate_test_case_output(test_case_name, test_case_input_path=None):
64-
if not test_case_input_path:
65-
test_case_input_path = os.path.realpath(
66-
os.path.join(inputs_path, test_case_name)
67-
)
64+
async def generate_test_case_output(
65+
test_case_input_path: Path, test_case_name: str, verbose: bool
66+
) -> int:
67+
"""
68+
Returns the max of the subprocess return values
69+
"""
6870

69-
test_case_output_path_reference = os.path.join(
70-
output_path_reference, test_case_name
71-
)
72-
test_case_output_path_betterproto = os.path.join(
73-
output_path_betterproto, test_case_name
74-
)
71+
test_case_output_path_reference = output_path_reference.joinpath(test_case_name)
72+
test_case_output_path_betterproto = output_path_betterproto.joinpath(test_case_name)
7573

74+
print(f"Generating output for {test_case_name!r}")
7675
os.makedirs(test_case_output_path_reference, exist_ok=True)
7776
os.makedirs(test_case_output_path_betterproto, exist_ok=True)
7877

7978
clear_directory(test_case_output_path_reference)
8079
clear_directory(test_case_output_path_betterproto)
8180

82-
protoc_reference(test_case_input_path, test_case_output_path_reference)
83-
protoc_plugin(test_case_input_path, test_case_output_path_betterproto)
81+
subproc_out = None if verbose else open(os.devnull, "w")
82+
return_values = await asyncio.gather(
83+
(
84+
await protoc_reference(
85+
test_case_input_path, test_case_output_path_reference, out=subproc_out,
86+
)
87+
).wait(),
88+
(
89+
await protoc_plugin(
90+
test_case_input_path,
91+
test_case_output_path_betterproto,
92+
out=subproc_out,
93+
)
94+
).wait(),
95+
)
96+
return max(return_values)
8497

8598

8699
HELP = "\n".join(
87-
[
88-
"Usage: python generate.py",
89-
" python generate.py [DIRECTORIES or NAMES]",
100+
(
101+
"Usage: python generate.py [-h] [-v] [DIRECTORIES or NAMES]",
90102
"Generate python classes for standard tests.",
91103
"",
92104
"DIRECTORIES One or more relative or absolute directories of test-cases to generate classes for.",
93105
" python generate.py inputs/bool inputs/double inputs/enum",
94106
"",
95107
"NAMES One or more test-case names to generate classes for.",
96108
" python generate.py bool double enums",
97-
]
109+
)
98110
)
99111

100112

101113
def main():
102114
if set(sys.argv).intersection({"-h", "--help"}):
103115
print(HELP)
104116
return
105-
whitelist = set(sys.argv[1:])
106-
107-
generate(whitelist)
117+
if sys.argv[1:2] == ["-v"]:
118+
verbose = True
119+
whitelist = set(sys.argv[2:])
120+
else:
121+
verbose = False
122+
whitelist = set(sys.argv[1:])
123+
asyncio.get_event_loop().run_until_complete(generate(whitelist, verbose))
108124

109125

110126
if __name__ == "__main__":

betterproto/tests/test_inputs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
class TestCases:
2525
def __init__(self, path, services: Set[str], xfail: Set[str]):
26-
_all = set(get_directories(path))
26+
_all = set(get_directories(path)) - {"__pycache__"}
2727
_services = services
2828
_messages = _all - services
2929
_messages_with_json = {

betterproto/tests/util.py

+21-30
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1+
import asyncio
12
import os
2-
import subprocess
3-
from typing import Generator
3+
from pathlib import Path
4+
from typing import Generator, IO, Optional
45

56
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
67

7-
root_path = os.path.dirname(os.path.realpath(__file__))
8-
inputs_path = os.path.join(root_path, "inputs")
9-
output_path_reference = os.path.join(root_path, "output_reference")
10-
output_path_betterproto = os.path.join(root_path, "output_betterproto")
8+
root_path = Path(__file__).resolve().parent
9+
inputs_path = root_path.joinpath("inputs")
10+
output_path_reference = root_path.joinpath("output_reference")
11+
output_path_betterproto = root_path.joinpath("output_betterproto")
1112

1213
if os.name == "nt":
13-
plugin_path = os.path.join(root_path, "..", "plugin.bat")
14+
plugin_path = root_path.joinpath("..", "plugin.bat").resolve()
1415
else:
15-
plugin_path = os.path.join(root_path, "..", "plugin.py")
16+
plugin_path = root_path.joinpath("..", "plugin.py").resolve()
1617

1718

18-
def get_files(path, end: str) -> Generator[str, None, None]:
19+
def get_files(path, suffix: str) -> Generator[str, None, None]:
1920
for r, dirs, files in os.walk(path):
20-
for filename in [f for f in files if f.endswith(end)]:
21+
for filename in [f for f in files if f.endswith(suffix)]:
2122
yield os.path.join(r, filename)
2223

2324

@@ -27,36 +28,26 @@ def get_directories(path):
2728
yield directory
2829

2930

30-
def relative(file: str, path: str):
31-
return os.path.join(os.path.dirname(file), path)
32-
33-
34-
def read_relative(file: str, path: str):
35-
with open(relative(file, path)) as fh:
36-
return fh.read()
37-
38-
39-
def protoc_plugin(path: str, output_dir: str) -> subprocess.CompletedProcess:
40-
return subprocess.run(
31+
async def protoc_plugin(path: str, output_dir: str, out: IO = None):
32+
return await asyncio.create_subprocess_shell(
4133
f"protoc --plugin=protoc-gen-custom={plugin_path} --custom_out={output_dir} --proto_path={path} {path}/*.proto",
42-
shell=True,
43-
check=True,
34+
stderr=out,
4435
)
4536

4637

47-
def protoc_reference(path: str, output_dir: str):
48-
subprocess.run(
38+
async def protoc_reference(path: str, output_dir: str, out: IO = None):
39+
return await asyncio.create_subprocess_shell(
4940
f"protoc --python_out={output_dir} --proto_path={path} {path}/*.proto",
50-
shell=True,
41+
stderr=out,
5142
)
5243

5344

54-
def get_test_case_json_data(test_case_name, json_file_name=None):
45+
def get_test_case_json_data(test_case_name: str, json_file_name: Optional[str] = None):
5546
test_data_file_name = json_file_name if json_file_name else f"{test_case_name}.json"
56-
test_data_file_path = os.path.join(inputs_path, test_case_name, test_data_file_name)
47+
test_data_file_path = inputs_path.joinpath(test_case_name, test_data_file_name)
5748

58-
if not os.path.exists(test_data_file_path):
49+
if not test_data_file_path.exists():
5950
return None
6051

61-
with open(test_data_file_path) as fh:
52+
with test_data_file_path.open("r") as fh:
6253
return fh.read()

0 commit comments

Comments
 (0)